Files
ZenXercise/test/views/player_view_test.dart
T
CedricandClaude Opus 5.5 971ff64117 Stage 4: guided Player for both Modes and the Session summary
- buildSegments lays a Plan out as holds with Transitions (get ready,
  switch sides, next round, next exercise) or sets with count-up Rests;
  Supersets alternate members and rest per round unless asked otherwise
- SessionRunner drives segments against an injected clock: auto-advance
  with a chime, pause, skip, back, rep sets confirmed with logged reps
  and kg, timed sets started on demand, early end
- Player screen: countdown ring, accent pulse in the last 3 s, Flip
  Animation, next-up cards, Rest Screen, mute toggle, screen kept on
- Sessions table (schema v2 migration) keeps a Plan snapshot and Set
  Logs; nothing is saved when no hold or set was done
- Summary with time, exercises or sets logged, and every Set Log
- Synthesised quiet cue sounds (tools/sounds), status bar per Mode

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 00:20:16 +01:00

149 lines
5.3 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter_test/flutter_test.dart';
import 'package:zenxercise/src/history/session_repository.dart';
import 'package:zenxercise/src/modes/mode.dart';
import 'package:zenxercise/src/plans/models/plan.dart';
import 'package:zenxercise/src/plans/plan_repository.dart';
import 'package:zenxercise/src/player/views/player_view.dart';
import '../support/test_app.dart';
import '../support/test_database.dart';
void main() {
late PlanRepository plans;
late SessionRepository sessions;
late FakePlayerEffects effects;
setUp(() {
final db = testDatabase();
addTearDown(db.close);
plans = PlanRepository(db);
sessions = SessionRepository(db);
effects = FakePlayerEffects();
});
Future<void> startPlan(WidgetTester tester, Mode mode, List<PlanItem> items) async {
final plan = await plans.create(mode, 'Routine');
await plans.save(plan.copyWith(items: items));
usePhoneScreen(tester);
await pumpApp(tester, plans: plans, sessions: sessions, effects: effects, prefs: {'mode': mode.name});
await tester.tap(find.byTooltip('Start'));
await tester.pump();
await tester.pump();
}
/// Drift's streams need real time to deliver, outside the test's fake clock.
Future<List<Session>> sessionsOf(WidgetTester tester, Mode mode) async =>
(await tester.runAsync(() => sessions.watchSessions(mode).first))!;
/// The Player's ticker never lets frames settle, so it is pumped for a fixed time.
Future<void> settle(WidgetTester tester) async {
for (var i = 0; i < 8; i++) {
await tester.pump(const Duration(milliseconds: 100));
}
}
Future<void> wait(WidgetTester tester, int seconds) async {
for (var i = 0; i < seconds * 5; i++) {
await tester.pump(const Duration(milliseconds: 200));
}
}
testWidgets('a Stretching Session counts down, chimes, keeps the screen on and ends on its summary', (tester) async {
await startPlan(tester, Mode.stretching, const [
StretchingStep(exerciseId: 'Calf_Stretch', holdSeconds: 10, bothSides: false, rounds: 1),
]);
expect(find.text('Get ready'), findsOneWidget);
expect(effects.screenOn, isTrue);
// After the get-ready and the 0.7 s crossfade into the hold.
await wait(tester, 5);
await settle(tester);
expect(find.text('Calf Stretch'), findsOneWidget);
expect(find.text('Get ready'), findsNothing);
await wait(tester, 10);
await settle(tester);
expect(find.text('Session complete'), findsOneWidget);
expect(effects.chimes, [Mode.stretching, Mode.stretching]);
expect(effects.screenOn, isFalse);
expect((await sessionsOf(tester, Mode.stretching)).single.complete, isTrue);
});
testWidgets('a Workout set is confirmed with its logged reps, then rested and repeated', (tester) async {
await startPlan(tester, Mode.workout, const [
WorkoutStep(exerciseId: 'Barbell_Squat', sets: 2, target: WorkoutTarget.reps(10), weightKg: 60),
]);
expect(find.text('Set 1 of 2'), findsOneWidget);
expect(find.text('10 reps · 60 kg'), findsOneWidget);
await tester.tap(find.text('Set done'));
await settle(tester);
await tester.tap(find.byTooltip('Reps −'));
await tester.pump();
await tester.tap(find.text('Save set'));
await settle(tester);
expect(find.text('Rest'), findsOneWidget);
await wait(tester, 3);
expect(find.text('0:03'), findsOneWidget);
await tester.tap(find.text('Start next set'));
await settle(tester);
await tester.tap(find.text('Set done'));
await settle(tester);
await tester.tap(find.text('Save set'));
await settle(tester);
expect(find.text('Session complete'), findsOneWidget);
expect(find.text('Set 1 · 9 reps · 60 kg'), findsOneWidget);
expect(find.text('Set 2 · 10 reps · 60 kg'), findsOneWidget);
});
testWidgets('ending early after a set saves an unfinished Session', (tester) async {
await startPlan(tester, Mode.workout, const [
WorkoutStep(exerciseId: 'Barbell_Squat', sets: 3, target: WorkoutTarget.reps(10)),
]);
await tester.tap(find.text('Set done'));
await settle(tester);
await tester.tap(find.text('Save set'));
await settle(tester);
await tester.tap(find.byTooltip('End session'));
await settle(tester);
await tester.tap(find.text('End session').last);
await settle(tester);
expect(find.text('Ended early'), findsOneWidget);
expect((await sessionsOf(tester, Mode.workout)).single.complete, isFalse);
});
testWidgets('skipping through everything leaves no Session behind', (tester) async {
await startPlan(tester, Mode.workout, const [
WorkoutStep(exerciseId: 'Barbell_Squat', sets: 1, target: WorkoutTarget.reps(10)),
]);
await tester.tap(find.byTooltip('Skip'));
await settle(tester);
expect(find.byType(PlayerView), findsNothing);
expect(await sessionsOf(tester, Mode.workout), isEmpty);
});
testWidgets('ending before anything is done leaves no Session behind', (tester) async {
await startPlan(tester, Mode.workout, const [
WorkoutStep(exerciseId: 'Barbell_Squat', sets: 3, target: WorkoutTarget.reps(10)),
]);
await tester.tap(find.byTooltip('End session'));
await settle(tester);
await tester.tap(find.text('End session').last);
await settle(tester);
expect(find.byType(PlayerView), findsNothing);
expect(await sessionsOf(tester, Mode.workout), isEmpty);
});
}