Files
ZenXercise/test/views/player_view_test.dart
CedricandClaude Opus 5.5 1ffe6269ea Stage 5: History, the shared daily Streak and Reminders
- Streak computed from all complete Sessions: Grace Hour (00:00-01:00
  counts for yesterday unless it is done), a Freeze per 7 Streak Days
  (max 2) spent automatically on a missed day (ADR 0006)
- Top bar flame (grey until today is done) opening a panel with current
  and longest Streak, Freezes and a month calendar; 'Day N of your
  streak' on a fresh summary
- History per Mode grouped by day, reopening each summary
- Two adjustable Reminders (19:00, 22:30) scheduled 14 days ahead with
  flutter_local_notifications, none on a done day; permission asked
  after the first complete Session or from Settings
- Settings: Reminders, the Player chime, exercise credits

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

163 lines
5.7 KiB
Dart
Raw Permalink 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;
late FakeNotifier notifier;
setUp(() {
final db = testDatabase();
addTearDown(db.close);
plans = PlanRepository(db);
sessions = SessionRepository(db);
effects = FakePlayerEffects();
notifier = FakeNotifier();
});
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,
notifier: notifier,
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);
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);
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);
});
}