Files
ZenXercise/test/support/test_app.dart
T
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

133 lines
4.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/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:zenxercise/src/app.dart';
import 'package:zenxercise/src/exercises/exercise.dart';
import 'package:zenxercise/src/exercises/exercise_library.dart';
import 'package:zenxercise/src/modes/mode.dart';
import 'package:zenxercise/src/history/session_repository.dart';
import 'package:zenxercise/src/plans/plan_repository.dart';
import 'package:zenxercise/src/player/player_effects.dart';
import 'package:zenxercise/src/settings/settings_controller.dart';
import 'package:zenxercise/src/streak/reminder_scheduler.dart';
import 'test_database.dart';
Exercise exercise(
String id,
Mode mode, {
String? name,
List<String> primary = const ['hamstrings'],
List<String> secondary = const [],
String? equipment = 'body only',
String level = 'beginner',
List<String> instructions = const ['Breathe.'],
int imageCount = 0,
}) => Exercise(
id: id,
name: name ?? id.replaceAll('_', ' '),
mode: mode,
category: mode == Mode.stretching ? 'stretching' : 'strength',
level: level,
equipment: equipment,
primaryMuscles: primary,
secondaryMuscles: secondary,
instructions: instructions,
imageCount: imageCount,
);
/// Two stretches and a squat. The 90/90 Hamstring's images exist in the real assets.
final sampleLibrary = ExerciseLibrary([
exercise(
'90_90_Hamstring',
Mode.stretching,
name: '90/90 Hamstring',
secondary: const ['calves'],
instructions: const ['Lie on your back.', 'Straighten one leg.'],
imageCount: 2,
),
exercise('Calf_Stretch', Mode.stretching, primary: const ['calves'], equipment: 'other', level: 'intermediate'),
exercise('Barbell_Squat', Mode.workout, primary: const ['quadriceps'], equipment: 'barbell'),
]);
/// Counts chimes instead of playing them and leaves the screen alone.
class FakePlayerEffects implements PlayerEffects {
final chimes = <Mode>[];
bool screenOn = false;
@override
Future<void> chime(Mode mode) async => chimes.add(mode);
@override
Future<void> keepScreenOn(bool on) async => screenOn = on;
}
/// Records what would be scheduled instead of talking to Android.
class FakeNotifier implements Notifier {
final scheduled = <(DateTime, String)>[];
var permissionRequests = 0;
@override
Future<void> init() async {}
@override
Future<bool> requestPermission() async {
permissionRequests++;
return true;
}
@override
Future<bool> enabled() async => true;
@override
Future<void> cancelAll() async => scheduled.clear();
@override
Future<void> schedule({required int id, required DateTime at, required String title, required String body}) async =>
scheduled.add((at, body));
}
/// A Pixel-sized screen instead of the default 800×600, so the floating
/// navigation bar does not cover what a test taps.
void usePhoneScreen(WidgetTester tester) {
tester.view.physicalSize = const Size(1080, 2400);
tester.view.devicePixelRatio = 2.75;
addTearDown(tester.view.reset);
}
/// The whole app over [library], with preferences starting from [prefs].
Future<SettingsController> pumpApp(
WidgetTester tester, {
ExerciseLibrary? library,
Map<String, Object> prefs = const {},
PlanRepository? plans,
SessionRepository? sessions,
PlayerEffects? effects,
FakeNotifier? notifier,
}) async {
SharedPreferences.setMockInitialValues(prefs);
final settings = SettingsController(SettingsService());
await settings.loadSettings();
final db = testDatabase();
addTearDown(db.close);
final sessionRepository = sessions ?? SessionRepository(db);
final fakeNotifier = notifier ?? FakeNotifier();
await tester.pumpWidget(
MultiProvider(
providers: [
ChangeNotifierProvider.value(value: settings),
Provider.value(value: library ?? sampleLibrary),
Provider.value(value: plans ?? PlanRepository(db)),
Provider.value(value: sessionRepository),
Provider<PlayerEffects>.value(value: effects ?? FakePlayerEffects()),
Provider<Notifier>.value(value: fakeNotifier),
Provider.value(value: ReminderScheduler(fakeNotifier, settings, sessionRepository)),
],
child: const ZenXerciseApp(),
),
);
await tester.pumpAndSettle();
return settings;
}