-
Notifications
You must be signed in to change notification settings - Fork 319
settings: Migrate to new RadioGroup API; write widget tests for two just-added settings #1602
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
settings: Migrate to new RadioGroup API; write widget tests for two just-added settings #1602
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for taking care of this (including writing those tests)! All looks good; some nits and for-the-future musings below.
This also seems like definitely a good change in the upstream API, too — better structured to match how the UI really works (or at least should work, for radio buttons to be the right fit).
test/widgets/settings_test.dart
Outdated
for (final setting in VisitFirstUnreadSetting.values) { | ||
final thisSettingTitle = settingTitle(setting); | ||
checkRadioButtonAppearsChecked<VisitFirstUnreadSetting>(tester, | ||
thisSettingTitle, thisSettingTitle == settingTitle(expectedSetting)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comparison can be on the values directly instead of the labels, right?
for (final setting in VisitFirstUnreadSetting.values) { | |
final thisSettingTitle = settingTitle(setting); | |
checkRadioButtonAppearsChecked<VisitFirstUnreadSetting>(tester, | |
thisSettingTitle, thisSettingTitle == settingTitle(expectedSetting)); | |
for (final setting in VisitFirstUnreadSetting.values) { | |
final thisSettingTitle = settingTitle(setting); | |
checkRadioButtonAppearsChecked<VisitFirstUnreadSetting>(tester, | |
thisSettingTitle, setting == expectedSetting); |
testNavObserver = TestNavigatorObserver() | ||
..onPushed = ((route, _) => lastPushedRoute = route) | ||
..onPopped = ((route, _) => lastPoppedRoute = route); | ||
lastPushedRoute = null; | ||
lastPoppedRoute = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not for this PR, but: given how much this pattern is recurring now, I'm thinking it'd be good to centralize the code for it.
Perhaps by giving TestNavigatorObserver (or a subclass) some more structure, to track this sort of state itself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed; #1668
await tester.pump(); | ||
check(lastPushedRoute).isA<MaterialWidgetRoute>() | ||
.page.isA<VisitFirstUnreadSettingPage>(); | ||
await tester.pump((lastPushedRoute as TransitionRoute).transitionDuration); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(again not for this PR)
If the nav observer tracks a log of both pushes and pops in a single list (or I guess even just tracks the last single such action), then this could perhaps even look something like:
await tester.pump((lastPushedRoute as TransitionRoute).transitionDuration); | |
await tester.pump(navObserver.lastTransitionDuration); |
and exactly the same in the pop case (when the last thing that happened was a pop, the lastTransitionDuration
getter would look at its reverseTransitionDuration
).
test/widgets/settings_test.dart
Outdated
|
||
await tester.tap(findRadioListTileWithTitle<VisitFirstUnreadSetting>( | ||
settingTitle(VisitFirstUnreadSetting.always))); | ||
await tester.pumpAndSettle(); // TODO why doesn't just `pump` work? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, dunno. Does pump(Duration.zero)
work?
Looking at the implementation of setThemeSetting
, it'll wait for the (simulated) DB write to complete before it updates the within-Dart store. In real life that's some IO that will take a small but nonzero amount of time, so its completion callback won't happen in a microtask, which is what pump()
does. So it fits that that also wouldn't suffice in the test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah a regular tester.pump
does indeed work when I change to a semantics-based approach instead of checking what paints :)
test/widgets/settings_test.dart
Outdated
await tester.pumpAndSettle(); | ||
checkPage(tester, expectedSetting: VisitFirstUnreadSetting.never); | ||
|
||
await tester.tap(find.byType(BackButton)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fun alternative (see its docs) which I happened across recently while browsing other code:
await tester.tap(find.byType(BackButton)); | |
await tester.tap(find.backButton()); |
}); | ||
}); | ||
|
||
group('MarkReadOnScrollSetting', () { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we add one or two more of these, we'll likely want to try abstracting them out a bit more — the body of the test case is repetitive enough that it risks getting hard to read without glossing over.
But explicit is always a good way to write the first version, and I think it works fine for two of these.
Hmm, flutter/flutter@a5a3ab3 breaks the "check what paints" commit. I think probably the fix is to follow a Figma design for radio buttons, instead of using Material widgets. Such a frame exists: https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=5003-65595&t=yxYjWrPU6rpZqcUg-0 |
Hmm. That's probably a good change to do eventually anyway — but it shouldn't be a blocker for this API migration. The Material widgets should have a reasonable way to test a UI that uses them. I think one good answer would be to switch from inspecting the paint behavior to inspecting the semantics, as in SemanticsNode. That's what people using a screen-reader would encounter, and conveniently it's also by nature more structured. To explore a bit, I tried this edit to the tests: void checkRadioButtonAppearsChecked<T>(WidgetTester tester, String title, bool expectedIsChecked) {
final element = tester.element(findRadioListTileWithTitle<T>(title));
+ print(element.renderObject!.debugSemantics!.toStringDeep()); and then ran the tests. The first couple of trees printed looked like this:
so it looks like that |
Interesting; thanks for the |
ac6c750
to
4d1baae
Compare
(Pushed here a branch intended for #1662, oops; then restored the branch that had been here.) |
4d1baae
to
ffd094c
Compare
OK, updated, checking semantics this time. PTAL! 🙂 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! This approach looks good. One comment below.
test/widgets/settings_test.dart
Outdated
hasSelectedState: true, | ||
hasTapAction: true, | ||
hasFocusAction: true)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, annoying we have to specify all these flags that seem like not the point of these tests. But I see that's how matchesSemantics
behaves.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
… Oho: how about containsSemantics
? Then I think this whole check statement becomes:
check(tester.semantics.find(findRadioListTileWithTitle<T>(title)))
.containsSemantics(
label: subtitle == null ? title : '$title\n$subtitle',
isInMutuallyExclusiveGroup: true,
hasCheckedState: true, isChecked: expectedIsChecked);
This tests the observable behavior more directly. RadioListTile.checked has been deprecated (zulip#1545), so we'd like this test to stop relying on that implementation detail.
"Open message feeds at" and "Mark messages as read on scroll". Related: zulip#1571 Related: zulip#1583
This migration is verified by the tests that we touched in the previous commits; they ensure that the buttons' checked state still updates visibly. Fixes: zulip#1545
ffd094c
to
5f9512e
Compare
Thanks! Revision pushed. |
Fixes #1545.
Fixes part of: