diff --git a/lib/widgets/action_sheet.dart b/lib/widgets/action_sheet.dart index c56e41d5c2..73f1b10485 100644 --- a/lib/widgets/action_sheet.dart +++ b/lib/widgets/action_sheet.dart @@ -255,7 +255,13 @@ class BottomSheetEmptyContentPlaceholder extends StatelessWidget { final designVariables = DesignVariables.of(context); final child = loading - ? CircularProgressIndicator() + ? Semantics( + textDirection: Directionality.of(context), + focusable: true, + liveRegion: true, + label: 'Loading…', + child: CircularProgressIndicator(), + ) : Text( textAlign: TextAlign.center, style: TextStyle( @@ -628,6 +634,8 @@ class ChannelFeedButton extends ActionSheetMenuItemButton { } } + + class CopyChannelLinkButton extends ActionSheetMenuItemButton { const CopyChannelLinkButton({ super.key, diff --git a/lib/widgets/home.dart b/lib/widgets/home.dart index c74da27a86..4f4c54f70f 100644 --- a/lib/widgets/home.dart +++ b/lib/widgets/home.dart @@ -246,7 +246,12 @@ class _LoadingPlaceholderPageState extends State<_LoadingPlaceholderPage> { child: Column( mainAxisSize: MainAxisSize.min, children: [ - const CircularProgressIndicator(), + Semantics( + label: 'Loading…', + textDirection: Directionality.of(context), + liveRegion: true, + child:const CircularProgressIndicator(), + ), Visibility( visible: showTryAnotherAccount, maintainSize: true, diff --git a/lib/widgets/message_list.dart b/lib/widgets/message_list.dart index 7ff3d395b8..9b9eff7493 100644 --- a/lib/widgets/message_list.dart +++ b/lib/widgets/message_list.dart @@ -1029,7 +1029,20 @@ class _MessageListState extends State with PerAccountStoreAwareStat Widget build(BuildContext context) { final zulipLocalizations = ZulipLocalizations.of(context); - if (!model.fetched) return const Center(child: CircularProgressIndicator()); + if (!model.fetched) { + return Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Semantics( + label: 'Loading more messages', + textDirection: Directionality.of(context), + liveRegion: true, + child: const CircularProgressIndicator(), + ), + ), + ); + } + if (model.items.isEmpty && model.haveNewest && model.haveOldest) { final String header; @@ -1283,10 +1296,18 @@ class _MessageListLoadingMore extends StatelessWidget { @override Widget build(BuildContext context) { - return const Center( + return Center( child: Padding( - padding: EdgeInsets.symmetric(vertical: 16.0), - child: CircularProgressIndicator())); // TODO perhaps a different indicator + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Semantics( + textDirection: Directionality.of(context), + label: 'Loading more messages', + liveRegion: true, + child: const CircularProgressIndicator(), + ), + ), + ); + // TODO perhaps a different indicator } } diff --git a/lib/widgets/topic_list.dart b/lib/widgets/topic_list.dart index 846f2202f7..d631d7298a 100644 --- a/lib/widgets/topic_list.dart +++ b/lib/widgets/topic_list.dart @@ -166,9 +166,21 @@ class _TopicListState extends State<_TopicList> with PerAccountStoreAwareStateMi @override Widget build(BuildContext context) { if (lastFetchedTopics == null) { - return const Center(child: CircularProgressIndicator()); + return Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Semantics( + textDirection: Directionality.of(context), + label: 'Loading…', // plain string (not localized) + liveRegion: true, + child: CircularProgressIndicator(), + ), + ), + ); + } + // TODO(design) handle the rare case when `lastFetchedTopics` is empty // This is adapted from parts of the build method on [_InboxPageState]. diff --git a/test/widgets/home_test.dart b/test/widgets/home_test.dart index 239449c990..3de9e39939 100644 --- a/test/widgets/home_test.dart +++ b/test/widgets/home_test.dart @@ -397,117 +397,165 @@ void main () { } testWidgets('smoke', (tester) async { - addTearDown(testBinding.reset); - testBinding.globalStore.loadPerAccountDuration = loadPerAccountDuration; - await prepare(tester); - await tester.pump(loadPerAccountDuration); - checkOnHomePage(tester, expectedAccount: eg.selfAccount); + final SemanticsHandle semanticsHandle = tester.ensureSemantics(); + try { + addTearDown(testBinding.reset); + testBinding.globalStore.loadPerAccountDuration = loadPerAccountDuration; + await prepare(tester); + + // While loading, the loading page should be shown and have the semantics label. + checkOnLoadingPage(); + expect(find.bySemanticsLabel('Loading…'), findsOneWidget); + + await tester.pump(loadPerAccountDuration); + checkOnHomePage(tester, expectedAccount: eg.selfAccount); + } finally { + semanticsHandle.dispose(); + } }); - testWidgets('"Try another account" button appears after timeout', (tester) async { - addTearDown(testBinding.reset); - testBinding.globalStore.loadPerAccountDuration = loadPerAccountDuration; - await prepare(tester); - checkOnLoadingPage(); - check(find.text('Try another account').hitTestable()).findsNothing(); - - await tester.pump(kTryAnotherAccountWaitPeriod); - checkOnLoadingPage(); - check(find.text('Try another account').hitTestable()).findsOne(); - await tester.pump(loadPerAccountDuration); - checkOnHomePage(tester, expectedAccount: eg.selfAccount); + testWidgets('"Try another account" button appears after timeout', (tester) async { + final SemanticsHandle semanticsHandle = tester.ensureSemantics(); + try { + addTearDown(testBinding.reset); + testBinding.globalStore.loadPerAccountDuration = loadPerAccountDuration; + await prepare(tester); + checkOnLoadingPage(); + // No try-another button immediately. + check(find.text('Try another account').hitTestable()).findsNothing(); + + // The loading semantics should already be present. + expect(find.bySemanticsLabel('Loading…'), findsOneWidget); + + await tester.pump(kTryAnotherAccountWaitPeriod); + checkOnLoadingPage(); + check(find.text('Try another account').hitTestable()).findsOne(); + + await tester.pump(loadPerAccountDuration); + checkOnHomePage(tester, expectedAccount: eg.selfAccount); + } finally { + semanticsHandle.dispose(); + } }); - testWidgets('while loading, go back from ChooseAccountPage', (tester) async { - testBinding.globalStore.loadPerAccountDuration = loadPerAccountDuration; - await prepare(tester); - await tester.pump(kTryAnotherAccountWaitPeriod); - await tapTryAnotherAccount(tester); - lastPoppedRoute = null; - await tester.tap(find.byType(BackButton)); - await tester.pump(); - check(lastPoppedRoute).isA().page.isA(); - await tester.pump( - (lastPoppedRoute as TransitionRoute).reverseTransitionDuration - // TODO not sure why a 1ms fudge is needed; investigate. - + Duration(milliseconds: 1)); - checkOnLoadingPage(); - - await tester.pump(loadPerAccountDuration); - checkOnHomePage(tester, expectedAccount: eg.selfAccount); + testWidgets('while loading, go back from ChooseAccountPage', (tester) async { + final SemanticsHandle semanticsHandle = tester.ensureSemantics(); + try { + testBinding.globalStore.loadPerAccountDuration = loadPerAccountDuration; + await prepare(tester); + await tester.pump(kTryAnotherAccountWaitPeriod); + await tapTryAnotherAccount(tester); + + lastPoppedRoute = null; + await tester.tap(find.byType(BackButton)); + await tester.pump(); + check(lastPoppedRoute).isA().page.isA(); + await tester.pump( + (lastPoppedRoute as TransitionRoute).reverseTransitionDuration + // TODO not sure why a 1ms fudge is needed; investigate. + + Duration(milliseconds: 1)); + checkOnLoadingPage(); + + // Semantics check: loading label present + expect(find.bySemanticsLabel('Loading…'), findsOneWidget); + + await tester.pump(loadPerAccountDuration); + checkOnHomePage(tester, expectedAccount: eg.selfAccount); + } finally { + semanticsHandle.dispose(); + } }); - testWidgets('while loading, choose a different account', (tester) async { - testBinding.globalStore.loadPerAccountDuration = loadPerAccountDuration; - await prepare(tester); - await tester.pump(kTryAnotherAccountWaitPeriod); - await tapTryAnotherAccount(tester); - testBinding.globalStore.loadPerAccountDuration = loadPerAccountDuration * 2; - await chooseAccountWithEmail(tester, eg.otherAccount.email); - - await tester.pump(loadPerAccountDuration); - // The second loadPerAccount is still pending. - checkOnLoadingPage(); - - await tester.pump(loadPerAccountDuration); - // The second loadPerAccount finished. - checkOnHomePage(tester, expectedAccount: eg.otherAccount); + testWidgets('while loading, choose a different account', (tester) async { + final SemanticsHandle semanticsHandle = tester.ensureSemantics(); + try { + testBinding.globalStore.loadPerAccountDuration = loadPerAccountDuration; + await prepare(tester); + await tester.pump(kTryAnotherAccountWaitPeriod); + await tapTryAnotherAccount(tester); + + testBinding.globalStore.loadPerAccountDuration = loadPerAccountDuration * 2; + await chooseAccountWithEmail(tester, eg.otherAccount.email); + + // While the second account is still loading, we should be on a loading page. + await tester.pump(loadPerAccountDuration); + checkOnLoadingPage(); + expect(find.bySemanticsLabel('Loading…'), findsOneWidget); + + await tester.pump(loadPerAccountDuration); + // The second load finished. + checkOnHomePage(tester, expectedAccount: eg.otherAccount); + } finally { + semanticsHandle.dispose(); + } }); - testWidgets('while loading, choosing an account disallows going back', (tester) async { - testBinding.globalStore.loadPerAccountDuration = loadPerAccountDuration; - await prepare(tester); - await tester.pump(kTryAnotherAccountWaitPeriod); - await tapTryAnotherAccount(tester); - - // While still loading, choose a different account. - await chooseAccountWithEmail(tester, eg.otherAccount.email); - // User cannot go back because the navigator stack - // was cleared after choosing an account. - check(getRouteOf(tester, find.byType(CircularProgressIndicator))) - .isNotNull().isFirst.isTrue(); - - await tester.pump(loadPerAccountDuration); // wait for loadPerAccount - checkOnHomePage(tester, expectedAccount: eg.otherAccount); + testWidgets('while loading, choosing an account disallows going back', (tester) async { + final SemanticsHandle semanticsHandle = tester.ensureSemantics(); + try { + testBinding.globalStore.loadPerAccountDuration = loadPerAccountDuration; + await prepare(tester); + await tester.pump(kTryAnotherAccountWaitPeriod); + await tapTryAnotherAccount(tester); + + // While still loading, choose a different account. + await chooseAccountWithEmail(tester, eg.otherAccount.email); + + // User cannot go back because the navigator stack was cleared after choosing an account. + check(getRouteOf(tester, find.byType(CircularProgressIndicator))) + .isNotNull().isFirst.isTrue(); + + // Semantics check: ensure loading label present and first route is our account route. + expect(find.bySemanticsLabel('Loading…'), findsOneWidget); + + await tester.pump(loadPerAccountDuration); // wait for loadPerAccount + checkOnHomePage(tester, expectedAccount: eg.otherAccount); + } finally { + semanticsHandle.dispose(); + } }); - testWidgets('while loading, go to nested levels of ChooseAccountPage', (tester) async { - testBinding.globalStore.loadPerAccountDuration = loadPerAccountDuration; - final thirdAccount = eg.account(user: eg.thirdUser); - await testBinding.globalStore.add(thirdAccount, eg.initialSnapshot( - realmUsers: [eg.thirdUser])); - await prepare(tester); - await tester.pump(kTryAnotherAccountWaitPeriod); - // While still loading the first account, choose a different account. - await tapTryAnotherAccount(tester); - await chooseAccountWithEmail(tester, eg.otherAccount.email); - // User cannot go back because the navigator stack - // was cleared after choosing an account. - check(getRouteOf(tester, find.byType(CircularProgressIndicator))) - .isA() + testWidgets('while loading, go to nested levels of ChooseAccountPage', (tester) async { + final SemanticsHandle semanticsHandle = tester.ensureSemantics(); + try { + testBinding.globalStore.loadPerAccountDuration = loadPerAccountDuration; + final thirdAccount = eg.account(user: eg.thirdUser); + await testBinding.globalStore.add(thirdAccount, eg.initialSnapshot( + realmUsers: [eg.thirdUser])); + await prepare(tester); + + await tester.pump(kTryAnotherAccountWaitPeriod); + await tapTryAnotherAccount(tester); + await chooseAccountWithEmail(tester, eg.otherAccount.email); + check(getRouteOf(tester, find.byType(CircularProgressIndicator))) + .isA() ..isFirst.isTrue() ..accountId.equals(eg.otherAccount.id); - await tester.pump(kTryAnotherAccountWaitPeriod); - // While still loading the second account, choose a different account. - await tapTryAnotherAccount(tester); - await chooseAccountWithEmail(tester, thirdAccount.email); - // User cannot go back because the navigator stack - // was cleared after choosing an account. - check(getRouteOf(tester, find.byType(CircularProgressIndicator))) - .isA() + // Semantics check: loading label present + expect(find.bySemanticsLabel('Loading…'), findsOneWidget); + + await tester.pump(kTryAnotherAccountWaitPeriod); + await tapTryAnotherAccount(tester); + await chooseAccountWithEmail(tester, thirdAccount.email); + check(getRouteOf(tester, find.byType(CircularProgressIndicator))) + .isA() ..isFirst.isTrue() ..accountId.equals(thirdAccount.id); - await tester.pump(loadPerAccountDuration); // wait for loadPerAccount - checkOnHomePage(tester, expectedAccount: thirdAccount); + await tester.pump(loadPerAccountDuration); // wait for loadPerAccount + checkOnHomePage(tester, expectedAccount: thirdAccount); + } finally { + semanticsHandle.dispose(); + } }); + testWidgets('after finishing loading, go back from ChooseAccountPage', (tester) async { testBinding.globalStore.loadPerAccountDuration = loadPerAccountDuration; await prepare(tester); diff --git a/test/widgets/message_list_test.dart b/test/widgets/message_list_test.dart index 89b2a31bd8..2f2ba9804f 100644 --- a/test/widgets/message_list_test.dart +++ b/test/widgets/message_list_test.dart @@ -826,54 +826,70 @@ void main() { testWidgets('spacer when have newest', (tester) async { final messages = List.generate(10, - (i) => eg.streamMessage(content: '

message $i

')); + (i) => eg.streamMessage(content: '

message $i

')); await setupMessageListPage(tester, narrow: CombinedFeedNarrow(), - fetchResult: eg.nearGetMessagesResult(anchor: messages.last.id, - foundOldest: true, foundNewest: true, messages: messages)); + fetchResult: eg.nearGetMessagesResult(anchor: messages.last.id, + foundOldest: true, foundNewest: true, messages: messages)); check(findMessageListScrollController(tester)!.position) - .extentAfter.equals(0); + .extentAfter.equals(0); // There's no loading indicator. check(findLoadingIndicator).findsNothing(); // The last message is spaced above the bottom of the viewport. check(tester.getRect(find.text('message 9'))) - .bottom..isGreaterThan(400)..isLessThan(570); + .bottom..isGreaterThan(400)..isLessThan(570); + + // --- Semantics check: ensure loading semantics label is not present --- + final SemanticsHandle semanticsHandle = tester.ensureSemantics(); + try { + expect(find.bySemanticsLabel('Loading more messages'), findsNothing); + } finally { + semanticsHandle.dispose(); + } }); + testWidgets('loading indicator displaces spacer etc.', (tester) async { - await setupMessageListPage(tester, narrow: CombinedFeedNarrow(), - skipPumpAndSettle: true, - // TODO(#1569) fix realism of this data: foundNewest false should mean - // some messages found after anchor (and then we might need to scroll - // to cause fetching newer messages). - fetchResult: eg.nearGetMessagesResult(anchor: 1000, - foundOldest: true, foundNewest: false, - messages: List.generate(10, - (i) => eg.streamMessage(id: 100 + i, content: '

message $i

')))); - await tester.pump(); + final SemanticsHandle semanticsHandle = tester.ensureSemantics(); + try { + await setupMessageListPage(tester, narrow: CombinedFeedNarrow(), + skipPumpAndSettle: true, + // TODO(#1569) fix realism of this data: foundNewest false should mean + // some messages found after anchor (and then we might need to scroll + // to cause fetching newer messages). + fetchResult: eg.nearGetMessagesResult(anchor: 1000, + foundOldest: true, foundNewest: false, + messages: List.generate(10, + (i) => eg.streamMessage(id: 100 + i, content: '

message $i

')))); + await tester.pump(); - // The message list will immediately start fetching newer messages. - connection.prepare(json: eg.newerGetMessagesResult( - anchor: 109, foundNewest: true, messages: List.generate(100, - (i) => eg.streamMessage(id: 110 + i))).toJson()); - await tester.pump(Duration(milliseconds: 10)); - await tester.pump(); + // The message list will immediately start fetching newer messages. + connection.prepare(json: eg.newerGetMessagesResult( + anchor: 109, foundNewest: true, messages: List.generate(100, + (i) => eg.streamMessage(id: 110 + i))).toJson()); + await tester.pump(Duration(milliseconds: 10)); + await tester.pump(); - // There's a loading indicator. - check(findLoadingIndicator).findsOne(); - // It's at the bottom. - check(findMessageListScrollController(tester)!.position) - .extentAfter.equals(0); - final loadingIndicatorRect = tester.getRect(findLoadingIndicator); - check(loadingIndicatorRect).bottom.isGreaterThan(575); - // The last message is shortly above it; no spacer or anything else. - check(tester.getRect(find.text('message 9'))) - .bottom.isGreaterThan(loadingIndicatorRect.top - 36); // TODO(#1569) where's this space going? - await tester.pumpAndSettle(); + // There's a loading indicator. + check(findLoadingIndicator).findsOne(); + // It's at the bottom. + check(findMessageListScrollController(tester)!.position) + .extentAfter.equals(0); + final loadingIndicatorRect = tester.getRect(findLoadingIndicator); + check(loadingIndicatorRect).bottom.isGreaterThan(575); + // The last message is shortly above it; no spacer or anything else. + check(tester.getRect(find.text('message 9'))) + .bottom.isGreaterThan(loadingIndicatorRect.top - 36); // TODO(#1569) + + // --- Semantics check: ensure the loading indicator exposes the expected label --- + expect(find.bySemanticsLabel('Loading more messages'), findsOneWidget); + + await tester.pumpAndSettle(); + } finally { + semanticsHandle.dispose(); + } }); - // TODO(#1569) test no typing status or mark-read button when not haveNewest - // (even without loading indicator) }); group('TypingStatusWidget', () { diff --git a/test/widgets/topic_list_test.dart b/test/widgets/topic_list_test.dart index 1cd9c4bb26..049495b6eb 100644 --- a/test/widgets/topic_list_test.dart +++ b/test/widgets/topic_list_test.dart @@ -43,7 +43,7 @@ void main() { await store.addSubscription(eg.subscription(channel)); for (final userTopic in userTopics) { await store.setUserTopic( - channel, userTopic.topicName.apiName, userTopic.visibilityPolicy); + channel, userTopic.topicName.apiName, userTopic.visibilityPolicy); } topics ??= [eg.getStreamTopicsEntry()]; messages ??= [eg.streamMessage(stream: channel, topic: topics.first.name.apiName)]; @@ -51,8 +51,8 @@ void main() { connection.prepare(json: GetStreamTopicsResult(topics: topics).toJson()); await tester.pumpWidget(TestZulipApp( - accountId: eg.selfAccount.id, - child: TopicListPage(streamId: channel.streamId))); + accountId: eg.selfAccount.id, + child: TopicListPage(streamId: channel.streamId))); await tester.pump(); await tester.pump(Duration.zero); check(connection.takeRequests()).single.isA() @@ -69,10 +69,10 @@ void main() { final channel = eg.stream(); (store.connection as FakeApiConnection).prepare( - json: GetStreamTopicsResult(topics: []).toJson()); + json: GetStreamTopicsResult(topics: []).toJson()); await tester.pumpWidget(TestZulipApp( - accountId: eg.selfAccount.id, - child: TopicListPage(streamId: channel.streamId))); + accountId: eg.selfAccount.id, + child: TopicListPage(streamId: channel.streamId))); await tester.pump(); await tester.pump(Duration.zero); check(find.widgetWithText(ZulipAppBar, '(unknown channel)')).findsOne(); @@ -83,20 +83,20 @@ void main() { await prepare(tester, channel: channel); connection.prepare(json: eg.newestGetMessagesResult( - foundOldest: true, messages: [eg.streamMessage(stream: channel)]).toJson()); + foundOldest: true, messages: [eg.streamMessage(stream: channel)]).toJson()); await tester.tap(find.byIcon(ZulipIcons.message_feed)); await tester.pump(); await tester.pump(Duration.zero); check(find.descendant( - of: find.byType(MessageListPage), - matching: find.text('channel foo')), + of: find.byType(MessageListPage), + matching: find.text('channel foo')), ).findsOne(); }); testWidgets('show channel action sheet', (tester) async { final channel = eg.stream(name: 'channel foo'); await prepare(tester, channel: channel, - messages: [eg.streamMessage(stream: channel)]); + messages: [eg.streamMessage(stream: channel)]); await tester.longPress(find.text('channel foo')); await tester.pump(Duration(milliseconds: 100)); // bottom-sheet animation @@ -114,14 +114,27 @@ void main() { json: GetStreamTopicsResult(topics: []).toJson(), delay: Duration(seconds: 1), ); - await tester.pumpWidget(TestZulipApp( - accountId: eg.selfAccount.id, - child: TopicListPage(streamId: channel.streamId))); - await tester.pump(); - check(find.byType(CircularProgressIndicator)).findsOne(); - await tester.pump(Duration(seconds: 1)); - check(find.byType(CircularProgressIndicator)).findsNothing(); + // Enable semantics for this test so we can assert the accessibility label. + final SemanticsHandle semanticsHandle = tester.ensureSemantics(); + try { + await tester.pumpWidget(TestZulipApp( + accountId: eg.selfAccount.id, + child: TopicListPage(streamId: channel.streamId))); + await tester.pump(); + // Visual indicator present + check(find.byType(CircularProgressIndicator)).findsOne(); + // Semantics label should be discoverable while loading. + expect(find.bySemanticsLabel('Loading…'), findsOneWidget); + + await tester.pump(Duration(seconds: 1)); + // Visual indicator gone after load completes + check(find.byType(CircularProgressIndicator)).findsNothing(); + // Semantics label should also be gone. + expect(find.bySemanticsLabel('Loading…'), findsNothing); + } finally { + semanticsHandle.dispose(); + } }); testWidgets('fetch again when navigating away and back', (tester) async { @@ -133,15 +146,15 @@ void main() { // Start from a message list page in a channel narrow. connection.prepare(json: eg.newestGetMessagesResult( - foundOldest: true, messages: []).toJson()); + foundOldest: true, messages: []).toJson()); await tester.pumpWidget(TestZulipApp( - accountId: eg.selfAccount.id, - child: MessageListPage(initNarrow: ChannelNarrow(channel.streamId)))); + accountId: eg.selfAccount.id, + child: MessageListPage(initNarrow: ChannelNarrow(channel.streamId)))); await tester.pump(); // Tap "TOPICS" button navigating to the topic-list page… connection.prepare(json: GetStreamTopicsResult( - topics: [eg.getStreamTopicsEntry(name: 'topic A')]).toJson()); + topics: [eg.getStreamTopicsEntry(name: 'topic A')]).toJson()); await tester.tap(find.byIcon(ZulipIcons.topics)); await tester.pump(); await tester.pump(Duration.zero); @@ -153,7 +166,7 @@ void main() { // … then back to the topic-list page, expecting to fetch again. connection.prepare(json: GetStreamTopicsResult( - topics: [eg.getStreamTopicsEntry(name: 'topic B')]).toJson()); + topics: [eg.getStreamTopicsEntry(name: 'topic B')]).toJson()); await tester.tap(find.byIcon(ZulipIcons.topics)); await tester.pump(); await tester.pump(Duration.zero); @@ -162,17 +175,17 @@ void main() { }); Finder topicItemFinder = find.descendant( - of: find.byType(ListView), - matching: find.byType(Material)); + of: find.byType(ListView), + matching: find.byType(Material)); Finder findInTopicItemAt(int index, Finder finder) => find.descendant( - of: topicItemFinder.at(index), - matching: finder); + of: topicItemFinder.at(index), + matching: finder); testWidgets('show topic action sheet', (tester) async { final channel = eg.stream(); await prepare(tester, channel: channel, - topics: [eg.getStreamTopicsEntry(name: 'topic foo')]); + topics: [eg.getStreamTopicsEntry(name: 'topic foo')]); await tester.longPress(topicItemFinder); await tester.pump(Duration(milliseconds: 150)); // bottom-sheet animation @@ -215,11 +228,11 @@ void main() { check(findInTopicItemAt(0, find.text('resolved'))).findsOne(); check(findInTopicItemAt(0, find.byIcon(ZulipIcons.check).hitTestable())) - .findsOne(); + .findsOne(); check(findInTopicItemAt(1, find.text('unresolved'))).findsOne(); check(findInTopicItemAt(1, find.byType(Icon)).hitTestable()) - .findsNothing(); + .findsNothing(); }); testWidgets('handle empty topics', (tester) async { @@ -227,52 +240,52 @@ void main() { eg.getStreamTopicsEntry(name: ''), ]); check(findInTopicItemAt(0, - find.text(eg.defaultRealmEmptyTopicDisplayName))).findsOne(); + find.text(eg.defaultRealmEmptyTopicDisplayName))).findsOne(); }); group('unreads', () { testWidgets('muted and non-muted topics', (tester) async { final channel = eg.stream(); await prepare(tester, channel: channel, - topics: [ - eg.getStreamTopicsEntry(maxId: 2, name: 'muted'), - eg.getStreamTopicsEntry(maxId: 1, name: 'non-muted'), - ], - userTopics: [ - eg.userTopicItem(channel, 'muted', UserTopicVisibilityPolicy.muted), - ], - messages: [ - eg.streamMessage(stream: channel, topic: 'muted'), - eg.streamMessage(stream: channel, topic: 'non-muted'), - eg.streamMessage(stream: channel, topic: 'non-muted'), - ]); + topics: [ + eg.getStreamTopicsEntry(maxId: 2, name: 'muted'), + eg.getStreamTopicsEntry(maxId: 1, name: 'non-muted'), + ], + userTopics: [ + eg.userTopicItem(channel, 'muted', UserTopicVisibilityPolicy.muted), + ], + messages: [ + eg.streamMessage(stream: channel, topic: 'muted'), + eg.streamMessage(stream: channel, topic: 'non-muted'), + eg.streamMessage(stream: channel, topic: 'non-muted'), + ]); check(findInTopicItemAt(0, find.text('1'))).findsOne(); check(findInTopicItemAt(0, find.text('muted'))).findsOne(); check(findInTopicItemAt(0, find.byIcon(ZulipIcons.mute).hitTestable())) - .findsOne(); + .findsOne(); check(findInTopicItemAt(1, find.text('2'))).findsOne(); check(findInTopicItemAt(1, find.text('non-muted'))).findsOne(); check(findInTopicItemAt(1, find.byType(Icon).hitTestable())) - .findsNothing(); + .findsNothing(); }); testWidgets('with and without unread mentions', (tester) async { final channel = eg.stream(); await prepare(tester, channel: channel, - topics: [ - eg.getStreamTopicsEntry(maxId: 2, name: 'not mentioned'), - eg.getStreamTopicsEntry(maxId: 1, name: 'mentioned'), - ], - messages: [ - eg.streamMessage(stream: channel, topic: 'not mentioned'), - eg.streamMessage(stream: channel, topic: 'not mentioned'), - eg.streamMessage(stream: channel, topic: 'not mentioned', - flags: [MessageFlag.mentioned, MessageFlag.read]), - eg.streamMessage(stream: channel, topic: 'mentioned', - flags: [MessageFlag.mentioned]), - ]); + topics: [ + eg.getStreamTopicsEntry(maxId: 2, name: 'not mentioned'), + eg.getStreamTopicsEntry(maxId: 1, name: 'mentioned'), + ], + messages: [ + eg.streamMessage(stream: channel, topic: 'not mentioned'), + eg.streamMessage(stream: channel, topic: 'not mentioned'), + eg.streamMessage(stream: channel, topic: 'not mentioned', + flags: [MessageFlag.mentioned, MessageFlag.read]), + eg.streamMessage(stream: channel, topic: 'mentioned', + flags: [MessageFlag.mentioned]), + ]); check(findInTopicItemAt(0, find.text('2'))).findsOne(); check(findInTopicItemAt(0, find.text('not mentioned'))).findsOne(); @@ -288,43 +301,43 @@ void main() { testWidgets('default', (tester) async { final channel = eg.stream(); await prepare(tester, channel: channel, - topics: [eg.getStreamTopicsEntry(name: 'topic')]); + topics: [eg.getStreamTopicsEntry(name: 'topic')]); check(find.descendant(of: topicItemFinder, - matching: find.byType(Icons))).findsNothing(); + matching: find.byType(Icons))).findsNothing(); }); testWidgets('muted', (tester) async { final channel = eg.stream(); await prepare(tester, channel: channel, - topics: [eg.getStreamTopicsEntry(name: 'topic')], - userTopics: [ - eg.userTopicItem(channel, 'topic', UserTopicVisibilityPolicy.muted), - ]); + topics: [eg.getStreamTopicsEntry(name: 'topic')], + userTopics: [ + eg.userTopicItem(channel, 'topic', UserTopicVisibilityPolicy.muted), + ]); check(find.descendant(of: topicItemFinder, - matching: find.byIcon(ZulipIcons.mute))).findsOne(); + matching: find.byIcon(ZulipIcons.mute))).findsOne(); }); testWidgets('unmuted', (tester) async { final channel = eg.stream(); await prepare(tester, channel: channel, - topics: [eg.getStreamTopicsEntry(name: 'topic')], - userTopics: [ - eg.userTopicItem(channel, 'topic', UserTopicVisibilityPolicy.unmuted), - ]); + topics: [eg.getStreamTopicsEntry(name: 'topic')], + userTopics: [ + eg.userTopicItem(channel, 'topic', UserTopicVisibilityPolicy.unmuted), + ]); check(find.descendant(of: topicItemFinder, - matching: find.byIcon(ZulipIcons.unmute))).findsOne(); + matching: find.byIcon(ZulipIcons.unmute))).findsOne(); }); testWidgets('followed', (tester) async { final channel = eg.stream(); await prepare(tester, channel: channel, - topics: [eg.getStreamTopicsEntry(name: 'topic')], - userTopics: [ - eg.userTopicItem(channel, 'topic', UserTopicVisibilityPolicy.followed), - ]); + topics: [eg.getStreamTopicsEntry(name: 'topic')], + userTopics: [ + eg.userTopicItem(channel, 'topic', UserTopicVisibilityPolicy.followed), + ]); check(find.descendant(of: topicItemFinder, - matching: find.byIcon(ZulipIcons.follow))).findsOne(); + matching: find.byIcon(ZulipIcons.follow))).findsOne(); }); }); }