mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-14 04:10:58 +01:00
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
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:
parent
9d6b17fd28
commit
3084080d74
@ -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',
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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');
|
||||
|
||||
56
sync_settings_ui/lib/src/sync_settings_cues.dart
Normal file
56
sync_settings_ui/lib/src/sync_settings_cues.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -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),
|
||||
],
|
||||
],
|
||||
),
|
||||
|
||||
@ -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}',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)));
|
||||
|
||||
@ -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)));
|
||||
|
||||
@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user