mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-11 16:14:01 +01:00
Make history screen text-free
Section headings and the all-workouts entry are glyphs, tiles drop their word prefixes, chart and calendar dates are numeric, weekday headers are dots. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
661b293bc2
commit
5fa160f114
@ -173,12 +173,7 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
||||
_syncedRows.isEmpty &&
|
||||
_plannedRows.isEmpty &&
|
||||
_runs.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
'No workouts yet.',
|
||||
style: TextStyle(color: colorScheme.onSurfaceVariant),
|
||||
),
|
||||
)
|
||||
? const _EmptyHistory()
|
||||
: ListView(
|
||||
padding: const EdgeInsets.all(12),
|
||||
children: [
|
||||
@ -210,18 +205,21 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
||||
_RunsStrip(runs: _runs, onOpen: _openRun),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
const _SectionLabel('TOTAL VOLUME (2-session rolling avg, kg)'),
|
||||
const _SectionLabel(
|
||||
Icons.stacked_line_chart,
|
||||
'TOTAL VOLUME (2-session rolling avg, kg)',
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
_WeightChart(points: _rollingAvg2(_totalVolumePoints(_rows))),
|
||||
const SizedBox(height: 16),
|
||||
_calendar(_allWorkoutDates(_rows, _syncedRows, _runs)),
|
||||
const SizedBox(height: 16),
|
||||
const _SectionLabel('ALL SESSIONS'),
|
||||
const _SectionLabel(Icons.list, 'ALL SESSIONS'),
|
||||
const SizedBox(height: 8),
|
||||
..._rows.map((row) => _AllSessionTile(row: row)),
|
||||
if (_syncedRows.isNotEmpty) ...[
|
||||
const SizedBox(height: 16),
|
||||
const _SectionLabel('SYNCED FROM PC'),
|
||||
const _SectionLabel(Icons.computer, 'SYNCED FROM PC'),
|
||||
const SizedBox(height: 8),
|
||||
..._syncedRows.map((row) => _SyncedWorkoutTile(payload: row)),
|
||||
],
|
||||
@ -232,13 +230,14 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
||||
_ProgressStatsCard(state: _selectedState!),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
const _SectionLabel('WEIGHT OVER TIME'),
|
||||
const _SectionLabel(Icons.show_chart, 'WEIGHT OVER TIME'),
|
||||
const SizedBox(height: 6),
|
||||
_WeightChart(points: _exerciseWeightPoints(_rows, name)),
|
||||
const SizedBox(height: 16),
|
||||
_calendar(_exerciseDates(_rows, name)),
|
||||
const SizedBox(height: 16),
|
||||
_SectionLabel(name.toUpperCase()),
|
||||
// The picker above already names the exercise: a list glyph here.
|
||||
_SectionLabel(Icons.list, name),
|
||||
const SizedBox(height: 8),
|
||||
..._sessionsForExercise(
|
||||
_rows,
|
||||
|
||||
@ -7,24 +7,44 @@ part of 'history_screen.dart';
|
||||
|
||||
// ── Shared sub-widgets ──────────────────────────────────────────────────────
|
||||
|
||||
/// A section heading as a glyph (textgate: was an all-caps word heading;
|
||||
/// the words survive as [spoken], the glyph's semantic label).
|
||||
class _SectionLabel extends StatelessWidget {
|
||||
const _SectionLabel(this.text);
|
||||
const _SectionLabel(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: 18,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontSize: AppTextSize.caption,
|
||||
letterSpacing: 1.3,
|
||||
semanticLabel: spoken,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The empty history: a large muted history glyph (textgate: was "No
|
||||
/// workouts yet.").
|
||||
class _EmptyHistory extends StatelessWidget {
|
||||
const _EmptyHistory();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Center(
|
||||
child: Icon(
|
||||
Icons.history,
|
||||
size: 48,
|
||||
color: Theme.of(context).colorScheme.outlineVariant,
|
||||
semanticLabel: 'No workouts yet.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _ExercisePicker extends StatelessWidget {
|
||||
const _ExercisePicker({
|
||||
required this.names,
|
||||
@ -49,15 +69,18 @@ class _ExercisePicker extends StatelessWidget {
|
||||
.map(
|
||||
(n) => DropdownMenuItem(
|
||||
value: n,
|
||||
child: Text(
|
||||
n,
|
||||
style: TextStyle(
|
||||
color: n == _kTotal
|
||||
? colorScheme.onSurfaceVariant
|
||||
: colorScheme.onSurface,
|
||||
fontStyle: n == _kTotal ? FontStyle.italic : FontStyle.normal,
|
||||
),
|
||||
),
|
||||
// textgate: the all-workouts entry is a sum glyph (was
|
||||
// "Total (all workouts)"); exercise names are content.
|
||||
child: n == _kTotal
|
||||
? Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Icon(
|
||||
Icons.functions,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
semanticLabel: _kTotal,
|
||||
),
|
||||
)
|
||||
: Text(n, style: TextStyle(color: colorScheme.onSurface)),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
@ -162,7 +185,8 @@ class _StreakRow extends StatelessWidget {
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${threshold - shown} more to $direction $actionLabel',
|
||||
// textgate: was "2 more to ↑ 25 kg".
|
||||
'${threshold - shown} $direction $actionLabel',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
fontSize: AppTextSize.caption,
|
||||
@ -187,9 +211,11 @@ class _WeightChart extends StatelessWidget {
|
||||
return Container(
|
||||
height: 80,
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'Not enough data for chart',
|
||||
style: TextStyle(color: colorScheme.onSurfaceVariant),
|
||||
// textgate: was "Not enough data for chart".
|
||||
child: Icon(
|
||||
Icons.show_chart,
|
||||
color: colorScheme.outlineVariant,
|
||||
semanticLabel: 'Not enough data for chart',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -5,7 +5,6 @@
|
||||
// these are `part` files.
|
||||
part of 'history_screen.dart';
|
||||
|
||||
|
||||
class _ChartPainter extends CustomPainter {
|
||||
_ChartPainter(
|
||||
this.points, {
|
||||
@ -25,22 +24,8 @@ class _ChartPainter extends CustomPainter {
|
||||
static const _bottomPad = 22.0; // room for X-axis dates
|
||||
static const _hPad = 8.0;
|
||||
|
||||
static const _months = [
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'May',
|
||||
'Jun',
|
||||
'Jul',
|
||||
'Aug',
|
||||
'Sep',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec',
|
||||
];
|
||||
|
||||
static String _shortDate(DateTime d) => '${_months[d.month - 1]} ${d.day}';
|
||||
// textgate: numeric day.month on the axis (was "Aug 15").
|
||||
static String _shortDate(DateTime d) => formatShortDate(d);
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
|
||||
@ -17,7 +17,7 @@ extension _HistoryPlanned on _HistoryScreenState {
|
||||
/// when there are none.
|
||||
List<Widget> _plannedSection() => [
|
||||
if (_plannedRows.isNotEmpty) ...[
|
||||
const _SectionLabel('PLANNED (not done yet)'),
|
||||
const _SectionLabel(Icons.event, 'PLANNED (not done yet)'),
|
||||
const SizedBox(height: 8),
|
||||
..._plannedRows.map(
|
||||
(row) => _PlannedWorkoutTile(
|
||||
@ -153,7 +153,9 @@ class _PlannedWorkoutTile extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${payload['date']} $time · Planned',
|
||||
// textgate: was "<date> <time> · Planned"; the event
|
||||
// glyph and the section glyph already say planned.
|
||||
'${payload['date']} $time',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface,
|
||||
fontWeight: FontWeight.bold,
|
||||
|
||||
@ -13,10 +13,12 @@ class _SyncedWorkoutTile extends StatelessWidget {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final kind = '${payload['kind']}';
|
||||
final isRun = kind == 'runnerup_verified';
|
||||
final label = isRun ? 'Run' : 'Manual';
|
||||
final semanticLabel = isRun ? 'Run' : 'Manual';
|
||||
final detail = '${payload['source'] ?? ''}';
|
||||
|
||||
return Container(
|
||||
// Tests find a tile by date and kind; nothing paints the kind word.
|
||||
key: ValueKey('synced-${payload['date']}-$semanticLabel'),
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
@ -32,14 +34,16 @@ class _SyncedWorkoutTile extends StatelessWidget {
|
||||
isRun ? Icons.directions_run : Icons.edit_note,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
size: 20,
|
||||
semanticLabel: semanticLabel,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// textgate: was "<date> · Run|Manual"; the glyph says which.
|
||||
Text(
|
||||
'${payload['date']} · $label',
|
||||
'${payload['date']}',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface,
|
||||
fontWeight: FontWeight.bold,
|
||||
@ -104,8 +108,9 @@ class _AllSessionTile extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// textgate: was "Workout A · <date>".
|
||||
Text(
|
||||
'Workout $type · $date',
|
||||
'$type · $date',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface,
|
||||
fontWeight: FontWeight.bold,
|
||||
@ -191,17 +196,29 @@ class _ExerciseSessionTile extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'${weight ?? '?'}kg · $doneSets/$targetSets sets'
|
||||
' · ${warmupDone ? '⬤ warmup' : '○ no warmup'}',
|
||||
style: mutedStyle,
|
||||
// textgate: was "20kg · 3/3 sets · ⬤ warmup" / "reps: 5, 5"
|
||||
// / "workout: 30m 00s". Now numbers behind glyphs; the warmup
|
||||
// is a filled or hollow sun.
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'${weight ?? '?'}kg · $doneSets/$targetSets ',
|
||||
style: mutedStyle,
|
||||
),
|
||||
Icon(
|
||||
warmupDone ? Icons.wb_sunny : Icons.wb_sunny_outlined,
|
||||
size: 14,
|
||||
color: mutedStyle.color,
|
||||
semanticLabel: warmupDone ? 'warmup' : 'no warmup',
|
||||
),
|
||||
],
|
||||
),
|
||||
if (repsSummary.isNotEmpty) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text('reps: $repsSummary', style: mutedStyle),
|
||||
_GlyphLine(Icons.repeat, repsSummary, mutedStyle),
|
||||
],
|
||||
const SizedBox(height: 2),
|
||||
Text('workout: $dur', style: mutedStyle),
|
||||
_GlyphLine(Icons.timer, dur, mutedStyle),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -210,3 +227,22 @@ class _ExerciseSessionTile extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A small glyph followed by a number line (textgate: replaces a word
|
||||
/// prefix such as "reps:" or "workout:").
|
||||
class _GlyphLine extends StatelessWidget {
|
||||
const _GlyphLine(this.icon, this.text, this.style);
|
||||
|
||||
final IconData icon;
|
||||
final String text;
|
||||
final TextStyle style;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Row(
|
||||
children: [
|
||||
Icon(icon, size: 14, color: style.color),
|
||||
const SizedBox(width: 4),
|
||||
Text(text, style: style),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@ -27,21 +27,16 @@ class WorkoutCalendar extends StatelessWidget {
|
||||
/// Called when the user taps the next-month chevron.
|
||||
final VoidCallback onNextMonth;
|
||||
|
||||
static const _weekHeaders = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'];
|
||||
|
||||
static const _monthNames = [
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December',
|
||||
// textgate: weekday headers are dots (weekend dots hollow) and the
|
||||
// month header is `08.2026`; the old words are spoken only.
|
||||
static const _weekdaySemanticsLabels = [
|
||||
'Monday',
|
||||
'Tuesday',
|
||||
'Wednesday',
|
||||
'Thursday',
|
||||
'Friday',
|
||||
'Saturday',
|
||||
'Sunday',
|
||||
];
|
||||
|
||||
String _dateKey(int year, int m, int day) =>
|
||||
@ -81,7 +76,7 @@ class WorkoutCalendar extends StatelessWidget {
|
||||
onPressed: onPrevMonth,
|
||||
),
|
||||
Text(
|
||||
'${_monthNames[m - 1]} $year',
|
||||
'${m.toString().padLeft(2, '0')}.$year',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface,
|
||||
fontWeight: FontWeight.bold,
|
||||
@ -103,22 +98,29 @@ class WorkoutCalendar extends StatelessWidget {
|
||||
// Day-of-week headers
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: _weekHeaders
|
||||
.map(
|
||||
(h) => SizedBox(
|
||||
width: 30,
|
||||
child: Text(
|
||||
h,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
fontSize: AppTextSize.caption,
|
||||
fontWeight: FontWeight.bold,
|
||||
children: [
|
||||
for (var i = 0; i < 7; i++)
|
||||
SizedBox(
|
||||
width: 30,
|
||||
child: Semantics(
|
||||
label: _weekdaySemanticsLabels[i],
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
// Weekend: hollow ring; weekday: filled dot.
|
||||
color: i >= 5 ? null : colorScheme.onSurfaceVariant,
|
||||
border: Border.all(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
// Day grid
|
||||
|
||||
@ -138,8 +138,8 @@ void main() {
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
|
||||
// Exercise view rendered: chart + streak stats card near the top.
|
||||
expect(find.text('WEIGHT OVER TIME'), findsOneWidget);
|
||||
expect(find.textContaining('more to'), findsWidgets);
|
||||
expect(iconLabeled('WEIGHT OVER TIME'), findsOneWidget);
|
||||
expect(find.textContaining('↑'), findsWidgets);
|
||||
|
||||
// Exercise-view calendar month nav (covers its onPrev/onNext closures).
|
||||
await tester.tap(find.byIcon(Icons.chevron_right).first);
|
||||
@ -149,14 +149,17 @@ void main() {
|
||||
|
||||
// The session list + name label sit below the fold; scroll to build them.
|
||||
await tester.scrollUntilVisible(
|
||||
find.text(name.toUpperCase()),
|
||||
iconLabeled(name),
|
||||
300,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
expect(find.text(name.toUpperCase()), findsOneWidget);
|
||||
expect(iconLabeled(name), findsOneWidget);
|
||||
// Session tiles: warmup marker and reps summary both present.
|
||||
expect(find.textContaining('warmup'), findsWidgets);
|
||||
expect(find.textContaining('reps:'), findsWidgets);
|
||||
expect([
|
||||
...iconLabeled('warmup').evaluate(),
|
||||
...iconLabeled('no warmup').evaluate(),
|
||||
], isNotEmpty);
|
||||
expect(find.byIcon(Icons.repeat), findsWidgets);
|
||||
});
|
||||
|
||||
testWidgets('chart handles two points on the same date (tRange == 0)', (
|
||||
@ -181,7 +184,7 @@ void main() {
|
||||
await seedSession(tester, vol(40), date: '2024-06-01');
|
||||
await pumpHistory(tester, wrapHistory());
|
||||
expect(find.byType(HistoryScreen), findsOneWidget);
|
||||
expect(find.text('WEIGHT OVER TIME').evaluate(), isEmpty); // total view
|
||||
expect(iconLabeled('WEIGHT OVER TIME').evaluate(), isEmpty); // total view
|
||||
});
|
||||
|
||||
testWidgets('shows the PC-synced workouts the phone has no session for', (
|
||||
@ -210,12 +213,15 @@ void main() {
|
||||
await pumpHistory(tester, wrapHistory(httpClient: historySyncMock()));
|
||||
|
||||
await tester.scrollUntilVisible(
|
||||
find.text('SYNCED FROM PC'),
|
||||
iconLabeled('SYNCED FROM PC'),
|
||||
200,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
expect(find.text('SYNCED FROM PC'), findsOneWidget);
|
||||
expect(find.textContaining('2026-07-13 · Run'), findsOneWidget);
|
||||
expect(find.textContaining('2026-07-13 · Manual'), findsOneWidget);
|
||||
expect(iconLabeled('SYNCED FROM PC'), findsOneWidget);
|
||||
expect(find.byKey(const ValueKey('synced-2026-07-13-Run')), findsOneWidget);
|
||||
expect(
|
||||
find.byKey(const ValueKey('synced-2026-07-13-Manual')),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -139,14 +139,23 @@ void main() {
|
||||
await _open(tester, _github(_seed()).client);
|
||||
|
||||
// No local sessions, but synced and planned entries still show.
|
||||
expect(find.text('No workouts yet.'), findsNothing);
|
||||
expect(find.text('PLANNED (not done yet)'), findsOneWidget);
|
||||
expect(find.text('2026-10-17 10:00 · Planned'), findsOneWidget);
|
||||
expect(iconLabeled('No workouts yet.'), findsNothing);
|
||||
expect(iconLabeled('PLANNED (not done yet)'), findsOneWidget);
|
||||
expect(find.text('2026-10-17 10:00'), findsOneWidget);
|
||||
expect(iconLabeled('Delete planned workout'), findsOneWidget);
|
||||
// Today's entry already happened: done, and not deletable.
|
||||
expect(find.text('2026-10-16 · Manual'), findsOneWidget);
|
||||
expect(find.text('2026-10-14 · Manual'), findsOneWidget);
|
||||
expect(find.text('2026-10-18 · Manual'), findsOneWidget);
|
||||
expect(
|
||||
find.byKey(const ValueKey('synced-2026-10-16-Manual')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.byKey(const ValueKey('synced-2026-10-14-Manual')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.byKey(const ValueKey('synced-2026-10-18-Manual')),
|
||||
findsOneWidget,
|
||||
);
|
||||
final calendar = tester.widget<WorkoutCalendar>(
|
||||
find.byType(WorkoutCalendar),
|
||||
);
|
||||
@ -169,8 +178,8 @@ void main() {
|
||||
// The tile goes only once the reload has read the tombstone back.
|
||||
await _settle(tester, 4);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('PLANNED (not done yet)'), findsNothing);
|
||||
expect(find.text('2026-10-17 10:00 · Planned'), findsNothing);
|
||||
expect(iconLabeled('PLANNED (not done yet)'), findsNothing);
|
||||
expect(find.text('2026-10-17 10:00'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('KEEP leaves the plan alone', (tester) async {
|
||||
@ -184,7 +193,7 @@ void main() {
|
||||
github.files.containsKey('screen-locker-sync/devices/phone/log.json'),
|
||||
isFalse,
|
||||
);
|
||||
expect(find.text('2026-10-17 10:00 · Planned'), findsOneWidget);
|
||||
expect(find.text('2026-10-17 10:00'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a failed delete keeps the tile and says why', (tester) async {
|
||||
@ -195,7 +204,7 @@ void main() {
|
||||
|
||||
expect(iconLabeled('Not deleted'), findsOneWidget);
|
||||
expect(find.byIcon(Icons.cloud_off), findsOneWidget);
|
||||
expect(find.text('2026-10-17 10:00 · Planned'), findsOneWidget);
|
||||
expect(find.text('2026-10-17 10:00'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a delete confirmed once the day has started is refused', (
|
||||
@ -220,6 +229,9 @@ void main() {
|
||||
);
|
||||
// Reloaded against the new day: it is done now, not planned.
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('2026-10-17 · Manual'), findsOneWidget);
|
||||
expect(
|
||||
find.byKey(const ValueKey('synced-2026-10-17-Manual')),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -52,7 +52,7 @@ Future<void> _pump(WidgetTester tester, {bool withPc = false}) async {
|
||||
}
|
||||
|
||||
Future<void> _scrollToSynced(WidgetTester tester) => tester.scrollUntilVisible(
|
||||
find.text('SYNCED FROM PC'),
|
||||
iconLabeled('SYNCED FROM PC'),
|
||||
200,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
@ -75,7 +75,7 @@ void main() {
|
||||
await _seedRun(tester, DateTime(2026, 7, 13, 12));
|
||||
await _seedRun(tester, DateTime(2026, 7, 14, 12), sport: RunSport.walking);
|
||||
await _pump(tester);
|
||||
expect(find.text('No workouts yet.'), findsNothing);
|
||||
expect(iconLabeled('No workouts yet.'), findsNothing);
|
||||
expect(find.byKey(const Key('runs_strip')), findsOneWidget);
|
||||
expect(find.text('5.20'), findsNWidgets(2));
|
||||
expect(find.text('32:10'), findsNWidgets(2));
|
||||
@ -95,8 +95,11 @@ void main() {
|
||||
await _pump(tester, withPc: true);
|
||||
await _scrollToSynced(tester);
|
||||
// The PC's manual workout of that day stays; its run entry is gone.
|
||||
expect(find.textContaining('2026-07-13 · Manual'), findsOneWidget);
|
||||
expect(find.textContaining('2026-07-13 · Run'), findsNothing);
|
||||
expect(
|
||||
find.byKey(const ValueKey('synced-2026-07-13-Manual')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.byKey(const ValueKey('synced-2026-07-13-Run')), findsNothing);
|
||||
expect(find.text('5.20'), findsOneWidget);
|
||||
});
|
||||
|
||||
@ -106,7 +109,7 @@ void main() {
|
||||
await _seedRun(tester, DateTime(2026, 7, 20, 12));
|
||||
await _pump(tester, withPc: true);
|
||||
await _scrollToSynced(tester);
|
||||
expect(find.textContaining('2026-07-13 · Run'), findsOneWidget);
|
||||
expect(find.byKey(const ValueKey('synced-2026-07-13-Run')), findsOneWidget);
|
||||
expect(find.text('5.20'), findsOneWidget);
|
||||
});
|
||||
|
||||
|
||||
@ -37,7 +37,7 @@ void main() {
|
||||
|
||||
testWidgets('shows "No workouts yet." when history is empty', (tester) async {
|
||||
await pumpHistory(tester, wrapHistory());
|
||||
expect(find.text('No workouts yet.'), findsOneWidget);
|
||||
expect(iconLabeled('No workouts yet.'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows session rows when history has data', (tester) async {
|
||||
@ -58,7 +58,7 @@ void main() {
|
||||
});
|
||||
await seedSession(tester, json);
|
||||
await pumpHistory(tester, wrapHistory());
|
||||
expect(find.textContaining('Workout A'), findsWidgets);
|
||||
expect(find.text('A · 2024-06-01'), findsWidgets);
|
||||
});
|
||||
|
||||
testWidgets('exercise picker shows "Total (all workouts)" initially', (
|
||||
@ -79,7 +79,7 @@ void main() {
|
||||
});
|
||||
await seedSession(tester, json);
|
||||
await pumpHistory(tester, wrapHistory());
|
||||
expect(find.textContaining('Total'), findsOneWidget);
|
||||
expect(iconLabeled('Total (all workouts)'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('calendar prev/next month navigation works', (tester) async {
|
||||
|
||||
@ -60,20 +60,20 @@ void main() {
|
||||
),
|
||||
);
|
||||
final threshold = seeded.successThreshold;
|
||||
final upRow = find.textContaining(RegExp(r'more to ↑'));
|
||||
final upRow = find.textContaining(RegExp(r'^\d+ ↑'));
|
||||
|
||||
await openExercise(tester, name);
|
||||
await tester.pump();
|
||||
expect(
|
||||
tester.widget<Text>(upRow).data,
|
||||
startsWith('$threshold more'),
|
||||
startsWith('$threshold ↑'),
|
||||
reason: 'first frame: nothing counted yet, every dot still to fill',
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
tester.widget<Text>(upRow).data,
|
||||
startsWith('${threshold - 2} more'),
|
||||
startsWith('${threshold - 2} ↑'),
|
||||
reason: 'settled: exactly the stored streak, never an invented one',
|
||||
);
|
||||
});
|
||||
@ -88,8 +88,8 @@ void main() {
|
||||
await openExercise(tester, name);
|
||||
await tester.pump();
|
||||
expect(
|
||||
tester.widget<Text>(find.textContaining(RegExp(r'more to ↑'))).data,
|
||||
startsWith('${seeded.successThreshold} more'),
|
||||
tester.widget<Text>(find.textContaining(RegExp(r'^\d+ ↑'))).data,
|
||||
startsWith('${seeded.successThreshold} ↑'),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -23,7 +23,7 @@ void main() {
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(find.text('June 2024'), findsOneWidget);
|
||||
expect(find.text('06.2024'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows day-of-week headers', (tester) async {
|
||||
@ -37,8 +37,8 @@ void main() {
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(find.text('Mo'), findsOneWidget);
|
||||
expect(find.text('Su'), findsOneWidget);
|
||||
expect(find.bySemanticsLabel('Monday'), findsOneWidget);
|
||||
expect(find.bySemanticsLabel('Sunday'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('calls onPrevMonth when left arrow tapped', (tester) async {
|
||||
@ -101,7 +101,7 @@ void main() {
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(find.text('September 2024'), findsOneWidget);
|
||||
expect(find.text('09.2024'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('renders January (first month name) correctly', (tester) async {
|
||||
@ -115,7 +115,7 @@ void main() {
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(find.text('January 2024'), findsOneWidget);
|
||||
expect(find.text('01.2024'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('renders December (last month name) correctly', (tester) async {
|
||||
@ -129,7 +129,7 @@ void main() {
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(find.text('December 2024'), findsOneWidget);
|
||||
expect(find.text('12.2024'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user