diff --git a/assets/l10n/app_en.arb b/assets/l10n/app_en.arb index 2b3622387b..ed24c820a5 100644 --- a/assets/l10n/app_en.arb +++ b/assets/l10n/app_en.arb @@ -1488,5 +1488,9 @@ "zulipAppTitle": "Zulip", "@zulipAppTitle": { "description": "The name of Zulip. This should be either 'Zulip' or a transliteration." - } + }, + "loading": "Loading…", + "@loading": { + "description": "Semantic label for a loading indicator" + } } diff --git a/lib/generated/l10n/zulip_localizations.dart b/lib/generated/l10n/zulip_localizations.dart index 3cf5ddc85d..35c2bffce7 100644 --- a/lib/generated/l10n/zulip_localizations.dart +++ b/lib/generated/l10n/zulip_localizations.dart @@ -2156,6 +2156,12 @@ abstract class ZulipLocalizations { /// In en, this message translates to: /// **'Zulip'** String get zulipAppTitle; + + /// Semantic label for a loading indicator + /// + /// In en, this message translates to: + /// **'Loading…'** + String get loading; } class _ZulipLocalizationsDelegate diff --git a/lib/generated/l10n/zulip_localizations_ar.dart b/lib/generated/l10n/zulip_localizations_ar.dart index 27189bbb11..cfb28ca6d2 100644 --- a/lib/generated/l10n/zulip_localizations_ar.dart +++ b/lib/generated/l10n/zulip_localizations_ar.dart @@ -1242,4 +1242,7 @@ class ZulipLocalizationsAr extends ZulipLocalizations { @override String get zulipAppTitle => 'Zulip'; + + @override + String get loading => 'Loading…'; } diff --git a/lib/generated/l10n/zulip_localizations_de.dart b/lib/generated/l10n/zulip_localizations_de.dart index 4caa2a8082..cef75454b6 100644 --- a/lib/generated/l10n/zulip_localizations_de.dart +++ b/lib/generated/l10n/zulip_localizations_de.dart @@ -1268,4 +1268,7 @@ class ZulipLocalizationsDe extends ZulipLocalizations { @override String get zulipAppTitle => 'Zulip'; + + @override + String get loading => 'Loading…'; } diff --git a/lib/generated/l10n/zulip_localizations_el.dart b/lib/generated/l10n/zulip_localizations_el.dart index 670a442e87..198d677c77 100644 --- a/lib/generated/l10n/zulip_localizations_el.dart +++ b/lib/generated/l10n/zulip_localizations_el.dart @@ -1242,4 +1242,7 @@ class ZulipLocalizationsEl extends ZulipLocalizations { @override String get zulipAppTitle => 'Zulip'; + + @override + String get loading => 'Loading…'; } diff --git a/lib/generated/l10n/zulip_localizations_en.dart b/lib/generated/l10n/zulip_localizations_en.dart index b2fbfcb72f..91af591f21 100644 --- a/lib/generated/l10n/zulip_localizations_en.dart +++ b/lib/generated/l10n/zulip_localizations_en.dart @@ -1242,6 +1242,9 @@ class ZulipLocalizationsEn extends ZulipLocalizations { @override String get zulipAppTitle => 'Zulip'; + + @override + String get loading => 'Loading…'; } /// The translations for English, as used in the United Kingdom (`en_GB`). diff --git a/lib/generated/l10n/zulip_localizations_es.dart b/lib/generated/l10n/zulip_localizations_es.dart index f3ee9284e9..1132ad0e49 100644 --- a/lib/generated/l10n/zulip_localizations_es.dart +++ b/lib/generated/l10n/zulip_localizations_es.dart @@ -1242,4 +1242,7 @@ class ZulipLocalizationsEs extends ZulipLocalizations { @override String get zulipAppTitle => 'Zulip'; + + @override + String get loading => 'Loading…'; } diff --git a/lib/generated/l10n/zulip_localizations_fr.dart b/lib/generated/l10n/zulip_localizations_fr.dart index 32cb94bf82..4322b3df02 100644 --- a/lib/generated/l10n/zulip_localizations_fr.dart +++ b/lib/generated/l10n/zulip_localizations_fr.dart @@ -1258,4 +1258,7 @@ class ZulipLocalizationsFr extends ZulipLocalizations { @override String get zulipAppTitle => 'Zulip'; + + @override + String get loading => 'Loading…'; } diff --git a/lib/generated/l10n/zulip_localizations_he.dart b/lib/generated/l10n/zulip_localizations_he.dart index 37dc15e7e4..105c0dbd3c 100644 --- a/lib/generated/l10n/zulip_localizations_he.dart +++ b/lib/generated/l10n/zulip_localizations_he.dart @@ -1242,4 +1242,7 @@ class ZulipLocalizationsHe extends ZulipLocalizations { @override String get zulipAppTitle => 'Zulip'; + + @override + String get loading => 'Loading…'; } diff --git a/lib/generated/l10n/zulip_localizations_hu.dart b/lib/generated/l10n/zulip_localizations_hu.dart index 44404f1955..7d4461c88a 100644 --- a/lib/generated/l10n/zulip_localizations_hu.dart +++ b/lib/generated/l10n/zulip_localizations_hu.dart @@ -1242,4 +1242,7 @@ class ZulipLocalizationsHu extends ZulipLocalizations { @override String get zulipAppTitle => 'Zulip'; + + @override + String get loading => 'Loading…'; } diff --git a/lib/generated/l10n/zulip_localizations_it.dart b/lib/generated/l10n/zulip_localizations_it.dart index 1533008103..fe76101960 100644 --- a/lib/generated/l10n/zulip_localizations_it.dart +++ b/lib/generated/l10n/zulip_localizations_it.dart @@ -1262,4 +1262,7 @@ class ZulipLocalizationsIt extends ZulipLocalizations { @override String get zulipAppTitle => 'Zulip'; + + @override + String get loading => 'Loading…'; } diff --git a/lib/generated/l10n/zulip_localizations_ja.dart b/lib/generated/l10n/zulip_localizations_ja.dart index 6465223b8a..89d0d92793 100644 --- a/lib/generated/l10n/zulip_localizations_ja.dart +++ b/lib/generated/l10n/zulip_localizations_ja.dart @@ -1216,4 +1216,7 @@ class ZulipLocalizationsJa extends ZulipLocalizations { @override String get zulipAppTitle => 'Zulip'; + + @override + String get loading => 'Loading…'; } diff --git a/lib/generated/l10n/zulip_localizations_nb.dart b/lib/generated/l10n/zulip_localizations_nb.dart index cfcc46ebb6..561b81aa3c 100644 --- a/lib/generated/l10n/zulip_localizations_nb.dart +++ b/lib/generated/l10n/zulip_localizations_nb.dart @@ -1242,4 +1242,7 @@ class ZulipLocalizationsNb extends ZulipLocalizations { @override String get zulipAppTitle => 'Zulip'; + + @override + String get loading => 'Loading…'; } diff --git a/lib/generated/l10n/zulip_localizations_pl.dart b/lib/generated/l10n/zulip_localizations_pl.dart index e12fe1aeb2..b02140342a 100644 --- a/lib/generated/l10n/zulip_localizations_pl.dart +++ b/lib/generated/l10n/zulip_localizations_pl.dart @@ -1261,4 +1261,7 @@ class ZulipLocalizationsPl extends ZulipLocalizations { @override String get zulipAppTitle => 'Zulip'; + + @override + String get loading => 'Loading…'; } diff --git a/lib/generated/l10n/zulip_localizations_ru.dart b/lib/generated/l10n/zulip_localizations_ru.dart index f2a9f0561e..cc723f8009 100644 --- a/lib/generated/l10n/zulip_localizations_ru.dart +++ b/lib/generated/l10n/zulip_localizations_ru.dart @@ -1274,4 +1274,7 @@ class ZulipLocalizationsRu extends ZulipLocalizations { @override String get zulipAppTitle => 'Zulip'; + + @override + String get loading => 'Loading…'; } diff --git a/lib/generated/l10n/zulip_localizations_sk.dart b/lib/generated/l10n/zulip_localizations_sk.dart index b28622eb3e..c4e4ab190a 100644 --- a/lib/generated/l10n/zulip_localizations_sk.dart +++ b/lib/generated/l10n/zulip_localizations_sk.dart @@ -1244,4 +1244,7 @@ class ZulipLocalizationsSk extends ZulipLocalizations { @override String get zulipAppTitle => 'Zulip'; + + @override + String get loading => 'Loading…'; } diff --git a/lib/generated/l10n/zulip_localizations_sl.dart b/lib/generated/l10n/zulip_localizations_sl.dart index 10dc8727cd..454e0967ff 100644 --- a/lib/generated/l10n/zulip_localizations_sl.dart +++ b/lib/generated/l10n/zulip_localizations_sl.dart @@ -1283,4 +1283,7 @@ class ZulipLocalizationsSl extends ZulipLocalizations { @override String get zulipAppTitle => 'Zulip'; + + @override + String get loading => 'Loading…'; } diff --git a/lib/generated/l10n/zulip_localizations_uk.dart b/lib/generated/l10n/zulip_localizations_uk.dart index e2872b11a0..5e8e75e378 100644 --- a/lib/generated/l10n/zulip_localizations_uk.dart +++ b/lib/generated/l10n/zulip_localizations_uk.dart @@ -1263,4 +1263,7 @@ class ZulipLocalizationsUk extends ZulipLocalizations { @override String get zulipAppTitle => 'Zulip'; + + @override + String get loading => 'Loading…'; } diff --git a/lib/generated/l10n/zulip_localizations_zh.dart b/lib/generated/l10n/zulip_localizations_zh.dart index e05f76bee3..5c03da5597 100644 --- a/lib/generated/l10n/zulip_localizations_zh.dart +++ b/lib/generated/l10n/zulip_localizations_zh.dart @@ -1242,6 +1242,9 @@ class ZulipLocalizationsZh extends ZulipLocalizations { @override String get zulipAppTitle => 'Zulip'; + + @override + String get loading => 'Loading…'; } /// The translations for Chinese, as used in China, using the Han script (`zh_Hans_CN`). diff --git a/lib/widgets/action_sheet.dart b/lib/widgets/action_sheet.dart index c56e41d5c2..b13eca493c 100644 --- a/lib/widgets/action_sheet.dart +++ b/lib/widgets/action_sheet.dart @@ -253,10 +253,13 @@ class BottomSheetEmptyContentPlaceholder extends StatelessWidget { @override Widget build(BuildContext context) { final designVariables = DesignVariables.of(context); - - final child = loading - ? CircularProgressIndicator() - : Text( + final zulipLocalizations = ZulipLocalizations.of(context); + final child = loading ? Semantics( + label:zulipLocalizations.loading , + liveRegion: true, + focusable: true, + child: CircularProgressIndicator()) + : Text( textAlign: TextAlign.center, style: TextStyle( color: designVariables.labelSearchPrompt, diff --git a/lib/widgets/home.dart b/lib/widgets/home.dart index 3c0a58d6ee..8b95e1ddad 100644 --- a/lib/widgets/home.dart +++ b/lib/widgets/home.dart @@ -248,7 +248,11 @@ class _LoadingPlaceholderPageState extends State<_LoadingPlaceholderPage> { child: Column( mainAxisSize: MainAxisSize.min, children: [ - const CircularProgressIndicator(), + Semantics( + label: zulipLocalizations.loading, + 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 e5239ec453..903164da32 100644 --- a/lib/widgets/message_list.dart +++ b/lib/widgets/message_list.dart @@ -1027,7 +1027,15 @@ class _MessageListState extends State with PerAccountStoreAwareStat @override Widget build(BuildContext context) { - if (!model.fetched) return const Center(child: CircularProgressIndicator()); + final zulipLocalizations = ZulipLocalizations.of(context); + + if (!model.fetched) { + return Center( + child: Semantics( + label: zulipLocalizations.loading, + liveRegion: true, + child: const CircularProgressIndicator())); + } if (model.items.isEmpty && model.haveNewest && model.haveOldest) { return _EmptyMessageListPlaceholder(narrow: widget.narrow); @@ -1366,10 +1374,12 @@ class _MessageListLoadingMore extends StatelessWidget { @override Widget build(BuildContext context) { - return const Center( - child: Padding( - padding: EdgeInsets.symmetric(vertical: 16.0), - child: CircularProgressIndicator())); // TODO perhaps a different indicator + final zulipLocalizations = ZulipLocalizations.of(context); + return Center( + child: Semantics( + label: zulipLocalizations.loading, + liveRegion: true, + child: CircularProgressIndicator())); // TODO perhaps a different indicator } } diff --git a/lib/widgets/topic_list.dart b/lib/widgets/topic_list.dart index 846f2202f7..861cb43e98 100644 --- a/lib/widgets/topic_list.dart +++ b/lib/widgets/topic_list.dart @@ -166,7 +166,13 @@ class _TopicListState extends State<_TopicList> with PerAccountStoreAwareStateMi @override Widget build(BuildContext context) { if (lastFetchedTopics == null) { - return const Center(child: CircularProgressIndicator()); + final zulipLocalizations = ZulipLocalizations.of(context); + + return Center( + child: Semantics( + label: zulipLocalizations.loading, + liveRegion: true, + child: CircularProgressIndicator())); } // TODO(design) handle the rare case when `lastFetchedTopics` is empty diff --git a/test/widgets/home_test.dart b/test/widgets/home_test.dart index 84475108ea..e82cea3ac7 100644 --- a/test/widgets/home_test.dart +++ b/test/widgets/home_test.dart @@ -18,6 +18,7 @@ import 'package:zulip/widgets/page.dart'; import 'package:zulip/widgets/profile.dart'; import 'package:zulip/widgets/subscription_list.dart'; import 'package:zulip/widgets/theme.dart'; +import 'package:zulip/generated/l10n/zulip_localizations.dart'; import '../api/fake_api.dart'; import '../example_data.dart' as eg; @@ -422,115 +423,170 @@ 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(); + final zulipLocalizations= await ZulipLocalizations.delegate.load(const Locale('en')); + 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(zulipLocalizations.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); + final SemanticsHandle semanticsHandle = tester.ensureSemantics(); + final zulipLocalizations = await ZulipLocalizations.delegate.load(const Locale('en')); + 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(zulipLocalizations.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 + final SemanticsHandle semanticsHandle = tester.ensureSemantics(); + final zulipLocalizations = await ZulipLocalizations.delegate.load(const Locale('en')); + 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(); + checkOnLoadingPage(); - await tester.pump(loadPerAccountDuration); - checkOnHomePage(tester, expectedAccount: eg.selfAccount); + // Semantics check: loading label present + expect(find.bySemanticsLabel(zulipLocalizations.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); + final SemanticsHandle semanticsHandle = tester.ensureSemantics(); + final zulipLocalizations = await ZulipLocalizations.delegate.load(const Locale('en')); + 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(zulipLocalizations.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); + final SemanticsHandle semanticsHandle = tester.ensureSemantics(); + final zulipLocalizations = await ZulipLocalizations.delegate.load(const Locale('en')); + 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(zulipLocalizations.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() + final SemanticsHandle semanticsHandle = tester.ensureSemantics(); + final zulipLocalizations = await ZulipLocalizations.delegate.load(const Locale('en')); + 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); + + // While loading the first account, navigate to ChooseAccountPage + // and select a different account. + 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: loading label should appear during the account switch. + expect(find.bySemanticsLabel(zulipLocalizations.loading), findsOneWidget); + + await tester.pump(kTryAnotherAccountWaitPeriod); + + // While loading the second account, navigate again and select a third account. + 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); // Finish loading the third account. + checkOnHomePage(tester, expectedAccount: thirdAccount); + } finally { + semanticsHandle.dispose(); + } }); testWidgets('after finishing loading, go back from ChooseAccountPage', (tester) async { diff --git a/test/widgets/message_list_test.dart b/test/widgets/message_list_test.dart index 91a5e33ba1..885cb7d382 100644 --- a/test/widgets/message_list_test.dart +++ b/test/widgets/message_list_test.dart @@ -39,6 +39,7 @@ import 'package:zulip/widgets/channel_colors.dart'; import 'package:zulip/widgets/theme.dart'; import 'package:zulip/widgets/topic_list.dart'; import 'package:zulip/widgets/user.dart'; +import 'package:zulip/generated/l10n/zulip_localizations.dart'; import '../api/fake_api.dart'; import '../example_data.dart' as eg; @@ -1048,19 +1049,30 @@ void main() { // The last message is spaced above the bottom of the viewport. check(tester.getRect(find.text('message 9'))) .bottom..isGreaterThan(400)..isLessThan(570); + + // Semantics check: ensure loading semantics label is not present + final SemanticsHandle semanticsHandle = tester.ensureSemantics(); + final zulipLocalizations = await ZulipLocalizations.delegate.load(const Locale('en')); + try { + expect(find.bySemanticsLabel(zulipLocalizations.loading), 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( @@ -1079,7 +1091,13 @@ void main() { // 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(); + //Semantics check: ensure the loading indicator exposes the expected label + final zulipLocalizations = await ZulipLocalizations.delegate.load(const Locale('en')); + expect(find.bySemanticsLabel(zulipLocalizations.loading), findsOneWidget); + await tester.pumpAndSettle(); + } finally { + semanticsHandle.dispose(); + } }); // TODO(#1569) test no typing status or mark-read button when not haveNewest diff --git a/test/widgets/topic_list_test.dart b/test/widgets/topic_list_test.dart index 1cd9c4bb26..ac1d6e0759 100644 --- a/test/widgets/topic_list_test.dart +++ b/test/widgets/topic_list_test.dart @@ -12,6 +12,7 @@ import 'package:zulip/widgets/app_bar.dart'; import 'package:zulip/widgets/icons.dart'; import 'package:zulip/widgets/message_list.dart'; import 'package:zulip/widgets/topic_list.dart'; +import 'package:zulip/generated/l10n/zulip_localizations.dart'; import '../api/fake_api.dart'; import '../example_data.dart' as eg; @@ -114,14 +115,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(); + final zulipLocalizations = await ZulipLocalizations.delegate.load(const Locale('en')); + 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(zulipLocalizations.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(zulipLocalizations.loading), findsNothing); + } finally { + semanticsHandle.dispose(); + } }); testWidgets('fetch again when navigating away and back', (tester) async {