- 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>
102 lines
3.3 KiB
Dart
102 lines
3.3 KiB
Dart
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 '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;
|
||
}
|
||
|
||
/// 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,
|
||
}) async {
|
||
SharedPreferences.setMockInitialValues(prefs);
|
||
final settings = SettingsController(SettingsService());
|
||
await settings.loadSettings();
|
||
final db = testDatabase();
|
||
addTearDown(db.close);
|
||
await tester.pumpWidget(
|
||
MultiProvider(
|
||
providers: [
|
||
ChangeNotifierProvider.value(value: settings),
|
||
Provider.value(value: library ?? sampleLibrary),
|
||
Provider.value(value: plans ?? PlanRepository(db)),
|
||
Provider.value(value: sessions ?? SessionRepository(db)),
|
||
Provider<PlayerEffects>.value(value: effects ?? FakePlayerEffects()),
|
||
],
|
||
child: const ZenXerciseApp(),
|
||
),
|
||
);
|
||
await tester.pumpAndSettle();
|
||
return settings;
|
||
}
|