Skip to content

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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

chrisbobbe
Copy link
Collaborator

@chrisbobbe chrisbobbe requested a review from gnprice June 17, 2025 21:23
@chrisbobbe chrisbobbe added the integration review Added by maintainers when PR may be ready for integration label Jun 17, 2025
Copy link
Member

@gnprice gnprice left a 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).

Comment on lines 177 to 180
for (final setting in VisitFirstUnreadSetting.values) {
final thisSettingTitle = settingTitle(setting);
checkRadioButtonAppearsChecked<VisitFirstUnreadSetting>(tester,
thisSettingTitle, thisSettingTitle == settingTitle(expectedSetting));
Copy link
Member

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?

Suggested change
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);

Comment on lines +30 to +33
testNavObserver = TestNavigatorObserver()
..onPushed = ((route, _) => lastPushedRoute = route)
..onPopped = ((route, _) => lastPoppedRoute = route);
lastPushedRoute = null;
lastPoppedRoute = null;
Copy link
Member

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.

Copy link
Collaborator Author

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);
Copy link
Member

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:

Suggested change
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).


await tester.tap(findRadioListTileWithTitle<VisitFirstUnreadSetting>(
settingTitle(VisitFirstUnreadSetting.always)));
await tester.pumpAndSettle(); // TODO why doesn't just `pump` work?
Copy link
Member

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.

Copy link
Collaborator Author

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 :)

await tester.pumpAndSettle();
checkPage(tester, expectedSetting: VisitFirstUnreadSetting.never);

await tester.tap(find.byType(BackButton));
Copy link
Member

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:

Suggested change
await tester.tap(find.byType(BackButton));
await tester.tap(find.backButton());

});
});

group('MarkReadOnScrollSetting', () {
Copy link
Member

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.

@chrisbobbe
Copy link
Collaborator Author

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
image

@gnprice
Copy link
Member

gnprice commented Jul 2, 2025

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:

SemanticsNode#5
 │ merge boundary ⛔️
 │ Rect.fromLTRB(0.0, 56.0, 800.0, 112.0)
 │ actions: focus, tap
 │ flags: hasEnabledState, isEnabled, isFocusable, hasSelectedState
 │ label: "System"
 │ textDirection: ltr
 │
 └─SemanticsNode#6
     merged up ⬆️
     Rect.fromLTRB(16.0, 8.0, 56.0, 48.0)
     actions: focus, tap
     flags: hasCheckedState, hasEnabledState, isEnabled,
       isInMutuallyExclusiveGroup, isFocusable

SemanticsNode#7
 │ merge boundary ⛔️
 │ Rect.fromLTRB(0.0, 112.0, 800.0, 168.0)
 │ actions: focus, tap
 │ flags: hasEnabledState, isEnabled, isFocusable, hasSelectedState
 │ label: "Light"
 │ textDirection: ltr
 │
 └─SemanticsNode#8
     merged up ⬆️
     Rect.fromLTRB(16.0, 8.0, 56.0, 48.0)
     actions: focus, tap
     flags: hasCheckedState, isChecked, hasEnabledState, isEnabled,
       isInMutuallyExclusiveGroup, isFocusable

so it looks like that isChecked flag is the thing to inspect.

@chrisbobbe
Copy link
Collaborator Author

Interesting; thanks for the debugSemantics tip—I definitely tried having the test check semantics before trying the check-what-paints approach, and I couldn't get it to work (the isChecked flag wasn't present on the node where I thought it should've been)—but I may have better luck with the help of debugSemantics when I come back to this

@gnprice gnprice force-pushed the pr-radiogroup-api-migration-again branch 2 times, most recently from ac6c750 to 4d1baae Compare July 3, 2025 18:54
@gnprice
Copy link
Member

gnprice commented Jul 3, 2025

(Pushed here a branch intended for #1662, oops; then restored the branch that had been here.)

@chrisbobbe chrisbobbe force-pushed the pr-radiogroup-api-migration-again branch from 4d1baae to ffd094c Compare July 7, 2025 18:59
@chrisbobbe
Copy link
Collaborator Author

OK, updated, checking semantics this time. PTAL! 🙂

Copy link
Member

@gnprice gnprice left a 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.

Comment on lines 70 to 72
hasSelectedState: true,
hasTapAction: true,
hasFocusAction: true));
Copy link
Member

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.

Copy link
Member

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
@chrisbobbe chrisbobbe force-pushed the pr-radiogroup-api-migration-again branch from ffd094c to 5f9512e Compare July 8, 2025 05:45
@chrisbobbe
Copy link
Collaborator Author

Thanks! Revision pushed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
integration review Added by maintainers when PR may be ready for integration
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Resolve deprecation warnings about radio buttons
2 participants