2026-10-29 00:20:16 +01:00
|
|
|
|
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;
|
2026-10-29 00:34:49 +01:00
|
|
|
|
late FakeNotifier notifier;
|
2026-10-29 00:20:16 +01:00
|
|
|
|
|
|
|
|
|
|
setUp(() {
|
|
|
|
|
|
final db = testDatabase();
|
|
|
|
|
|
addTearDown(db.close);
|
|
|
|
|
|
plans = PlanRepository(db);
|
|
|
|
|
|
sessions = SessionRepository(db);
|
|
|
|
|
|
effects = FakePlayerEffects();
|
2026-10-29 00:34:49 +01:00
|
|
|
|
notifier = FakeNotifier();
|
2026-10-29 00:20:16 +01:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
2026-10-29 00:34:49 +01:00
|
|
|
|
await pumpApp(
|
|
|
|
|
|
tester,
|
|
|
|
|
|
plans: plans,
|
|
|
|
|
|
sessions: sessions,
|
|
|
|
|
|
effects: effects,
|
|
|
|
|
|
notifier: notifier,
|
|
|
|
|
|
prefs: {'mode': mode.name},
|
|
|
|
|
|
);
|
2026-10-29 00:20:16 +01:00
|
|
|
|
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);
|
2026-10-29 00:34:49 +01:00
|
|
|
|
await tester.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 50)));
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
expect(find.text('Day 1 of your streak'), findsOneWidget);
|
|
|
|
|
|
// The first finished Session is when Reminders ask for permission.
|
|
|
|
|
|
expect(notifier.permissionRequests, 1);
|
2026-10-29 00:20:16 +01:00
|
|
|
|
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);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|