diff --git a/github_device_auth/lib/src/device_code_dialog.dart b/github_device_auth/lib/src/device_code_dialog.dart index 63f05d3..31dcecf 100644 --- a/github_device_auth/lib/src/device_code_dialog.dart +++ b/github_device_auth/lib/src/device_code_dialog.dart @@ -64,47 +64,57 @@ class _DeviceCodeDialogState extends State { @override Widget build(BuildContext context) { + // Glyphs in place of app-written words (kuhy's textgate rule); each old + // sentence is the spoken label. The user code is GitHub's data, typed + // by the user, so it stays visible. return AlertDialog( - title: const Text('Authorize on GitHub'), + title: const Icon(Icons.key, semanticLabel: 'Authorize on GitHub'), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text('Enter this code on GitHub:'), - const SizedBox(height: 8), - SelectableText( - widget.device.userCode, - style: Theme.of(context).textTheme.headlineSmall, + Row( + children: [ + const Icon( + Icons.keyboard, + semanticLabel: 'Enter this code on GitHub:', + ), + const SizedBox(width: 12), + SelectableText( + widget.device.userCode, + style: Theme.of(context).textTheme.headlineSmall, + ), + ], ), const SizedBox(height: 16), - if (_error == null) - const Row( - children: [ - SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator(strokeWidth: 2), - ), - SizedBox(width: 12), - Expanded(child: Text('Waiting for authorization…')), - ], + if (_error case final error?) + Icon( + Icons.error, + color: Theme.of(context).colorScheme.error, + semanticLabel: error, ) else - Text( - _error!, - style: TextStyle(color: Theme.of(context).colorScheme.error), + Semantics( + label: 'Waiting for authorization…', + child: const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ), ), ], ), actions: [ - TextButton( + IconButton( onPressed: () => Navigator.of(context).pop(), - child: const Text('Cancel'), + icon: const Icon(Icons.close, semanticLabel: 'Cancel'), ), - FilledButton.icon( + FilledButton( onPressed: _openPage, - icon: const Icon(Icons.open_in_new), - label: const Text('Open GitHub & copy code'), + child: const Icon( + Icons.open_in_new, + semanticLabel: 'Open GitHub & copy code', + ), ), ], ); diff --git a/github_device_auth/pubspec.yaml b/github_device_auth/pubspec.yaml index e92561d..499384d 100644 --- a/github_device_auth/pubspec.yaml +++ b/github_device_auth/pubspec.yaml @@ -4,7 +4,7 @@ description: >- modal that drives it. Extracted from four apps that each carried a copy, two of them byte-identical. publish_to: 'none' -version: 0.3.2 +version: 0.4.0 environment: sdk: ^3.12.2 diff --git a/github_device_auth/test/device_code_dialog_test.dart b/github_device_auth/test/device_code_dialog_test.dart index 0fee81d..f48c5b1 100644 --- a/github_device_auth/test/device_code_dialog_test.dart +++ b/github_device_auth/test/device_code_dialog_test.dart @@ -67,6 +67,9 @@ Future _showDialog( ); await tester.tap(find.text('open')); await tester.pump(); + // Let the dialog fade in: a transparent route has no semantics yet, and + // the dialog's words are spoken labels now. + await tester.pump(const Duration(milliseconds: 300)); return () => popped; } @@ -74,9 +77,9 @@ void main() { testWidgets('shows the user code and a waiting state', (tester) async { await _showDialog(tester, _pendingAuth()); - expect(find.text('Authorize on GitHub'), findsOneWidget); + expect(find.bySemanticsLabel('Authorize on GitHub'), findsOneWidget); expect(find.text('ABCD-1234'), findsOneWidget); - expect(find.text('Waiting for authorization…'), findsOneWidget); + expect(find.bySemanticsLabel('Waiting for authorization…'), findsOneWidget); expect(find.byType(CircularProgressIndicator), findsOneWidget); }); @@ -111,7 +114,7 @@ void main() { await tester.pump(); expect(find.byType(CircularProgressIndicator), findsNothing); - expect(find.textContaining('access_denied'), findsOneWidget); + expect(find.bySemanticsLabel(RegExp('access_denied')), findsOneWidget); }); testWidgets('surfaces an Error, not just an Exception', (tester) async { @@ -126,12 +129,12 @@ void main() { await tester.pump(); expect(find.byType(CircularProgressIndicator), findsNothing); - expect(find.textContaining('No host specified'), findsOneWidget); + expect(find.bySemanticsLabel(RegExp('No host specified')), findsOneWidget); }); testWidgets('cancel pops without a token', (tester) async { final popped = await _showDialog(tester, _pendingAuth()); - await tester.tap(find.text('Cancel')); + await tester.tap(find.bySemanticsLabel('Cancel')); await tester.pumpAndSettle(); expect(find.byType(AlertDialog), findsNothing); @@ -161,7 +164,7 @@ void main() { ); await _showDialog(tester, auth); - await tester.tap(find.text('Open GitHub & copy code')); + await tester.tap(find.bySemanticsLabel('Open GitHub & copy code')); await tester.pump(); expect(copied, 'ABCD-1234'); diff --git a/sync_settings_ui/lib/src/sync_settings_cues.dart b/sync_settings_ui/lib/src/sync_settings_cues.dart new file mode 100644 index 0000000..b0b43e8 --- /dev/null +++ b/sync_settings_ui/lib/src/sync_settings_cues.dart @@ -0,0 +1,56 @@ +part of 'sync_settings_screen.dart'; + +/// The screen's glyph vocabulary, in place of written headings and status +/// sentences (kuhy's textgate rule). Every old sentence survives as the +/// spoken label, so screen readers and widget tests still find it. + +/// A result: a glyph, the old sentence (spoken only), and whether it is a +/// failure. Colour is the second channel: danger hue for failures. +class _Cue { + const _Cue(this.icon, this.spoken) : error = false; + + const _Cue.error(this.icon, this.spoken) : error = true; + + final IconData icon; + final String spoken; + final bool error; +} + +/// Paints a [_Cue] as a large glyph. +class _CueView extends StatelessWidget { + const _CueView(this.cue); + + final _Cue cue; + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + return Align( + alignment: Alignment.centerLeft, + child: Icon( + cue.icon, + size: 32, + color: cue.error ? scheme.error : scheme.primary, + semanticLabel: cue.spoken, + ), + ); + } +} + +/// A section heading as a glyph, spoken as [spoken]. +class _Heading extends StatelessWidget { + const _Heading(this.icon, this.spoken); + + final IconData icon; + final String spoken; + + @override + Widget build(BuildContext context) => Align( + alignment: Alignment.centerLeft, + child: Icon( + icon, + color: Theme.of(context).colorScheme.primary, + semanticLabel: spoken, + ), + ); +} diff --git a/sync_settings_ui/lib/src/sync_settings_screen.dart b/sync_settings_ui/lib/src/sync_settings_screen.dart index 607376d..b489d28 100644 --- a/sync_settings_ui/lib/src/sync_settings_screen.dart +++ b/sync_settings_ui/lib/src/sync_settings_screen.dart @@ -6,6 +6,7 @@ import 'package:sync_settings_ui/src/backup_slot.dart'; import 'package:sync_settings_ui/src/firebase_connect_result.dart'; import 'package:sync_settings_ui/src/firebase_sync_controller.dart'; +part 'sync_settings_cues.dart'; part 'sync_settings_sections.dart'; part 'sync_settings_widget.dart'; @@ -25,7 +26,7 @@ class _SyncSettingsScreenState extends State { bool _loading = true; bool _firebaseConnected = false; bool _busy = false; - String? _status; + _Cue? _status; @override void initState() { @@ -56,7 +57,12 @@ class _SyncSettingsScreenState extends State { final email = _emailController.text.trim(); final password = _passwordController.text; if (email.isEmpty || password.isEmpty) { - setState(() => _status = 'Enter the sync account email and password.'); + setState( + () => _status = const _Cue.error( + Icons.edit_off, + 'Enter the sync account email and password.', + ), + ); return; } setState(() => _busy = true); @@ -64,7 +70,7 @@ class _SyncSettingsScreenState extends State { email: email, password: password, onProgress: (stage) { - if (mounted) setState(() => _status = stage); + if (mounted) setState(() => _status = _Cue(Icons.hourglass_top, stage)); }, ); if (!mounted) return; @@ -74,7 +80,7 @@ class _SyncSettingsScreenState extends State { Future _connectGoogle() async { setState(() { _busy = true; - _status = 'Signing in…'; + _status = const _Cue(Icons.hourglass_top, 'Signing in…'); }); final result = await _firebase.connectWithGoogle(); if (!mounted) return; @@ -93,17 +99,35 @@ class _SyncSettingsScreenState extends State { setState(() { _busy = false; _firebaseConnected = result.outcome == FirebaseConnectOutcome.connected; + // One glyph per outcome; the sentence is spoken only. _status = switch (result.outcome) { - FirebaseConnectOutcome.connected => 'Connected to Firebase.', - FirebaseConnectOutcome.rejected => result.message != null - ? 'Firebase rejected that account: ${result.message}' - : 'Firebase rejected that account.', - FirebaseConnectOutcome.cancelled => 'Google sign-in was cancelled.', - FirebaseConnectOutcome.signedInButNotPersisted => + FirebaseConnectOutcome.connected => const _Cue( + Icons.cloud_done, + 'Connected to Firebase.', + ), + FirebaseConnectOutcome.rejected => _Cue.error( + Icons.lock, + result.message != null + ? 'Firebase rejected that account: ${result.message}' + : 'Firebase rejected that account.', + ), + FirebaseConnectOutcome.cancelled => const _Cue( + Icons.cancel_outlined, + 'Google sign-in was cancelled.', + ), + FirebaseConnectOutcome.signedInButNotPersisted => const _Cue.error( + Icons.save_as_outlined, 'Signed in, but this device did not save the session - it will ' - 'sync over GitHub after a restart. Try connecting again.', - FirebaseConnectOutcome.wrongAccount => result.message, - FirebaseConnectOutcome.failed => 'Sign-in failed: ${result.message}', + 'sync over GitHub after a restart. Try connecting again.', + ), + FirebaseConnectOutcome.wrongAccount => _Cue.error( + Icons.no_accounts, + result.message ?? '', + ), + FirebaseConnectOutcome.failed => _Cue.error( + Icons.error, + 'Sign-in failed: ${result.message}', + ), }; }); } @@ -115,32 +139,37 @@ class _SyncSettingsScreenState extends State { _passwordController.clear(); setState(() { _firebaseConnected = false; - _status = 'Firebase disconnected.'; + _status = const _Cue(Icons.link_off, 'Firebase disconnected.'); }); } Future _exportBackup(BackupSlot backup) async { try { await backup.export(); - if (mounted) setState(() => _status = 'Exported ${backup.label}.'); + _show(_Cue(Icons.upload, 'Exported ${backup.label}.')); } on BackupCancelled { - if (mounted) setState(() => _status = null); + _show(null); } on Exception catch (error) { - if (mounted) setState(() => _status = 'Export failed: $error'); + _show(_Cue.error(Icons.upload, 'Export failed: $error')); } } Future _importBackup(BackupSlot backup) async { try { await backup.import(); - if (mounted) setState(() => _status = 'Imported ${backup.label}.'); + _show(_Cue(Icons.download, 'Imported ${backup.label}.')); } on BackupCancelled { - if (mounted) setState(() => _status = null); + _show(null); } on Exception catch (error) { - if (mounted) setState(() => _status = 'Import failed: $error'); + _show(_Cue.error(Icons.download, 'Import failed: $error')); } } + /// Shows [cue] as the latest result (null clears it), if still mounted. + void _show(_Cue? cue) { + if (mounted) setState(() => _status = cue); + } + @override void dispose() { _emailController.dispose(); @@ -150,20 +179,23 @@ class _SyncSettingsScreenState extends State { @override Widget build(BuildContext context) { + // Glyphs in place of the title and section headings (kuhy's textgate + // rule: nothing he sees carries app-written words); the old words are + // the spoken labels. + const title = Icon(Icons.sync, semanticLabel: 'Sync settings'); if (_loading) { return Scaffold( - appBar: AppBar(title: const Text('Sync settings')), + appBar: AppBar(title: title), body: const Center(child: CircularProgressIndicator()), ); } final backup = widget.backup; - final textTheme = Theme.of(context).textTheme; return Scaffold( - appBar: AppBar(title: const Text('Sync settings')), + appBar: AppBar(title: title), body: ListView( padding: const EdgeInsets.all(16), children: [ - Text('Firebase sync', style: textTheme.titleMedium), + const _Heading(Icons.local_fire_department, 'Firebase sync'), const SizedBox(height: 8), _FirebaseSection( connected: _firebaseConnected, @@ -180,7 +212,7 @@ class _SyncSettingsScreenState extends State { const SizedBox(height: 24), const Divider(), const SizedBox(height: 8), - Text('Backup', style: textTheme.titleMedium), + const _Heading(Icons.backup_outlined, 'Backup'), const SizedBox(height: 8), _BackupSection( backup: backup, @@ -188,9 +220,9 @@ class _SyncSettingsScreenState extends State { onImport: () => _importBackup(backup), ), ], - if (_status != null) ...[ + if (_status case final status?) ...[ const SizedBox(height: 16), - Text(_status!, style: textTheme.bodyMedium), + _CueView(status), ], ], ), diff --git a/sync_settings_ui/lib/src/sync_settings_sections.dart b/sync_settings_ui/lib/src/sync_settings_sections.dart index 426a0b0..d68dbfe 100644 --- a/sync_settings_ui/lib/src/sync_settings_sections.dart +++ b/sync_settings_ui/lib/src/sync_settings_sections.dart @@ -36,9 +36,9 @@ class _FirebaseSection extends StatelessWidget { const Icon(Icons.cloud_done, size: 20), const SizedBox(width: 8), Expanded(child: Text(emailController.text)), - TextButton( + IconButton( onPressed: busy ? null : onDisconnect, - child: const Text('Disconnect'), + icon: const Icon(Icons.link_off, semanticLabel: 'Disconnect'), ), ], ); @@ -49,10 +49,12 @@ class _FirebaseSection extends StatelessWidget { if (googleAvailable) ...[ Align( alignment: Alignment.centerLeft, - child: FilledButton.icon( + child: FilledButton( onPressed: busy ? null : onConnectGoogle, - icon: const Icon(Icons.account_circle), - label: const Text('Sign in with Google'), + child: const Icon( + Icons.account_circle, + semanticLabel: 'Sign in with Google', + ), ), ), const SizedBox(height: 12), @@ -61,24 +63,35 @@ class _FirebaseSection extends StatelessWidget { // is not the "control that can never succeed" trap -- that one // looks live and reports a fake cancellation. This one says why, // which beats a screen that silently offers less on one platform. - Align( - alignment: Alignment.centerLeft, - child: FilledButton.icon( - onPressed: null, - icon: const Icon(Icons.account_circle), - label: const Text('Sign in with Google'), - ), - ), - const SizedBox(height: 4), - Text( - googleUnavailableReason!, - style: Theme.of(context).textTheme.bodySmall, + // The why is a block glyph beside it, the reason spoken only. + Row( + children: [ + const FilledButton( + onPressed: null, + child: Icon( + Icons.account_circle, + semanticLabel: 'Sign in with Google', + ), + ), + const SizedBox(width: 8), + Icon( + Icons.block, + color: Theme.of(context).colorScheme.error, + semanticLabel: googleUnavailableReason, + ), + ], ), const SizedBox(height: 12), ], ExpansionTile( initiallyExpanded: !googleAvailable, - title: const Text('Use the account password instead'), + title: const Align( + alignment: Alignment.centerLeft, + child: Icon( + Icons.password, + semanticLabel: 'Use the account password instead', + ), + ), tilePadding: EdgeInsets.zero, childrenPadding: const EdgeInsets.only(bottom: 8), children: [ @@ -87,7 +100,10 @@ class _FirebaseSection extends StatelessWidget { keyboardType: TextInputType.emailAddress, autocorrect: false, decoration: const InputDecoration( - labelText: 'Sync account email', + prefixIcon: Icon( + Icons.alternate_email, + semanticLabel: 'Sync account email', + ), border: OutlineInputBorder(), ), ), @@ -97,17 +113,22 @@ class _FirebaseSection extends StatelessWidget { obscureText: true, autocorrect: false, decoration: const InputDecoration( - labelText: 'Sync account password', + prefixIcon: Icon( + Icons.lock_outline, + semanticLabel: 'Sync account password', + ), border: OutlineInputBorder(), ), ), const SizedBox(height: 12), Align( alignment: Alignment.centerLeft, - child: FilledButton.icon( + child: FilledButton( onPressed: busy ? null : onConnectPassword, - icon: const Icon(Icons.cloud_done), - label: const Text('Connect Firebase'), + child: const Icon( + Icons.cloud_done, + semanticLabel: 'Connect Firebase', + ), ), ), ], @@ -133,18 +154,19 @@ class _BackupSection extends StatelessWidget { return Row( children: [ Expanded( - child: OutlinedButton.icon( + child: OutlinedButton( onPressed: onExport, - icon: const Icon(Icons.upload), - label: Text('Export ${backup.label}'), + child: Icon(Icons.upload, semanticLabel: 'Export ${backup.label}'), ), ), const SizedBox(width: 12), Expanded( - child: OutlinedButton.icon( + child: OutlinedButton( onPressed: onImport, - icon: const Icon(Icons.download), - label: Text('Import ${backup.label}'), + child: Icon( + Icons.download, + semanticLabel: 'Import ${backup.label}', + ), ), ), ], diff --git a/sync_settings_ui/pubspec.yaml b/sync_settings_ui/pubspec.yaml index 631432e..06eaf44 100644 --- a/sync_settings_ui/pubspec.yaml +++ b/sync_settings_ui/pubspec.yaml @@ -5,7 +5,7 @@ description: >- injected closures -- no keystore or platform plugin access lives in this package itself. publish_to: 'none' -version: 0.6.3 +version: 0.7.0 environment: sdk: ^3.12.2 diff --git a/sync_settings_ui/test/sync_settings_screen_backup_test.dart b/sync_settings_ui/test/sync_settings_screen_backup_test.dart index 16ec9a1..c772fae 100644 --- a/sync_settings_ui/test/sync_settings_screen_backup_test.dart +++ b/sync_settings_ui/test/sync_settings_screen_backup_test.dart @@ -38,15 +38,15 @@ void main() { ); await tester.pumpAndSettle(); - await tester.tap(find.text('Export notes')); + await tester.tap(find.bySemanticsLabel('Export notes')); await tester.pumpAndSettle(); - expect(find.text('Exported notes.'), findsNothing); - expect(find.textContaining('failed'), findsNothing); + expect(find.bySemanticsLabel('Exported notes.'), findsNothing); + expect(spoken('failed'), findsNothing); - await tester.tap(find.text('Import notes')); + await tester.tap(find.bySemanticsLabel('Import notes')); await tester.pumpAndSettle(); - expect(find.text('Imported notes.'), findsNothing); - expect(find.textContaining('failed'), findsNothing); + expect(find.bySemanticsLabel('Imported notes.'), findsNothing); + expect(spoken('failed'), findsNothing); }); testWidgets('omitted entirely when backup is null', (tester) async { @@ -63,7 +63,7 @@ void main() { ); await tester.pumpAndSettle(); - expect(find.text('Backup'), findsNothing); + expect(find.bySemanticsLabel('Backup'), findsNothing); }); testWidgets('export and import call the injected callbacks', ( @@ -89,16 +89,16 @@ void main() { ); await tester.pumpAndSettle(); - expect(find.text('Backup'), findsOneWidget); - await tester.tap(find.text('Export notes')); + expect(find.bySemanticsLabel('Backup'), findsOneWidget); + await tester.tap(find.bySemanticsLabel('Export notes')); await tester.pumpAndSettle(); expect(exported, isTrue); - expect(find.text('Exported notes.'), findsOneWidget); + expect(find.bySemanticsLabel('Exported notes.'), findsOneWidget); - await tester.tap(find.text('Import notes')); + await tester.tap(find.bySemanticsLabel('Import notes')); await tester.pumpAndSettle(); expect(imported, isTrue); - expect(find.text('Imported notes.'), findsOneWidget); + expect(find.bySemanticsLabel('Imported notes.'), findsOneWidget); }); testWidgets('export failure reports the error', (tester) async { @@ -120,9 +120,9 @@ void main() { ); await tester.pumpAndSettle(); - await tester.tap(find.text('Export notes')); + await tester.tap(find.bySemanticsLabel('Export notes')); await tester.pumpAndSettle(); - expect(find.textContaining('Export failed'), findsOneWidget); + expect(spoken('Export failed'), findsOneWidget); }); testWidgets('import failure reports the error', (tester) async { @@ -144,9 +144,9 @@ void main() { ); await tester.pumpAndSettle(); - await tester.tap(find.text('Import notes')); + await tester.tap(find.bySemanticsLabel('Import notes')); await tester.pumpAndSettle(); - expect(find.textContaining('Import failed'), findsOneWidget); + expect(spoken('Import failed'), findsOneWidget); }); }); @@ -173,3 +173,7 @@ void main() { expect(find.byType(CircularProgressIndicator), findsNothing); }); } + +/// A node whose spoken label contains [part] literally. +Finder spoken(String part) => + find.bySemanticsLabel(RegExp(RegExp.escape(part))); diff --git a/sync_settings_ui/test/sync_settings_screen_google_test.dart b/sync_settings_ui/test/sync_settings_screen_google_test.dart index 30ba78d..61fa8a5 100644 --- a/sync_settings_ui/test/sync_settings_screen_google_test.dart +++ b/sync_settings_ui/test/sync_settings_screen_google_test.dart @@ -35,10 +35,10 @@ void main() { ); await tester.pumpAndSettle(); - await tester.tap(find.text('Sign in with Google')); + await tester.tap(find.bySemanticsLabel('Sign in with Google')); await tester.pumpAndSettle(); - expect(find.text('Google sign-in was cancelled.'), findsOneWidget); + expect(find.bySemanticsLabel('Google sign-in was cancelled.'), findsOneWidget); }); testWidgets( @@ -60,11 +60,11 @@ void main() { ); await tester.pumpAndSettle(); - await tester.tap(find.text('Sign in with Google')); + await tester.tap(find.bySemanticsLabel('Sign in with Google')); await tester.pumpAndSettle(); expect( - find.textContaining('did not save the session'), + spoken('did not save the session'), findsOneWidget, ); }, @@ -89,10 +89,10 @@ void main() { ); await tester.pumpAndSettle(); - await tester.tap(find.text('Sign in with Google')); + await tester.tap(find.bySemanticsLabel('Sign in with Google')); await tester.pumpAndSettle(); - expect(find.text('wrong uid'), findsOneWidget); + expect(find.bySemanticsLabel('wrong uid'), findsOneWidget); }); testWidgets('Google sign-in generic failure surfaces the error text', ( @@ -113,10 +113,10 @@ void main() { ); await tester.pumpAndSettle(); - await tester.tap(find.text('Sign in with Google')); + await tester.tap(find.bySemanticsLabel('Sign in with Google')); await tester.pumpAndSettle(); - expect(find.textContaining('Sign-in failed'), findsOneWidget); + expect(spoken('Sign-in failed'), findsOneWidget); }); testWidgets( @@ -138,7 +138,7 @@ void main() { ); await tester.pumpAndSettle(); - expect(find.text('Sign in with Google'), findsNothing); + expect(find.bySemanticsLabel('Sign in with Google'), findsNothing); }, ); @@ -161,12 +161,12 @@ void main() { ); await tester.pumpAndSettle(); - expect(find.text('Sign in with Google'), findsOneWidget); - expect(find.text('Not available on this platform.'), findsOneWidget); + expect(find.bySemanticsLabel('Sign in with Google'), findsOneWidget); + expect(find.bySemanticsLabel('Not available on this platform.'), findsOneWidget); // Disabled, so it cannot report a misleading "cancelled". final button = tester.widget( - find.widgetWithText(FilledButton, 'Sign in with Google'), + find.widgetWithIcon(FilledButton, Icons.account_circle), ); expect(button.onPressed, isNull); }); @@ -189,7 +189,11 @@ void main() { ); await tester.pumpAndSettle(); - expect(find.text('Sign in with Google'), findsNothing); + expect(find.bySemanticsLabel('Sign in with Google'), findsNothing); }); }); } + +/// A node whose spoken label contains [part] literally. +Finder spoken(String part) => + find.bySemanticsLabel(RegExp(RegExp.escape(part))); diff --git a/sync_settings_ui/test/sync_settings_screen_test.dart b/sync_settings_ui/test/sync_settings_screen_test.dart index f863297..ff287af 100644 --- a/sync_settings_ui/test/sync_settings_screen_test.dart +++ b/sync_settings_ui/test/sync_settings_screen_test.dart @@ -52,7 +52,7 @@ void main() { await tester.pumpAndSettle(); expect(find.text('a@b.com'), findsOneWidget); - expect(find.text('Disconnect'), findsOneWidget); + expect(find.bySemanticsLabel('Disconnect'), findsOneWidget); }); testWidgets('disconnect clears the account and shows status', ( @@ -73,12 +73,12 @@ void main() { ); await tester.pumpAndSettle(); - await tester.tap(find.text('Disconnect')); + await tester.tap(find.bySemanticsLabel('Disconnect')); await tester.pumpAndSettle(); expect(cleared, isTrue); - expect(find.text('Firebase disconnected.'), findsOneWidget); - expect(find.text('Connect Firebase'), findsOneWidget); + expect(find.bySemanticsLabel('Firebase disconnected.'), findsOneWidget); + expect(find.bySemanticsLabel('Connect Firebase'), findsOneWidget); }); testWidgets('connect with password shows an error when empty', ( @@ -98,11 +98,11 @@ void main() { await tester.pumpAndSettle(); // Google unavailable by default -> password form starts expanded. - await tester.tap(find.text('Connect Firebase')); + await tester.tap(find.bySemanticsLabel('Connect Firebase')); await tester.pumpAndSettle(); expect( - find.text('Enter the sync account email and password.'), + find.bySemanticsLabel('Enter the sync account email and password.'), findsOneWidget, ); }); @@ -124,17 +124,17 @@ void main() { await tester.pumpAndSettle(); await tester.enterText( - find.widgetWithText(TextField, 'Sync account email'), + find.widgetWithIcon(TextField, Icons.alternate_email), 'a@b.com', ); await tester.enterText( - find.widgetWithText(TextField, 'Sync account password'), + find.widgetWithIcon(TextField, Icons.lock_outline), 'pw', ); - await tester.tap(find.text('Connect Firebase')); + await tester.tap(find.bySemanticsLabel('Connect Firebase')); await tester.pumpAndSettle(); - expect(find.text('Connected to Firebase.'), findsOneWidget); + expect(find.bySemanticsLabel('Connected to Firebase.'), findsOneWidget); expect(find.text('a@b.com'), findsOneWidget); }); @@ -153,17 +153,17 @@ void main() { await tester.pumpAndSettle(); await tester.enterText( - find.widgetWithText(TextField, 'Sync account email'), + find.widgetWithIcon(TextField, Icons.alternate_email), 'a@b.com', ); await tester.enterText( - find.widgetWithText(TextField, 'Sync account password'), + find.widgetWithIcon(TextField, Icons.lock_outline), 'wrong', ); - await tester.tap(find.text('Connect Firebase')); + await tester.tap(find.bySemanticsLabel('Connect Firebase')); await tester.pumpAndSettle(); - expect(find.text('Firebase rejected that account.'), findsOneWidget); + expect(find.bySemanticsLabel('Firebase rejected that account.'), findsOneWidget); }); testWidgets( @@ -186,23 +186,23 @@ void main() { await tester.pumpAndSettle(); await tester.enterText( - find.widgetWithText(TextField, 'Sync account email'), + find.widgetWithIcon(TextField, Icons.alternate_email), 'a@b.com', ); await tester.enterText( - find.widgetWithText(TextField, 'Sync account password'), + find.widgetWithIcon(TextField, Icons.lock_outline), 'wrong', ); - await tester.tap(find.text('Connect Firebase')); + await tester.tap(find.bySemanticsLabel('Connect Firebase')); await tester.pumpAndSettle(); - expect(find.text('Signing in to Firebase…'), findsNothing); + expect(find.bySemanticsLabel('Signing in to Firebase…'), findsNothing); expect( - find.text('Firebase rejected that account: bad creds'), + find.bySemanticsLabel('Firebase rejected that account: bad creds'), findsOneWidget, ); final button = tester.widget( - find.widgetWithText(FilledButton, 'Connect Firebase'), + find.widgetWithIcon(FilledButton, Icons.cloud_done), ); expect(button.onPressed, isNotNull); }, @@ -231,18 +231,18 @@ void main() { ); await tester.pumpAndSettle(); - expect(find.text('Sign in with Google'), findsOneWidget); + expect(find.bySemanticsLabel('Sign in with Google'), findsOneWidget); // The password form is collapsed behind its ExpansionTile when // Google is offered -- only the field labels prove that; the tile's // own title text is present either way. expect( - find.widgetWithText(TextField, 'Sync account email'), + find.widgetWithIcon(TextField, Icons.alternate_email), findsNothing, ); - await tester.tap(find.text('Sign in with Google')); + await tester.tap(find.bySemanticsLabel('Sign in with Google')); await tester.pumpAndSettle(); - expect(find.text('Connected to Firebase.'), findsOneWidget); + expect(find.bySemanticsLabel('Connected to Firebase.'), findsOneWidget); }, ); });