@@ -4,36 +4,62 @@ import 'package:flutter/material.dart';
44import 'package:flutter_checks/flutter_checks.dart' ;
55import 'package:flutter_test/flutter_test.dart' ;
66import 'package:zulip/model/settings.dart' ;
7+ import 'package:zulip/widgets/page.dart' ;
78import 'package:zulip/widgets/settings.dart' ;
9+ import 'package:zulip/widgets/store.dart' ;
810
911import '../flutter_checks.dart' ;
1012import '../model/binding.dart' ;
1113import '../model/store_checks.dart' ;
1214import '../example_data.dart' as eg;
15+ import '../test_navigation.dart' ;
16+ import 'checks.dart' ;
1317import 'test_app.dart' ;
1418
1519void main () {
1620 TestZulipBinding .ensureInitialized ();
1721
22+ late TestNavigatorObserver testNavObserver;
23+ late Route <dynamic >? lastPushedRoute;
24+ late Route <dynamic >? lastPoppedRoute;
25+
1826 Future <void > prepare (WidgetTester tester) async {
1927 addTearDown (testBinding.reset);
2028
29+ testNavObserver = TestNavigatorObserver ()
30+ ..onPushed = ((route, _) => lastPushedRoute = route)
31+ ..onPopped = ((route, _) => lastPoppedRoute = route);
32+ lastPushedRoute = null ;
33+ lastPoppedRoute = null ;
34+
2135 await testBinding.globalStore.add (eg.selfAccount, eg.initialSnapshot ());
2236 await tester.pumpWidget (TestZulipApp (
2337 accountId: eg.selfAccount.id,
38+ navigatorObservers: [testNavObserver],
2439 child: SettingsPage ()));
2540 await tester.pump ();
2641 await tester.pump ();
2742 }
2843
44+ void checkTileOnSettingsPage (WidgetTester tester, {
45+ required String expectedTitle,
46+ required String expectedSubtitle,
47+ }) {
48+ check (find.descendant (of: find.widgetWithText (ListTile , expectedTitle),
49+ matching: find.text (expectedSubtitle))).findsOne ();
50+ }
51+
2952 Finder findRadioListTileWithTitle <T >(String title) => find.ancestor (
3053 of: find.text (title),
3154 matching: find.byType (RadioListTile <T >));
3255
33- void checkRadioButtonAppearsChecked <T >(WidgetTester tester, String title, bool expectedIsChecked) {
56+ void checkRadioButtonAppearsChecked <T >(WidgetTester tester,
57+ String title, bool expectedIsChecked, {String ? subtitle}) {
3458 check (tester.semantics.find (findRadioListTileWithTitle <T >(title)))
3559 .containsSemantics (
36- label: title,
60+ label: subtitle == null
61+ ? title
62+ : '$title \n $subtitle ' ,
3763 isInMutuallyExclusiveGroup: true ,
3864 hasCheckedState: true , isChecked: expectedIsChecked);
3965 }
@@ -134,7 +160,140 @@ void main() {
134160 }, variant: TargetPlatformVariant ({TargetPlatform .android, TargetPlatform .iOS}));
135161 });
136162
137- // TODO(#1571): test visitFirstUnread setting UI
163+ group ('VisitFirstUnreadSetting' , () {
164+ String settingTitle (VisitFirstUnreadSetting setting) => switch (setting) {
165+ VisitFirstUnreadSetting .always => 'First unread message' ,
166+ VisitFirstUnreadSetting .conversations => 'First unread message in conversation views, newest message elsewhere' ,
167+ VisitFirstUnreadSetting .never => 'Newest message' ,
168+ };
169+
170+ void checkPage (WidgetTester tester, {
171+ required VisitFirstUnreadSetting expectedSetting,
172+ }) {
173+ for (final setting in VisitFirstUnreadSetting .values) {
174+ final thisSettingTitle = settingTitle (setting);
175+ checkRadioButtonAppearsChecked <VisitFirstUnreadSetting >(tester,
176+ thisSettingTitle, setting == expectedSetting);
177+ }
178+ }
179+
180+ testWidgets ('smoke' , (tester) async {
181+ await prepare (tester);
182+
183+ // "conversations" is the default, and it appears in the SettingsPage
184+ // (as the setting tile's subtitle)
185+ check (GlobalStoreWidget .settingsOf (tester.element (find.byType (SettingsPage ))))
186+ .visitFirstUnread.equals (VisitFirstUnreadSetting .conversations);
187+ checkTileOnSettingsPage (tester,
188+ expectedTitle: 'Open message feeds at' ,
189+ expectedSubtitle: settingTitle (VisitFirstUnreadSetting .conversations));
190+
191+ await tester.tap (find.text ('Open message feeds at' ));
192+ await tester.pump ();
193+ check (lastPushedRoute).isA <MaterialWidgetRoute >()
194+ .page.isA <VisitFirstUnreadSettingPage >();
195+ await tester.pump ((lastPushedRoute as TransitionRoute ).transitionDuration);
196+ checkPage (tester, expectedSetting: VisitFirstUnreadSetting .conversations);
197+
198+ await tester.tap (findRadioListTileWithTitle <VisitFirstUnreadSetting >(
199+ settingTitle (VisitFirstUnreadSetting .always)));
200+ await tester.pump ();
201+ checkPage (tester, expectedSetting: VisitFirstUnreadSetting .always);
202+
203+ await tester.tap (findRadioListTileWithTitle <VisitFirstUnreadSetting >(
204+ settingTitle (VisitFirstUnreadSetting .conversations)));
205+ await tester.pump ();
206+ checkPage (tester, expectedSetting: VisitFirstUnreadSetting .conversations);
207+
208+ await tester.tap (findRadioListTileWithTitle <VisitFirstUnreadSetting >(
209+ settingTitle (VisitFirstUnreadSetting .never)));
210+ await tester.pump ();
211+ checkPage (tester, expectedSetting: VisitFirstUnreadSetting .never);
212+
213+ await tester.tap (find.backButton ());
214+ check (lastPoppedRoute).isA <MaterialWidgetRoute >()
215+ .page.isA <VisitFirstUnreadSettingPage >();
216+ await tester.pump ((lastPoppedRoute as TransitionRoute ).reverseTransitionDuration);
217+ check (GlobalStoreWidget .settingsOf (tester.element (find.byType (SettingsPage ))))
218+ .visitFirstUnread.equals (VisitFirstUnreadSetting .never);
219+
220+ checkTileOnSettingsPage (tester,
221+ expectedTitle: 'Open message feeds at' ,
222+ expectedSubtitle: settingTitle (VisitFirstUnreadSetting .never));
223+ });
224+ });
225+
226+ group ('MarkReadOnScrollSetting' , () {
227+ String settingTitle (MarkReadOnScrollSetting setting) => switch (setting) {
228+ MarkReadOnScrollSetting .always => 'Always' ,
229+ MarkReadOnScrollSetting .conversations => 'Only in conversation views' ,
230+ MarkReadOnScrollSetting .never => 'Never' ,
231+ };
232+
233+ String ? settingSubtitle (MarkReadOnScrollSetting setting) => switch (setting) {
234+ MarkReadOnScrollSetting .always => null ,
235+ MarkReadOnScrollSetting .conversations =>
236+ 'Messages will be automatically marked as read only when viewing a single topic or direct message conversation.' ,
237+ MarkReadOnScrollSetting .never => null ,
238+ };
239+
240+ void checkPage (WidgetTester tester, {
241+ required MarkReadOnScrollSetting expectedSetting,
242+ }) {
243+ for (final setting in MarkReadOnScrollSetting .values) {
244+ final thisSettingTitle = settingTitle (setting);
245+ checkRadioButtonAppearsChecked <MarkReadOnScrollSetting >(tester,
246+ thisSettingTitle,
247+ setting == expectedSetting,
248+ subtitle: settingSubtitle (setting));
249+ }
250+ }
251+
252+ testWidgets ('smoke' , (tester) async {
253+ await prepare (tester);
254+
255+ // "conversations" is the default, and it appears in the SettingsPage
256+ // (as the setting tile's subtitle)
257+ check (GlobalStoreWidget .settingsOf (tester.element (find.byType (SettingsPage ))))
258+ .markReadOnScroll.equals (MarkReadOnScrollSetting .conversations);
259+ checkTileOnSettingsPage (tester,
260+ expectedTitle: 'Mark messages as read on scroll' ,
261+ expectedSubtitle: settingTitle (MarkReadOnScrollSetting .conversations));
262+
263+ await tester.tap (find.text ('Mark messages as read on scroll' ));
264+ await tester.pump ();
265+ check (lastPushedRoute).isA <MaterialWidgetRoute >()
266+ .page.isA <MarkReadOnScrollSettingPage >();
267+ await tester.pump ((lastPushedRoute as TransitionRoute ).transitionDuration);
268+ checkPage (tester, expectedSetting: MarkReadOnScrollSetting .conversations);
269+
270+ await tester.tap (findRadioListTileWithTitle <MarkReadOnScrollSetting >(
271+ settingTitle (MarkReadOnScrollSetting .always)));
272+ await tester.pump ();
273+ checkPage (tester, expectedSetting: MarkReadOnScrollSetting .always);
274+
275+ await tester.tap (findRadioListTileWithTitle <MarkReadOnScrollSetting >(
276+ settingTitle (MarkReadOnScrollSetting .conversations)));
277+ await tester.pump ();
278+ checkPage (tester, expectedSetting: MarkReadOnScrollSetting .conversations);
279+
280+ await tester.tap (findRadioListTileWithTitle <MarkReadOnScrollSetting >(
281+ settingTitle (MarkReadOnScrollSetting .never)));
282+ await tester.pump ();
283+ checkPage (tester, expectedSetting: MarkReadOnScrollSetting .never);
284+
285+ await tester.tap (find.byType (BackButton ));
286+ check (lastPoppedRoute).isA <MaterialWidgetRoute >()
287+ .page.isA <MarkReadOnScrollSettingPage >();
288+ await tester.pump ((lastPoppedRoute as TransitionRoute ).reverseTransitionDuration);
289+ check (GlobalStoreWidget .settingsOf (tester.element (find.byType (SettingsPage ))))
290+ .markReadOnScroll.equals (MarkReadOnScrollSetting .never);
291+
292+ checkTileOnSettingsPage (tester,
293+ expectedTitle: 'Mark messages as read on scroll' ,
294+ expectedSubtitle: settingTitle (MarkReadOnScrollSetting .never));
295+ });
296+ });
138297
139298 // TODO maybe test GlobalSettingType.experimentalFeatureFlag settings
140299 // Or maybe not; after all, it's a developer-facing feature, so
0 commit comments