From b584c0441bcc0d12ed16275c148e2d7005f8f234 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Wed, 11 Nov 2026 01:28:08 +0100 Subject: [PATCH] Make settings screen text-free Headings are glyphs, helper paragraphs are gone, statuses are a tick/warning glyph with the sentence spoken, the exercise summary is glyphs + numbers, PC pairing a link glyph. Co-Authored-By: Claude Opus 5.5 --- .../lib/screens/settings_screen.dart | 10 +-- .../settings_screen_exercise_list.dart | 85 ++++++++++-------- .../settings_screen_exercise_sections.dart | 27 +----- .../lib/screens/settings_screen_pc_link.dart | 69 +++++++------- .../lib/screens/settings_screen_rows.dart | 50 ++++++++--- .../lib/screens/settings_screen_sandbox.dart | 36 +++----- .../lib/screens/settings_screen_sections.dart | 89 +++++++++---------- .../settings_screen_exercises_test.dart | 36 ++++++-- .../screens/settings_screen_pc_link_test.dart | 14 ++- .../screens/settings_screen_reset_test.dart | 13 ++- .../screens/settings_screen_sandbox_test.dart | 18 ++-- .../screens/settings_screen_sync_test.dart | 13 +-- .../test/screens/settings_screen_test.dart | 22 +++-- .../settings_screen_thresholds_test.dart | 26 +++--- .../settings_screen_transfer_test.dart | 7 +- 15 files changed, 271 insertions(+), 244 deletions(-) diff --git a/stronglift_replacement/workout_app/lib/screens/settings_screen.dart b/stronglift_replacement/workout_app/lib/screens/settings_screen.dart index 1af8864..a0c6bda 100644 --- a/stronglift_replacement/workout_app/lib/screens/settings_screen.dart +++ b/stronglift_replacement/workout_app/lib/screens/settings_screen.dart @@ -54,7 +54,8 @@ class _SettingsScreenState extends State { bool _soundEnabled = true; // Set after a Sync settings visit restores progression; the banner stays. - String? _progressionStatus; + /// Exercises the last sync-settings visit restored; null until one did. + int? _restoredCount; @override void initState() { @@ -126,10 +127,7 @@ class _SettingsScreenState extends State { ProgressionSyncService().pullProgression)(); if (!mounted) return; if (restored.changed) { - setState( - () => _progressionStatus = - 'Restored ${restored.count} exercise(s) from Firebase.', - ); + setState(() => _restoredCount = restored.count); await _load(); } } @@ -225,7 +223,7 @@ class _SettingsScreenState extends State { _SoundSection(enabled: _soundEnabled, onChanged: _setSound), const SizedBox(height: 20), _SyncSection( - progressionStatus: _progressionStatus, + restoredCount: _restoredCount, onOpenSyncSettings: () => unawaited(_openSyncSettings()), onOpenGitHubMirror: () => unawaited(_openGitHubMirror()), ), diff --git a/stronglift_replacement/workout_app/lib/screens/settings_screen_exercise_list.dart b/stronglift_replacement/workout_app/lib/screens/settings_screen_exercise_list.dart index 4d32478..c8a018d 100644 --- a/stronglift_replacement/workout_app/lib/screens/settings_screen_exercise_list.dart +++ b/stronglift_replacement/workout_app/lib/screens/settings_screen_exercise_list.dart @@ -25,20 +25,11 @@ class _ExercisesSection extends StatelessWidget { @override Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const _SectionHeader('PER EXERCISE'), - const SizedBox(height: 4), - Text( - 'Progression, warmup, rest and injury pause. ' - 'Same settings as the workout tile.', - style: TextStyle( - color: colorScheme.onSurfaceVariant, - fontSize: AppTextSize.caption, - ), - ), + // textgate: the helper sentence under the heading is gone. + const _SectionHeader(Icons.tune, 'PER EXERCISE'), const SizedBox(height: 12), ...orderedNames.map((name) { final s = states[name]; @@ -57,19 +48,38 @@ class _ExerciseSettingsRow extends StatelessWidget { final ExerciseState state; final VoidCallback onTap; - String get _summary { + // textgate: was "kg · ↑3 ↓2 · rest 3:00 / 5:00 · warmup 1:00" (or + // "no warmup"). Now the mode glyph (or rep range), the thresholds, a + // timer glyph before the rests, and a sun glyph before the warmup rest + // (absent when there is no warmup). + Widget _summary(Color color) { final s = state; - final mode = switch (s.mode) { - ProgressionMode.weight => 'kg', - ProgressionMode.reps => 'reps', - ProgressionMode.doubleProgression => '${s.repsLow}→${s.repsHigh} reps', + final style = TextStyle(color: color, fontSize: AppTextSize.caption); + final modeIcon = switch (s.mode) { + ProgressionMode.weight => Icons.fitness_center, + ProgressionMode.reps => Icons.repeat, + ProgressionMode.doubleProgression => null, }; - final warmup = s.hasWarmup - ? 'warmup ${formatRest(s.restWarmupSecs)}' - : 'no warmup'; - return '$mode · ↑${s.successThreshold} ↓${s.failThreshold} · ' - 'rest ${formatRest(s.restSuccessSecs)} / ' - '${formatRest(s.restFailSecs)} · $warmup'; + return Row( + key: const Key('exercise_settings_summary'), + children: [ + if (modeIcon == null) + Text('${s.repsLow}→${s.repsHigh}', style: style) + else + Icon(modeIcon, size: 14, color: color), + Text(' ↑${s.successThreshold} ↓${s.failThreshold} ', style: style), + Icon(Icons.timer, size: 14, color: color), + Text( + ' ${formatRest(s.restSuccessSecs)} / ${formatRest(s.restFailSecs)}', + style: style, + ), + if (s.hasWarmup) ...[ + const SizedBox(width: 8), + Icon(Icons.wb_sunny, size: 14, color: color), + Text(' ${formatRest(s.restWarmupSecs)}', style: style), + ], + ], + ); } @override @@ -108,21 +118,26 @@ class _ExerciseSettingsRow extends StatelessWidget { ), ), const SizedBox(height: 4), - Text( - _summary, - style: TextStyle( - color: colorScheme.onSurfaceVariant, - fontSize: AppTextSize.caption, - ), - ), + _summary(colorScheme.onSurfaceVariant), if (until != null) ...[ const SizedBox(height: 4), - Text( - 'Paused — back on ${formatShortDate(until)}', - style: TextStyle( - color: colorScheme.error, - fontSize: AppTextSize.caption, - ), + // textgate: was "Paused — back on 11 Oct". + Row( + children: [ + Icon( + Icons.pause_circle_outline, + size: 14, + color: colorScheme.error, + ), + const SizedBox(width: 4), + Text( + formatShortDate(until), + style: TextStyle( + color: colorScheme.error, + fontSize: AppTextSize.caption, + ), + ), + ], ), ], ], diff --git a/stronglift_replacement/workout_app/lib/screens/settings_screen_exercise_sections.dart b/stronglift_replacement/workout_app/lib/screens/settings_screen_exercise_sections.dart index 0a79806..57cb8b2 100644 --- a/stronglift_replacement/workout_app/lib/screens/settings_screen_exercise_sections.dart +++ b/stronglift_replacement/workout_app/lib/screens/settings_screen_exercise_sections.dart @@ -26,20 +26,11 @@ class _WeightsSection extends StatelessWidget { @override Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const _SectionHeader('WEIGHTS'), - const SizedBox(height: 4), - Text( - 'Override current working weight. ' - 'Resets streak counters. Rounded to 2.5 kg.', - style: TextStyle( - color: colorScheme.onSurfaceVariant, - fontSize: AppTextSize.caption, - ), - ), + // textgate: the helper sentence under the heading is gone. + const _SectionHeader(Icons.fitness_center, 'WEIGHTS'), const SizedBox(height: 12), ...orderedNames.map((name) { final w = weights[name]; @@ -74,21 +65,11 @@ class _RepsSection extends StatelessWidget { @override Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const _SectionHeader('TARGET REPS'), - const SizedBox(height: 4), - Text( - 'Override target reps per set. Resets streak counters. ' - 'Progression only ever raises reps, so this is the only way ' - 'to lower one.', - style: TextStyle( - color: colorScheme.onSurfaceVariant, - fontSize: AppTextSize.caption, - ), - ), + // textgate: the helper sentence under the heading is gone. + const _SectionHeader(Icons.repeat, 'TARGET REPS'), const SizedBox(height: 12), ...orderedNames.map((name) { final r = reps[name]; diff --git a/stronglift_replacement/workout_app/lib/screens/settings_screen_pc_link.dart b/stronglift_replacement/workout_app/lib/screens/settings_screen_pc_link.dart index 5f17f4b..815601c 100644 --- a/stronglift_replacement/workout_app/lib/screens/settings_screen_pc_link.dart +++ b/stronglift_replacement/workout_app/lib/screens/settings_screen_pc_link.dart @@ -20,7 +20,7 @@ class _PcLinkSection extends StatefulWidget { class _PcLinkSectionState extends State<_PcLinkSection> { final TextEditingController _host = TextEditingController(); bool? _paired; - String? _status; + ({bool ok, String spoken})? _status; @override void initState() { @@ -44,32 +44,39 @@ class _PcLinkSectionState extends State<_PcLinkSection> { Future _save() async { final host = _host.text.trim(); if (!isValidPcHost(host)) { - setState(() => _status = 'Not an IPv4 address or hostname: "$host".'); + setState( + () => _status = ( + ok: false, + spoken: 'Not an IPv4 address or hostname: "$host".', + ), + ); return; } await PcPairing.saveHost(host); SandboxLog.event('pc host saved', {'host': host}); - setState(() => _status = 'Workouts are now sent to $host:$kPcPokePort.'); + setState( + () => _status = ( + ok: true, + spoken: 'Workouts are now sent to $host:$kPcPokePort.', + ), + ); } + static String _pairedSemanticsLabel(bool? paired) => switch (paired) { + null => 'PC paired: checking…', + true => 'PC paired ✓', + false => 'PC paired ✗ — run scripts/pair_phone.sh on the PC', + }; + @override Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; final status = Theme.of(context).extension()!; final paired = _paired; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const _SectionHeader('PC LINK'), - const SizedBox(height: AppSpacing.xs), - Text( - 'A finished workout is sent straight to the PC on the home Wi-Fi, ' - 'so the credit lands in a second. Sync still delivers it otherwise.', - style: TextStyle( - color: colorScheme.onSurfaceVariant, - fontSize: AppTextSize.caption, - ), - ), + // textgate: the explanation sentence under the heading is gone. + const _SectionHeader(Icons.computer, 'PC LINK'), const SizedBox(height: AppSpacing.sm), Row( children: [ @@ -96,27 +103,27 @@ class _PcLinkSectionState extends State<_PcLinkSection> { ], ), const SizedBox(height: AppSpacing.sm), - Text( - switch (paired) { - null => 'PC paired: checking…', - true => 'PC paired ✓', - false => 'PC paired ✗ — run scripts/pair_phone.sh on the PC', - }, - key: const Key('pc-paired'), - style: TextStyle( + // textgate: was "PC paired: checking…" / "PC paired ✓" / "PC + // paired ✗ — run scripts/pair_phone.sh on the PC". Now a link glyph + // (hourglass while checking, broken link when unpaired) in the + // success / warning colour. + Align( + alignment: Alignment.centerLeft, + child: Icon( + switch (paired) { + null => Icons.hourglass_empty, + true => Icons.link, + false => Icons.link_off, + }, + key: const Key('pc-paired'), + size: 20, color: paired ?? false ? status.success : status.warning, - fontSize: AppTextSize.caption, + semanticLabel: _pairedSemanticsLabel(paired), ), ), - if (_status != null) ...[ + if (_status case final s?) ...[ const SizedBox(height: AppSpacing.xs), - Text( - _status!, - style: TextStyle( - color: colorScheme.onSurfaceVariant, - fontSize: AppTextSize.caption, - ), - ), + _StatusGlyph(ok: s.ok, spoken: s.spoken), ], ], ); diff --git a/stronglift_replacement/workout_app/lib/screens/settings_screen_rows.dart b/stronglift_replacement/workout_app/lib/screens/settings_screen_rows.dart index d73c44d..3219a38 100644 --- a/stronglift_replacement/workout_app/lib/screens/settings_screen_rows.dart +++ b/stronglift_replacement/workout_app/lib/screens/settings_screen_rows.dart @@ -4,19 +4,23 @@ // See history_screen_charts.dart for the full reasoning. part of 'settings_screen.dart'; +/// A section heading as a glyph (textgate: was an all-caps word; the word +/// survives as [spoken], the glyph's semantic label). class _SectionHeader extends StatelessWidget { - const _SectionHeader(this.text); + const _SectionHeader(this.icon, this.spoken); - final String text; + final IconData icon; + final String spoken; @override Widget build(BuildContext context) { - return Text( - text, - style: TextStyle( + return Align( + alignment: Alignment.centerLeft, + child: Icon( + icon, + size: 20, color: Theme.of(context).colorScheme.onSurfaceVariant, - fontSize: AppTextSize.caption, - letterSpacing: 1.4, + semanticLabel: spoken, ), ); } @@ -56,7 +60,8 @@ class _RepsRow extends StatelessWidget { SizedBox( width: 72, child: Text( - '$reps reps', + // textgate: was "12 reps"; the section's repeat glyph says it. + '×$reps', textAlign: TextAlign.center, style: TextStyle( color: colorScheme.onSurface, @@ -104,9 +109,8 @@ class _WeightRow extends StatelessWidget { ), _StepperButton( icon: Icons.remove, - onTap: () => onChanged( - (weight - kWeightIncrement).clamp(0.0, 999.0), - ), + onTap: () => + onChanged((weight - kWeightIncrement).clamp(0.0, 999.0)), ), // Fixed-width container supports up to "999.9kg" (7 chars). SizedBox( @@ -155,3 +159,27 @@ class _StepperButton extends StatelessWidget { ); } } + +/// An action's outcome as a glyph: a tick (success colour) or a warning +/// sign (warning colour). textgate: replaces a painted status sentence; +/// the sentence survives as [spoken], the glyph's semantic label. +class _StatusGlyph extends StatelessWidget { + const _StatusGlyph({required this.ok, required this.spoken, super.key}); + + final bool ok; + final String spoken; + + @override + Widget build(BuildContext context) { + final status = Theme.of(context).extension()!; + return Align( + alignment: Alignment.centerLeft, + child: Icon( + ok ? Icons.check_circle : Icons.error_outline, + size: 18, + color: ok ? status.success : status.warning, + semanticLabel: spoken, + ), + ); + } +} diff --git a/stronglift_replacement/workout_app/lib/screens/settings_screen_sandbox.dart b/stronglift_replacement/workout_app/lib/screens/settings_screen_sandbox.dart index f9f2d7e..6c95482 100644 --- a/stronglift_replacement/workout_app/lib/screens/settings_screen_sandbox.dart +++ b/stronglift_replacement/workout_app/lib/screens/settings_screen_sandbox.dart @@ -18,7 +18,7 @@ class _SandboxSectionState extends State<_SandboxSection> { late final TextEditingController _rest = TextEditingController( text: '${Sandbox.restSecs}', ); - String? _status; + ({bool ok, String spoken})? _status; @override void dispose() { @@ -26,9 +26,9 @@ class _SandboxSectionState extends State<_SandboxSection> { super.dispose(); } - void _report(String status) { + void _report(String status, {bool ok = true}) { SandboxLog.event('sandbox settings', {'status': status}); - setState(() => _status = status); + setState(() => _status = (ok: ok, spoken: status)); } Future _wipe() async { @@ -53,7 +53,10 @@ class _SandboxSectionState extends State<_SandboxSection> { Future _saveRest() async { final secs = int.tryParse(_rest.text.trim()); if (secs == null || secs < 1) { - _report('Rest length must be a whole number of seconds, 1 or more.'); + _report( + 'Rest length must be a whole number of seconds, 1 or more.', + ok: false, + ); return; } await StorageService.instance.setSandboxRestSecs(secs); @@ -73,17 +76,8 @@ class _SandboxSectionState extends State<_SandboxSection> { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const _SectionHeader('SANDBOX'), - const SizedBox(height: 4), - Text( - 'This install is the sandbox: its own data, no sync (only the ' - 'signed sandbox poke to the PC LINK address, which the PC never ' - 'credits), no LAN server. Nothing here can reach the daily build.', - style: TextStyle( - color: colorScheme.onSurfaceVariant, - fontSize: AppTextSize.caption, - ), - ), + // textgate: the explanation paragraph under the heading is gone. + const _SectionHeader(Icons.science, 'SANDBOX'), const SizedBox(height: 12), Wrap( spacing: 8, @@ -151,14 +145,12 @@ class _SandboxSectionState extends State<_SandboxSection> { ), ], ), - if (_status != null) ...[ + if (_status case final s?) ...[ const SizedBox(height: 8), - Text( - _status!, - style: TextStyle( - color: colorScheme.onSurfaceVariant, - fontSize: AppTextSize.caption, - ), + _StatusGlyph( + key: const Key('sandbox-status'), + ok: s.ok, + spoken: s.spoken, ), ], ], diff --git a/stronglift_replacement/workout_app/lib/screens/settings_screen_sections.dart b/stronglift_replacement/workout_app/lib/screens/settings_screen_sections.dart index 51cf15f..e7ad7d4 100644 --- a/stronglift_replacement/workout_app/lib/screens/settings_screen_sections.dart +++ b/stronglift_replacement/workout_app/lib/screens/settings_screen_sections.dart @@ -13,13 +13,13 @@ part of 'settings_screen.dart'; /// The SYNC section: progression status line plus the two sync routes. class _SyncSection extends StatelessWidget { const _SyncSection({ - required this.progressionStatus, + required this.restoredCount, required this.onOpenSyncSettings, required this.onOpenGitHubMirror, }); - /// Last progression-sync outcome, or null when there is nothing to report. - final String? progressionStatus; + /// Exercises the last progression pull restored, or null when none did. + final int? restoredCount; /// Opens the Firebase sync settings screen. final VoidCallback onOpenSyncSettings; @@ -33,15 +33,29 @@ class _SyncSection extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const _SectionHeader('SYNC'), + const _SectionHeader(Icons.sync, 'SYNC'), const SizedBox(height: 4), - if (progressionStatus != null) ...[ - Text( - progressionStatus!, - style: TextStyle( - color: colorScheme.onSurfaceVariant, - fontSize: AppTextSize.caption, - ), + // textgate: was "Restored N exercise(s) from Firebase."; now a + // download glyph and the count, in the success colour. + if (restoredCount != null) ...[ + Row( + key: const Key('settings_restored'), + children: [ + Icon( + Icons.cloud_download, + size: 18, + color: Theme.of(context).extension()!.success, + semanticLabel: 'Restored from Firebase', + ), + const SizedBox(width: 6), + Text( + '$restoredCount', + style: TextStyle( + color: colorScheme.onSurfaceVariant, + fontSize: AppTextSize.caption, + ), + ), + ], ), const SizedBox(height: 8), ], @@ -50,14 +64,12 @@ class _SyncSection extends StatelessWidget { color: colorScheme.surfaceContainerHigh, child: Column( children: [ + // textgate: was "Sync settings" / "Firebase sync". ListTile( - title: Text( - 'Sync settings', - style: TextStyle(color: colorScheme.onSurface), - ), - subtitle: Text( - 'Firebase sync', - style: TextStyle(color: colorScheme.onSurfaceVariant), + leading: Icon( + Icons.cloud, + color: colorScheme.onSurface, + semanticLabel: 'Sync settings', ), trailing: Icon( Icons.chevron_right, @@ -69,14 +81,14 @@ class _SyncSection extends StatelessWidget { height: 1, color: colorScheme.onSurfaceVariant.withValues(alpha: 0.2), ), + // textgate: was "Advanced sync (GitHub)" / "Cutover mirror — + // not recommended": a code glyph, muted as the not-recommended + // route. ListTile( - title: Text( - 'Advanced sync (GitHub)', - style: TextStyle(color: colorScheme.onSurface), - ), - subtitle: Text( - 'Cutover mirror — not recommended', - style: TextStyle(color: colorScheme.onSurfaceVariant), + leading: Icon( + Icons.code, + color: colorScheme.onSurfaceVariant, + semanticLabel: 'Advanced sync (GitHub)', ), trailing: Icon( Icons.chevron_right, @@ -107,26 +119,11 @@ class _OfflineBackupSection extends StatelessWidget { @override Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const _SectionHeader('OFFLINE BACKUP'), - const SizedBox(height: 4), - Text( - storageGranted - ? 'Granted. Progression is also written to ' - '$kBackupPath, so it survives a reinstall even ' - 'with no network.' - : 'Optional. Progression is restored from Firebase on ' - 'a fresh install, so this is a second, offline ' - 'copy — not a requirement. Granting it also keeps ' - 'a readable snapshot at $kBackupPath.', - style: TextStyle( - color: colorScheme.onSurfaceVariant, - fontSize: AppTextSize.caption, - ), - ), + // textgate: the explanation paragraph under the heading is gone. + const _SectionHeader(Icons.sd_storage, 'OFFLINE BACKUP'), const SizedBox(height: 12), if (storageGranted) // Was "Storage permission granted": storage glyph + green tick. @@ -188,12 +185,10 @@ class _SettingsAppBar extends StatelessWidget implements PreferredSizeWidget { iconTheme: IconThemeData(color: colorScheme.onSurface), actions: [ const RunnerUpImportBadge(), - TextButton( + IconButton( onPressed: loading ? null : onReset, - child: Text( - 'Reset defaults', - style: TextStyle(color: colorScheme.error), - ), + color: colorScheme.error, + icon: const Icon(Icons.restart_alt, semanticLabel: 'Reset defaults'), ), ], ); diff --git a/stronglift_replacement/workout_app/test/screens/settings_screen_exercises_test.dart b/stronglift_replacement/workout_app/test/screens/settings_screen_exercises_test.dart index f9b8eb4..1a33484 100644 --- a/stronglift_replacement/workout_app/test/screens/settings_screen_exercises_test.dart +++ b/stronglift_replacement/workout_app/test/screens/settings_screen_exercises_test.dart @@ -15,6 +15,28 @@ import '../icon_finders.dart'; const _situp = 'Situp exercise settings'; +/// A summary row read back as words: each glyph as the setting it stands +/// for, each number as painted. textgate: the row paints no words itself. +List _summaries(WidgetTester tester) { + final glyphs = { + Icons.fitness_center: 'kg', + Icons.repeat: 'reps', + Icons.timer: 'rest', + Icons.wb_sunny: 'warmup', + }; + return [ + for (final row + in find.byKey(const Key('exercise_settings_summary')).evaluate()) + [ + for (final w in (row.widget as Row).children) + if (w is Icon) + glyphs[w.icon] ?? '?' + else if (w is Text) + w.data!.trim(), + ].where((p) => p.isNotEmpty).join(' '), + ]; +} + void main() { late int pushes; late ProgressionSyncResult pushResult; @@ -80,10 +102,7 @@ void main() { testWidgets('each row summarises its exercise', (tester) async { await pumpSettings(tester); // Situp is the one plan exercise without a warmup. - expect( - find.text('kg · ↑3 ↓2 · rest 3:00 / 5:00 · no warmup'), - findsOneWidget, - ); + expect(_summaries(tester), contains('kg ↑3 ↓2 rest 3:00 / 5:00')); }); testWidgets('a double-progression row names its rep range', (tester) async { @@ -98,7 +117,10 @@ void main() { ); }); await pumpSettings(tester); - expect(find.textContaining('25→40 reps · ↑3 ↓2'), findsOneWidget); + expect( + _summaries(tester).where((r) => r.startsWith('25→40 ↑3 ↓2')), + hasLength(1), + ); }); testWidgets('edits persist, show on the row, and push once on close', ( @@ -122,8 +144,8 @@ void main() { expect(s.restSuccessSecs, 195); expect(s.mode, ProgressionMode.reps); expect(s.pausedUntil, isNotNull); - expect(find.textContaining('reps · ↑3 ↓2 · rest 3:15 / 5:00'), findsOne); - expect(find.textContaining('Paused — back on'), findsOneWidget); + expect(_summaries(tester), contains('reps ↑3 ↓2 rest 3:15 / 5:00')); + expect(find.byIcon(Icons.pause_circle_outline), findsOneWidget); expect(find.byType(SnackBar), findsNothing); }); diff --git a/stronglift_replacement/workout_app/test/screens/settings_screen_pc_link_test.dart b/stronglift_replacement/workout_app/test/screens/settings_screen_pc_link_test.dart index 169dc3f..8386679 100644 --- a/stronglift_replacement/workout_app/test/screens/settings_screen_pc_link_test.dart +++ b/stronglift_replacement/workout_app/test/screens/settings_screen_pc_link_test.dart @@ -10,6 +10,7 @@ import 'package:workout_app/services/storage_service.dart'; import 'package:workout_app/ui/theme.dart'; import '../fake_secure_storage.dart'; +import '../icon_finders.dart'; void main() { setUpAll(() { @@ -62,14 +63,11 @@ void main() { } String pairedLine(WidgetTester tester) => - tester.widget(find.byKey(const Key('pc-paired'))).data!; + tester.widget(find.byKey(const Key('pc-paired'))).semanticLabel!; testWidgets('a paired phone shows the host and a check', (tester) async { - await pump( - tester, - PcPairingState(host: '192.168.1.43', keyHex: 'c3' * 32), - ); - expect(find.text('PC LINK'), findsOneWidget); + await pump(tester, PcPairingState(host: '192.168.1.43', keyHex: 'c3' * 32)); + expect(iconLabeled('PC LINK'), findsOneWidget); expect(pairedLine(tester), 'PC paired ✓'); final field = tester.widget(find.byKey(const Key('pc-host'))); expect(field.controller!.text, '192.168.1.43'); @@ -84,7 +82,7 @@ void main() { await pump(tester, const PcPairingState(host: '10.0.0.2')); await save(tester, 'not a host'); expect( - find.text('Not an IPv4 address or hostname: "not a host".'), + iconLabeled('Not an IPv4 address or hostname: "not a host".'), findsOneWidget, ); final saved = await tester.runAsync(StorageService.instance.getPcHost); @@ -98,7 +96,7 @@ void main() { await pump(tester, const PcPairingState(host: '10.0.0.2')); await save(tester, ' 192.168.1.77 '); expect( - find.text('Workouts are now sent to 192.168.1.77:$kPcPokePort.'), + iconLabeled('Workouts are now sent to 192.168.1.77:$kPcPokePort.'), findsOneWidget, ); final saved = await tester.runAsync(StorageService.instance.getPcHost); diff --git a/stronglift_replacement/workout_app/test/screens/settings_screen_reset_test.dart b/stronglift_replacement/workout_app/test/screens/settings_screen_reset_test.dart index 558c7f4..9711a4b 100644 --- a/stronglift_replacement/workout_app/test/screens/settings_screen_reset_test.dart +++ b/stronglift_replacement/workout_app/test/screens/settings_screen_reset_test.dart @@ -12,6 +12,7 @@ import 'package:workout_app/ui/theme.dart'; import '../fake_pc_pairing.dart'; import '../fake_secure_storage.dart'; +import '../icon_finders.dart'; void main() { setUpAll(() { @@ -86,17 +87,17 @@ void main() { (tester) async { await _pump(tester, _wrap()); await tester.scrollUntilVisible( - find.text('Sync settings'), + iconLabeled('Sync settings'), 500, scrollable: find.byType(Scrollable).first, ); - await tester.tap(find.text('Sync settings')); + await tester.tap(iconLabeled('Sync settings')); await tester.pumpAndSettle(); await tester.pageBack(); await tester.pumpAndSettle(); - expect(find.textContaining('Restored'), findsNothing); + expect(iconLabeled('Restored from Firebase'), findsNothing); }, ); @@ -104,14 +105,12 @@ void main() { tester, ) async { await _pump(tester, _wrap()); - await tester.scrollUntilVisible(find.text('TARGET REPS'), 200); + await tester.scrollUntilVisible(iconLabeled('TARGET REPS'), 200); await tester.pumpAndSettle(); // Situp defaults to 30 reps and is the only exercise at that value. final minus = find.descendant( - of: find - .ancestor(of: find.text('30 reps'), matching: find.byType(Row)) - .first, + of: find.ancestor(of: find.text('×30'), matching: find.byType(Row)).first, matching: find.byIcon(Icons.remove), ); // Same 600ms debounce as weights: run on the real loop so the timer fires. diff --git a/stronglift_replacement/workout_app/test/screens/settings_screen_sandbox_test.dart b/stronglift_replacement/workout_app/test/screens/settings_screen_sandbox_test.dart index b06a285..378d50e 100644 --- a/stronglift_replacement/workout_app/test/screens/settings_screen_sandbox_test.dart +++ b/stronglift_replacement/workout_app/test/screens/settings_screen_sandbox_test.dart @@ -54,7 +54,7 @@ void main() { // The list, explicitly: the section's own TextField is a Scrollable too, // and scrolling that one never brings the header into view. await tester.scrollUntilVisible( - find.text('SANDBOX'), + iconLabeled('SANDBOX'), 300, scrollable: find.byType(Scrollable).first, ); @@ -79,7 +79,7 @@ void main() { await Future.delayed(const Duration(milliseconds: 300)); }); await tester.pump(); - expect(find.text('SANDBOX'), findsNothing); + expect(iconLabeled('SANDBOX'), findsNothing); }); testWidgets('wipe resets the database and the rest override', (tester) async { @@ -92,7 +92,7 @@ void main() { Sandbox.restSecs = 30; await pump(tester); await tapReal(tester, iconLabeled('Wipe sandbox data')); - expect(find.textContaining('Wiped —'), findsOneWidget); + expect(iconLabelContaining('Wiped —'), findsOneWidget); expect(Sandbox.restSecs, Sandbox.defaultRestSecs); final active = await tester.runAsync( StorageService.instance.loadActiveSession, @@ -103,7 +103,7 @@ void main() { testWidgets('mark today not done reports what it removed', (tester) async { await pump(tester); await tapReal(tester, iconLabeled('Mark today not done')); - expect(find.textContaining('Nothing logged today'), findsOneWidget); + expect(iconLabelContaining('Nothing logged today'), findsOneWidget); final today = DateTime.now().toIso8601String().substring(0, 10); await tester.runAsync( @@ -116,7 +116,7 @@ void main() { ), ); await tapReal(tester, iconLabeled('Mark today not done')); - expect(find.textContaining('Removed 1 workout'), findsOneWidget); + expect(iconLabelContaining('Removed 1 workout'), findsOneWidget); }); testWidgets('rest length: rejects junk, persists a whole number', ( @@ -131,16 +131,16 @@ void main() { ); await tester.enterText(field, 'abc'); await tapReal(tester, save); - expect(find.textContaining('whole number of seconds'), findsOneWidget); + expect(iconLabelContaining('whole number of seconds'), findsOneWidget); expect(Sandbox.restSecs, Sandbox.defaultRestSecs); await tester.enterText(field, '0'); await tapReal(tester, save); - expect(find.textContaining('whole number of seconds'), findsOneWidget); + expect(iconLabelContaining('whole number of seconds'), findsOneWidget); await tester.enterText(field, '45'); await tapReal(tester, save); - expect(find.text('Every rest now lasts 45 s.'), findsOneWidget); + expect(iconLabeled('Every rest now lasts 45 s.'), findsOneWidget); expect(Sandbox.restSecs, 45); final saved = await tester.runAsync( () => StorageService.instance.getSandboxRestSecs(5), @@ -157,7 +157,7 @@ void main() { matching: find.text('10×'), ); await tapReal(tester, tenX); - expect(find.text('GPS runs replay at 10×.'), findsOneWidget); + expect(iconLabeled('GPS runs replay at 10×.'), findsOneWidget); expect(Sandbox.runReplayFactor, 10); final saved = await tester.runAsync( () => StorageService.instance.getSandboxReplayFactor(60), diff --git a/stronglift_replacement/workout_app/test/screens/settings_screen_sync_test.dart b/stronglift_replacement/workout_app/test/screens/settings_screen_sync_test.dart index 5bc8388..35f1614 100644 --- a/stronglift_replacement/workout_app/test/screens/settings_screen_sync_test.dart +++ b/stronglift_replacement/workout_app/test/screens/settings_screen_sync_test.dart @@ -84,7 +84,7 @@ void main() { testWidgets('Reset dialog shows on tap and cancels', (tester) async { await _pump(tester, _wrap()); - await tester.tap(find.text('Reset defaults')); + await tester.tap(iconLabeled('Reset defaults')); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); expect(iconLabeled('Reset to defaults?'), findsOneWidget); @@ -101,7 +101,7 @@ void main() { ); await _pump(tester, _wrap()); - await tester.tap(find.text('Reset defaults')); + await tester.tap(iconLabeled('Reset defaults')); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); await tester.tap(iconLabeled('Reset')); @@ -135,11 +135,6 @@ void main() { 500, scrollable: find.byType(Scrollable).first, ); - // Framed as optional: Firebase restores progression without it. - expect( - find.textContaining('Optional.', skipOffstage: false), - findsOneWidget, - ); // The scroll step can leave the row just below the fold. await tester.ensureVisible(iconLabeled('Grant storage permission')); @@ -164,9 +159,5 @@ void main() { expect(iconLabeled('Storage permission granted'), findsOneWidget); expect(iconLabeled('Grant storage permission'), findsNothing); - expect( - find.textContaining('Granted.', skipOffstage: false), - findsOneWidget, - ); }); } diff --git a/stronglift_replacement/workout_app/test/screens/settings_screen_test.dart b/stronglift_replacement/workout_app/test/screens/settings_screen_test.dart index 0c706bc..62b509a 100644 --- a/stronglift_replacement/workout_app/test/screens/settings_screen_test.dart +++ b/stronglift_replacement/workout_app/test/screens/settings_screen_test.dart @@ -89,33 +89,31 @@ void main() { testWidgets('shows WEIGHTS, TARGET REPS and PER EXERCISE', (tester) async { await _pump(tester, _wrap()); - expect(find.text('WEIGHTS'), findsOneWidget); + expect(iconLabeled('WEIGHTS'), findsOneWidget); // TARGET REPS and the per-exercise list below it are off-screen at the test // viewport height, so scroll them into view rather than asserting on // whatever happens to be built. - await tester.scrollUntilVisible(find.text('TARGET REPS'), 200); - expect(find.text('TARGET REPS'), findsOneWidget); - await tester.scrollUntilVisible(find.text('PER EXERCISE'), 200); - expect(find.text('PER EXERCISE'), findsOneWidget); + await tester.scrollUntilVisible(iconLabeled('TARGET REPS'), 200); + expect(iconLabeled('TARGET REPS'), findsOneWidget); + await tester.scrollUntilVisible(iconLabeled('PER EXERCISE'), 200); + expect(iconLabeled('PER EXERCISE'), findsOneWidget); }); testWidgets('increment reps button increases the target reps', ( tester, ) async { await _pump(tester, _wrap()); - await tester.scrollUntilVisible(find.text('TARGET REPS'), 200); + await tester.scrollUntilVisible(iconLabeled('TARGET REPS'), 200); await tester.pumpAndSettle(); // Situp defaults to 30 reps and is the only exercise at that value. - expect(find.text('30 reps'), findsOneWidget); + expect(find.text('×30'), findsOneWidget); final plus = find.descendant( - of: find - .ancestor(of: find.text('30 reps'), matching: find.byType(Row)) - .first, + of: find.ancestor(of: find.text('×30'), matching: find.byType(Row)).first, matching: find.byIcon(Icons.add), ); await tester.tap(plus); await tester.pumpAndSettle(); - expect(find.text('31 reps'), findsOneWidget); + expect(find.text('×31'), findsOneWidget); }); testWidgets('shows all exercise names from both workout plans', ( @@ -129,7 +127,7 @@ void main() { testWidgets('Reset defaults button is present', (tester) async { await _pump(tester, _wrap()); - expect(find.text('Reset defaults'), findsOneWidget); + expect(iconLabeled('Reset defaults'), findsOneWidget); }); testWidgets('increment weight button increases weight', (tester) async { diff --git a/stronglift_replacement/workout_app/test/screens/settings_screen_thresholds_test.dart b/stronglift_replacement/workout_app/test/screens/settings_screen_thresholds_test.dart index dc162e0..0e3480c 100644 --- a/stronglift_replacement/workout_app/test/screens/settings_screen_thresholds_test.dart +++ b/stronglift_replacement/workout_app/test/screens/settings_screen_thresholds_test.dart @@ -87,22 +87,22 @@ void main() { ) async { await _pump(tester, _wrap()); await tester.scrollUntilVisible( - find.text('Sync settings'), + iconLabeled('Sync settings'), 500, scrollable: find.byType(Scrollable).first, ); - expect(find.text('Sync settings'), findsOneWidget); - expect(find.text('Advanced sync (GitHub)'), findsOneWidget); + expect(iconLabeled('Sync settings'), findsOneWidget); + expect(iconLabeled('Advanced sync (GitHub)'), findsOneWidget); }); testWidgets('opening Sync settings pushes the shared screen', (tester) async { await _pump(tester, _wrap()); await tester.scrollUntilVisible( - find.text('Sync settings'), + iconLabeled('Sync settings'), 500, scrollable: find.byType(Scrollable).first, ); - await tester.tap(find.text('Sync settings')); + await tester.tap(iconLabeled('Sync settings')); await tester.pumpAndSettle(); // The shared package's own screen -- its AppBar title, not this app's. @@ -115,13 +115,13 @@ void main() { ) async { await _pump(tester, _wrap()); await tester.scrollUntilVisible( - find.text('Advanced sync (GitHub)'), + iconLabeled('Advanced sync (GitHub)'), 500, scrollable: find.byType(Scrollable).first, ); - await tester.ensureVisible(find.text('Advanced sync (GitHub)')); + await tester.ensureVisible(iconLabeled('Advanced sync (GitHub)')); await tester.pumpAndSettle(); - await tester.tap(find.text('Advanced sync (GitHub)')); + await tester.tap(iconLabeled('Advanced sync (GitHub)')); await tester.pumpAndSettle(); expect(iconLabeled('Connect GitHub'), findsOneWidget); @@ -151,21 +151,19 @@ void main() { ), ); await tester.scrollUntilVisible( - find.text('Sync settings'), + iconLabeled('Sync settings'), 500, scrollable: find.byType(Scrollable).first, ); - await tester.tap(find.text('Sync settings')); + await tester.tap(iconLabeled('Sync settings')); await tester.pumpAndSettle(); await tester.pageBack(); await tester.pumpAndSettle(); expect(pulled, isTrue); - expect( - find.text('Restored 7 exercise(s) from Firebase.'), - findsOneWidget, - ); + expect(iconLabeled('Restored from Firebase'), findsOneWidget); + expect(find.text('7'), findsOneWidget); }, ); } diff --git a/stronglift_replacement/workout_app/test/screens/settings_screen_transfer_test.dart b/stronglift_replacement/workout_app/test/screens/settings_screen_transfer_test.dart index 3a02c86..6680fe8 100644 --- a/stronglift_replacement/workout_app/test/screens/settings_screen_transfer_test.dart +++ b/stronglift_replacement/workout_app/test/screens/settings_screen_transfer_test.dart @@ -44,7 +44,12 @@ void main() { }); await tester.pump(); final buttons = find.byType(DataTransferButtons); - await tester.scrollUntilVisible(buttons, 300); + await tester.scrollUntilVisible( + buttons, + 300, + // The PC address field is a Scrollable too, now on screen. + scrollable: find.byType(Scrollable).first, + ); expect(find.bySemanticsLabel('Export data'), findsOneWidget); expect(find.bySemanticsLabel('Import data'), findsOneWidget);