sync_settings_ui 0.7.0, gda 0.4.0: text-free
Some checks failed
dependency freshness / dependencies on newest stable (push) Waiting to run
file length / file-length (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
palette drift / palette (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
shared gate tests / gate unit tests (push) Waiting to run
github_device_auth tests / test (push) Has been cancelled
sync_settings_ui tests / test (push) Has been cancelled

Glyph + spoken label replaces every app-written string on the shared
Sync settings screen and the GitHub device-code dialog (kuhy's
textgate rule). Old sentences are semantics labels; tests find them
there. The device user code stays visible (GitHub's data).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Krzysztof Rudnicki 2026-11-11 01:27:57 +01:00
parent 9d6b17fd28
commit 3084080d74
10 changed files with 273 additions and 142 deletions

View File

@ -64,47 +64,57 @@ class _DeviceCodeDialogState extends State<DeviceCodeDialog> {
@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',
),
),
],
);

View File

@ -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

View File

@ -67,6 +67,9 @@ Future<Object? Function()> _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');

View File

@ -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,
),
);
}

View File

@ -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<SyncSettingsScreen> {
bool _loading = true;
bool _firebaseConnected = false;
bool _busy = false;
String? _status;
_Cue? _status;
@override
void initState() {
@ -56,7 +57,12 @@ class _SyncSettingsScreenState extends State<SyncSettingsScreen> {
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<SyncSettingsScreen> {
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<SyncSettingsScreen> {
Future<void> _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<SyncSettingsScreen> {
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<SyncSettingsScreen> {
_passwordController.clear();
setState(() {
_firebaseConnected = false;
_status = 'Firebase disconnected.';
_status = const _Cue(Icons.link_off, 'Firebase disconnected.');
});
}
Future<void> _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<void> _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<SyncSettingsScreen> {
@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<SyncSettingsScreen> {
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<SyncSettingsScreen> {
onImport: () => _importBackup(backup),
),
],
if (_status != null) ...[
if (_status case final status?) ...[
const SizedBox(height: 16),
Text(_status!, style: textTheme.bodyMedium),
_CueView(status),
],
],
),

View File

@ -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}',
),
),
),
],

View File

@ -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

View File

@ -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)));

View File

@ -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<FilledButton>(
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)));

View File

@ -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<FilledButton>(
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);
},
);
});