- Implémente la couche de synchronisation avec le serveur - Ajoute les fixtures versionnées pour les tests - Met à jour Drift database et repositories pour le support sync - Améliore les tests de synchronisation - Corrige et améliore le watch companion pour la collecte de métriques Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
899 lines
26 KiB
Dart
899 lines
26 KiB
Dart
import 'dart:convert';
|
||
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_test/flutter_test.dart';
|
||
import 'package:gametime/application/application.dart';
|
||
import 'package:gametime/domain/domain.dart';
|
||
import 'package:gametime/presentation/presentation.dart';
|
||
|
||
void main() {
|
||
testWidgets('la liste expose le lien secondaire Progression', (tester) async {
|
||
var opened = false;
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: HistoryListScreen(
|
||
historyUseCases: _historyUseCases(_FakeWorkoutHistoryRepository()),
|
||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||
activeUseCases: _activeUseCases(),
|
||
closeUseCase: _closeUseCase(),
|
||
onOpenProgression: () => opened = true,
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.tap(find.text('Progression'));
|
||
await tester.pump();
|
||
|
||
expect(opened, isTrue);
|
||
});
|
||
|
||
testWidgets('la liste groupe les séances par période', (tester) async {
|
||
final groups = groupHistoriesByPeriod([
|
||
_history(id: 'today', startedAt: DateTime.utc(2026, 7, 17, 9)),
|
||
_history(id: 'week', startedAt: DateTime.utc(2026, 7, 15, 9)),
|
||
_history(id: 'old', startedAt: DateTime.utc(2026, 7, 1, 9)),
|
||
], DateTime.utc(2026, 7, 17, 12));
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Scaffold(
|
||
body: Column(children: [for (final key in groups.keys) Text(key)]),
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(find.text("Aujourd'hui"), findsOneWidget);
|
||
expect(find.text('Cette semaine'), findsOneWidget);
|
||
expect(find.text('Plus ancien'), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('le détail affiche temps répétitions et score avec unité', (
|
||
tester,
|
||
) async {
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: HistoryDetailScreen(
|
||
history: _history(id: 'history-1'),
|
||
historyUseCases: _historyUseCases(_FakeWorkoutHistoryRepository()),
|
||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||
activeUseCases: _activeUseCases(),
|
||
closeUseCase: _closeUseCase(),
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(find.text('Programme jambes'), findsOneWidget);
|
||
expect(find.text('Squat'), findsOneWidget);
|
||
expect(
|
||
find.text('Temps 00:45 · 10 répétitions · Score 80 kg'),
|
||
findsOneWidget,
|
||
);
|
||
});
|
||
|
||
testWidgets('le détail affiche les stats montre quand elles existent', (
|
||
tester,
|
||
) async {
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: HistoryDetailScreen(
|
||
history: _history(
|
||
id: 'history-1',
|
||
withTelemetry: true,
|
||
minHeartRateBpm: 88,
|
||
averageHeartRateBpm: 126.4,
|
||
maxHeartRateBpm: 171,
|
||
totalDistanceMeters: 1234,
|
||
totalCaloriesKcal: 83,
|
||
),
|
||
historyUseCases: _historyUseCases(_FakeWorkoutHistoryRepository()),
|
||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||
activeUseCases: _activeUseCases(),
|
||
telemetryUseCases: _telemetryUseCases(),
|
||
closeUseCase: _closeUseCase(),
|
||
),
|
||
),
|
||
);
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.text('Stats montre'), findsOneWidget);
|
||
expect(find.text('Fréquence cardiaque'), findsOneWidget);
|
||
expect(find.text('Min'), findsAtLeastNWidgets(1));
|
||
expect(find.text('Moyenne'), findsOneWidget);
|
||
expect(find.text('Max'), findsAtLeastNWidgets(1));
|
||
expect(find.text('Distance'), findsAtLeastNWidgets(1));
|
||
expect(find.text('Calories'), findsAtLeastNWidgets(1));
|
||
expect(find.text('Étape'), findsOneWidget);
|
||
expect(find.text('Série'), findsAtLeastNWidgets(1));
|
||
expect(find.text('Exercice'), findsAtLeastNWidgets(1));
|
||
expect(find.text('Séance'), findsAtLeastNWidgets(1));
|
||
expect(find.text('FC'), findsOneWidget);
|
||
expect(find.text('Période'), findsOneWidget);
|
||
expect(find.text('30:00'), findsAtLeastNWidgets(1));
|
||
expect(find.text('Fréquence cardiaque · Séance · 03:00'), findsOneWidget);
|
||
expect(
|
||
find.byKey(const ValueKey('history-watch-stats-graph')),
|
||
findsOneWidget,
|
||
);
|
||
expect(find.text('88 bpm', findRichText: true), findsOneWidget);
|
||
expect(find.text('126 bpm', findRichText: true), findsOneWidget);
|
||
expect(find.text('171 bpm', findRichText: true), findsOneWidget);
|
||
expect(find.text('1.23 km', findRichText: true), findsOneWidget);
|
||
expect(find.text('83 kcal', findRichText: true), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('le détail affiche le score chrono comme temps réalisé', (
|
||
tester,
|
||
) async {
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: HistoryDetailScreen(
|
||
history: _history(id: 'history-1', stopwatchScore: true),
|
||
historyUseCases: _historyUseCases(_FakeWorkoutHistoryRepository()),
|
||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||
activeUseCases: _activeUseCases(),
|
||
closeUseCase: _closeUseCase(),
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(
|
||
find.text(
|
||
'Temps 00:45 · 10 répétitions · '
|
||
'Temps réalisé : 00:38.7 / objectif 00:45',
|
||
),
|
||
findsOneWidget,
|
||
);
|
||
expect(find.textContaining('Score 38.7'), findsNothing);
|
||
});
|
||
|
||
testWidgets('le détail affiche les résultats d’étapes par passage', (
|
||
tester,
|
||
) async {
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: HistoryDetailScreen(
|
||
history: _history(id: 'history-1', withStepResults: true),
|
||
historyUseCases: _historyUseCases(_FakeWorkoutHistoryRepository()),
|
||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||
activeUseCases: _activeUseCases(),
|
||
closeUseCase: _closeUseCase(),
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(find.text('Étapes réalisées'), findsOneWidget);
|
||
expect(find.text('Passage 1'), findsOneWidget);
|
||
expect(
|
||
find.text(
|
||
'Étape 1 · Dribble gauche · Temps 00:10 · Réussites : 4 paniers',
|
||
),
|
||
findsOneWidget,
|
||
);
|
||
expect(find.text('Étape 2 · Dribble droite'), findsOneWidget);
|
||
expect(find.text('Passée'), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('la suppression demande une confirmation', (tester) async {
|
||
final repository = _FakeWorkoutHistoryRepository();
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: HistoryDetailScreen(
|
||
history: _history(id: 'history-1'),
|
||
historyUseCases: _historyUseCases(repository),
|
||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||
activeUseCases: _activeUseCases(),
|
||
closeUseCase: _closeUseCase(),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.tap(find.text("Supprimer de l'historique"));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(repository.deletedIds, isEmpty);
|
||
expect(find.text("Supprimer de l'historique ?"), findsOneWidget);
|
||
|
||
await tester.tap(find.text('Annuler'));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(repository.deletedIds, isEmpty);
|
||
|
||
await tester.tap(find.text("Supprimer de l'historique"));
|
||
await tester.pumpAndSettle();
|
||
await tester.tap(find.widgetWithText(FilledButton, 'Supprimer'));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(repository.deletedIds, ['history-1']);
|
||
});
|
||
|
||
testWidgets('relancer un historique vide affiche une erreur sans navigation', (
|
||
tester,
|
||
) async {
|
||
final activeRepository = _FakeActiveSessionRepository();
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: HistoryDetailScreen(
|
||
history: _history(id: 'history-empty', emptySnapshot: true),
|
||
historyUseCases: _historyUseCases(_FakeWorkoutHistoryRepository()),
|
||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||
activeUseCases: _activeUseCases(activeRepository),
|
||
closeUseCase: _closeUseCase(),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.tap(find.text('Relancer cette séance'));
|
||
await tester.pump();
|
||
|
||
expect(
|
||
find.text(
|
||
'Cette séance ne peut pas être relancée car elle ne contient aucun exercice.',
|
||
),
|
||
findsOneWidget,
|
||
);
|
||
expect(find.byType(WorkoutExecutionScreen), findsNothing);
|
||
});
|
||
|
||
testWidgets('une session legacy invalide ne bloque pas une relance valide', (
|
||
tester,
|
||
) async {
|
||
final activeRepository = _FakeActiveSessionRepository()
|
||
..session = ActiveWorkoutSession(
|
||
metadata: _metadata('legacy-session'),
|
||
sourceWorkoutTemplateId: 'template-legacy',
|
||
status: ActiveWorkoutStatus.paused,
|
||
startedAt: DateTime.utc(2026, 7, 17, 9),
|
||
lastPersistedAt: DateTime.utc(2026, 7, 17, 9),
|
||
elapsedActiveMs: 0,
|
||
currentProgramIndex: 0,
|
||
currentExerciseIndex: 0,
|
||
currentSetIndex: 0,
|
||
resolvedTemplateSnapshotJson: _emptyResolvedSnapshot(),
|
||
);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: HistoryDetailScreen(
|
||
history: _history(id: 'history-1'),
|
||
historyUseCases: _historyUseCases(_FakeWorkoutHistoryRepository()),
|
||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||
activeUseCases: _activeUseCases(activeRepository),
|
||
closeUseCase: _closeUseCase(),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.tap(find.text('Relancer cette séance'));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.textContaining('Une séance est déjà en cours.'), findsNothing);
|
||
expect(find.byType(WorkoutExecutionScreen), findsOneWidget);
|
||
expect(activeRepository.session?.sourceWorkoutTemplateId, isNull);
|
||
});
|
||
}
|
||
|
||
WorkoutHistory _history({
|
||
required String id,
|
||
DateTime? startedAt,
|
||
bool stopwatchScore = false,
|
||
bool withStepResults = false,
|
||
bool withTelemetry = false,
|
||
bool emptySnapshot = false,
|
||
int? minHeartRateBpm,
|
||
double? averageHeartRateBpm,
|
||
int? maxHeartRateBpm,
|
||
double? totalDistanceMeters,
|
||
double? totalCaloriesKcal,
|
||
}) {
|
||
final start = startedAt ?? DateTime.utc(2026, 7, 17, 10);
|
||
return WorkoutHistory(
|
||
metadata: _metadata(id),
|
||
sourceWorkoutTemplateId: 'template-1',
|
||
sourceActiveWorkoutSessionId: 'session-1',
|
||
nameSnapshot: 'Séance jambes',
|
||
startedAt: start,
|
||
endedAt: start.add(const Duration(minutes: 30)),
|
||
totalActiveMs: 1800000,
|
||
minHeartRateBpm: minHeartRateBpm,
|
||
averageHeartRateBpm: averageHeartRateBpm,
|
||
maxHeartRateBpm: maxHeartRateBpm,
|
||
totalDistanceMeters: totalDistanceMeters,
|
||
totalCaloriesKcal: totalCaloriesKcal,
|
||
completed: true,
|
||
historySnapshotJson: jsonEncode({
|
||
'sessionId': 'session-1',
|
||
'resolvedTemplateSnapshotJson': emptySnapshot
|
||
? _emptyResolvedSnapshot()
|
||
: _resolvedSnapshot(
|
||
stopwatchScore: stopwatchScore,
|
||
withSteps: withStepResults,
|
||
),
|
||
'results': [
|
||
{
|
||
'programSnapshotId': 'program-snapshot-1',
|
||
'exerciseSnapshotId': 'exercise-snapshot-1',
|
||
'programIndex': 0,
|
||
'exerciseIndex': 0,
|
||
'setIndex': 0,
|
||
'actualTimeMs': 45000,
|
||
'actualReps': 10,
|
||
if (stopwatchScore) 'actualScoreTimeMs': 38700,
|
||
if (!stopwatchScore) 'actualScore': 80,
|
||
'scoreLabelSnapshot': 'Charge',
|
||
'scoreInputModeSnapshot': stopwatchScore
|
||
? ScoreInputMode.stopwatch.name
|
||
: ScoreInputMode.manual.name,
|
||
if (!stopwatchScore) 'scoreUnitSnapshot': 'kg',
|
||
if (stopwatchScore) 'targetScoreTimeMsSnapshot': 45000,
|
||
},
|
||
],
|
||
if (withTelemetry)
|
||
'telemetrySamples': [
|
||
{
|
||
'id': 'telemetry-1',
|
||
'sessionId': 'session-1',
|
||
'capturedAt': start.toUtc().toIso8601String(),
|
||
'programIndex': 0,
|
||
'exerciseIndex': 0,
|
||
'setIndex': 0,
|
||
'stepIndex': 0,
|
||
'heartRateBpm': 88,
|
||
'distanceMeters': 0,
|
||
'caloriesKcal': 0,
|
||
},
|
||
{
|
||
'id': 'telemetry-2',
|
||
'sessionId': 'session-1',
|
||
'capturedAt': start
|
||
.add(const Duration(minutes: 1, seconds: 30))
|
||
.toUtc()
|
||
.toIso8601String(),
|
||
'programIndex': 0,
|
||
'exerciseIndex': 0,
|
||
'setIndex': 0,
|
||
'stepIndex': 0,
|
||
'heartRateBpm': 126,
|
||
'distanceMeters': 600,
|
||
'caloriesKcal': 40,
|
||
},
|
||
{
|
||
'id': 'telemetry-3',
|
||
'sessionId': 'session-1',
|
||
'capturedAt': start
|
||
.add(const Duration(minutes: 3))
|
||
.toUtc()
|
||
.toIso8601String(),
|
||
'programIndex': 0,
|
||
'exerciseIndex': 0,
|
||
'setIndex': 0,
|
||
'stepIndex': 0,
|
||
'heartRateBpm': 171,
|
||
'distanceMeters': 1234,
|
||
'caloriesKcal': 83,
|
||
},
|
||
],
|
||
}),
|
||
stepResults: withStepResults
|
||
? [
|
||
WorkoutHistoryStepResult(
|
||
metadata: _metadata('step-result-1'),
|
||
workoutHistoryId: id,
|
||
programSnapshotId: 'program-snapshot-1',
|
||
exerciseSnapshotId: 'exercise-snapshot-1',
|
||
programIndex: 0,
|
||
exerciseIndex: 0,
|
||
setIndex: 0,
|
||
passageIndex: 0,
|
||
stepIndex: 0,
|
||
stepSnapshotId: 'step-1',
|
||
stepNameSnapshot: 'Dribble gauche',
|
||
stepTypeSnapshot: ExerciseStepType.time,
|
||
targetValueSnapshot: 10,
|
||
hasScoreSnapshot: true,
|
||
scoreInputModeSnapshot: ScoreInputMode.manual,
|
||
scoreLabelSnapshot: 'Réussites',
|
||
scoreUnitSnapshot: 'paniers',
|
||
targetScoreSnapshot: 3,
|
||
status: SetResultStatus.completed,
|
||
actualTimeMs: 10000,
|
||
actualScore: 4,
|
||
),
|
||
WorkoutHistoryStepResult(
|
||
metadata: _metadata('step-result-2'),
|
||
workoutHistoryId: id,
|
||
programSnapshotId: 'program-snapshot-1',
|
||
exerciseSnapshotId: 'exercise-snapshot-1',
|
||
programIndex: 0,
|
||
exerciseIndex: 0,
|
||
setIndex: 0,
|
||
passageIndex: 0,
|
||
stepIndex: 1,
|
||
stepSnapshotId: 'step-2',
|
||
stepNameSnapshot: 'Dribble droite',
|
||
stepTypeSnapshot: ExerciseStepType.reps,
|
||
targetValueSnapshot: 8,
|
||
hasScoreSnapshot: false,
|
||
status: SetResultStatus.skipped,
|
||
),
|
||
]
|
||
: const [],
|
||
);
|
||
}
|
||
|
||
String _resolvedSnapshot({
|
||
bool stopwatchScore = false,
|
||
bool withSteps = false,
|
||
}) {
|
||
return jsonEncode({
|
||
'name': 'Séance jambes',
|
||
'programs': [
|
||
{
|
||
'id': 'program-snapshot-1',
|
||
'programNameSnapshot': 'Programme jambes',
|
||
'programSnapshotJson': jsonEncode({
|
||
'exercises': [
|
||
{
|
||
'id': 'exercise-snapshot-1',
|
||
'exerciseNameSnapshot': 'Squat',
|
||
'setsCount': 1,
|
||
'timeEnabled': true,
|
||
'repsEnabled': true,
|
||
'scoreEnabled': true,
|
||
'targetTimeSeconds': 45,
|
||
'targetReps': 10,
|
||
if (stopwatchScore) 'targetScoreTimeMs': 45000,
|
||
if (!stopwatchScore) 'targetScore': 80,
|
||
'scoreInputModeSnapshot': stopwatchScore
|
||
? ScoreInputMode.stopwatch.name
|
||
: ScoreInputMode.manual.name,
|
||
'scoreLabelSnapshot': 'Charge',
|
||
if (!stopwatchScore) 'scoreUnitSnapshot': 'kg',
|
||
'restSecondsOverride': 0,
|
||
if (withSteps)
|
||
'exerciseStepsSnapshot': [
|
||
{
|
||
'id': 'step-1',
|
||
'position': 0,
|
||
'name': 'Dribble gauche',
|
||
'type': ExerciseStepType.time.name,
|
||
'defaultTargetValue': 10,
|
||
'hasScore': true,
|
||
'scoreInputMode': ScoreInputMode.manual.name,
|
||
'scoreLabel': 'Réussites',
|
||
'scoreUnit': 'paniers',
|
||
'defaultTargetScore': 3,
|
||
},
|
||
{
|
||
'id': 'step-2',
|
||
'position': 1,
|
||
'name': 'Dribble droite',
|
||
'type': ExerciseStepType.reps.name,
|
||
'defaultTargetValue': 8,
|
||
'hasScore': false,
|
||
},
|
||
],
|
||
},
|
||
],
|
||
}),
|
||
},
|
||
],
|
||
'overrides': const [],
|
||
});
|
||
}
|
||
|
||
String _emptyResolvedSnapshot() {
|
||
return jsonEncode({
|
||
'name': 'Séance vide',
|
||
'programs': [
|
||
{
|
||
'id': 'program-snapshot-empty',
|
||
'programNameSnapshot': 'Programme vide',
|
||
'programSnapshotJson': jsonEncode({'exercises': const []}),
|
||
},
|
||
],
|
||
'overrides': const [],
|
||
});
|
||
}
|
||
|
||
EntityMetadata _metadata(String id) {
|
||
return EntityMetadata(
|
||
id: id,
|
||
createdAt: DateTime.utc(2026, 7, 17),
|
||
updatedAt: DateTime.utc(2026, 7, 17),
|
||
originDeviceId: 'device-1',
|
||
);
|
||
}
|
||
|
||
WorkoutHistoryUseCases _historyUseCases(
|
||
_FakeWorkoutHistoryRepository repository,
|
||
) {
|
||
return WorkoutHistoryUseCases(
|
||
repository: repository,
|
||
clock: _FakeClock(DateTime.utc(2026, 7, 17)),
|
||
);
|
||
}
|
||
|
||
WorkoutTemplateUseCases _workoutTemplateUseCases() {
|
||
return WorkoutTemplateUseCases(
|
||
templateRepository: _FakeWorkoutTemplateRepository(),
|
||
programRepository: _FakeProgramRepository(),
|
||
clock: _FakeClock(DateTime.utc(2026, 7, 17)),
|
||
ids: _FakeIds(),
|
||
originDeviceId: 'device-1',
|
||
);
|
||
}
|
||
|
||
ActiveWorkoutSessionUseCases _activeUseCases([
|
||
_FakeActiveSessionRepository? repository,
|
||
]) {
|
||
return ActiveWorkoutSessionUseCases(
|
||
sessionRepository: repository ?? _FakeActiveSessionRepository(),
|
||
templateRepository: _FakeWorkoutTemplateRepository(),
|
||
clock: _FakeClock(DateTime.utc(2026, 7, 17)),
|
||
ids: _FakeIds(),
|
||
originDeviceId: 'device-1',
|
||
);
|
||
}
|
||
|
||
CloseWorkoutSessionUseCase _closeUseCase() {
|
||
return CloseWorkoutSessionUseCase(
|
||
sessionRepository: _FakeActiveSessionRepository(),
|
||
historyRepository: _FakeWorkoutHistoryRepository(),
|
||
clock: _FakeClock(DateTime.utc(2026, 7, 17)),
|
||
ids: _FakeIds(),
|
||
originDeviceId: 'device-1',
|
||
);
|
||
}
|
||
|
||
WorkoutTelemetryUseCases _telemetryUseCases([
|
||
_FakeWorkoutTelemetryRepository? repository,
|
||
]) {
|
||
return WorkoutTelemetryUseCases(
|
||
repository: repository ?? _FakeWorkoutTelemetryRepository(),
|
||
clock: _FakeClock(DateTime.utc(2026, 7, 17)),
|
||
ids: _FakeIds(),
|
||
);
|
||
}
|
||
|
||
final class _FakeClock implements Clock {
|
||
_FakeClock(this.value);
|
||
|
||
DateTime value;
|
||
|
||
@override
|
||
DateTime now() => value;
|
||
}
|
||
|
||
final class _FakeIds implements IdGenerator {
|
||
var _next = 0;
|
||
|
||
@override
|
||
String newId() {
|
||
_next += 1;
|
||
return 'id-$_next';
|
||
}
|
||
}
|
||
|
||
final class _FakeWorkoutHistoryRepository implements WorkoutHistoryRepository {
|
||
final deletedIds = <String>[];
|
||
|
||
@override
|
||
Future<void> delete(String id, DateTime deletedAt) async {
|
||
deletedIds.add(id);
|
||
}
|
||
|
||
@override
|
||
Future<WorkoutHistory?> findById(String id) async => null;
|
||
|
||
@override
|
||
Future<List<WorkoutHistory>> listActive() async => const [];
|
||
|
||
@override
|
||
Future<void> save(WorkoutHistory history) async {}
|
||
|
||
@override
|
||
Future<void> patchHeartRateSummary({
|
||
required String historyId,
|
||
int? minHeartRateBpm,
|
||
required double averageHeartRateBpm,
|
||
required int maxHeartRateBpm,
|
||
double? totalDistanceMeters,
|
||
double? totalCaloriesKcal,
|
||
required DateTime patchedAt,
|
||
}) async {}
|
||
|
||
@override
|
||
Future<void> saveSetResult(WorkoutHistorySetResult result) async {}
|
||
|
||
@override
|
||
Future<void> saveStepResult(WorkoutHistoryStepResult result) async {}
|
||
}
|
||
|
||
final class _FakeWorkoutTemplateRepository
|
||
implements WorkoutTemplateRepository {
|
||
@override
|
||
Future<WorkoutTemplate?> findById(String id) async => null;
|
||
|
||
@override
|
||
Future<List<WorkoutTemplate>> listActive() async => const [];
|
||
|
||
@override
|
||
Future<void> replaceComposition(
|
||
WorkoutTemplate template,
|
||
DateTime deletedAt,
|
||
) async {}
|
||
|
||
@override
|
||
Future<void> save(WorkoutTemplate template) async {}
|
||
|
||
@override
|
||
Future<void> saveAll(List<WorkoutTemplate> templates) async {}
|
||
|
||
@override
|
||
Future<void> saveOverride(WorkoutTemplateExerciseOverride override) async {}
|
||
|
||
@override
|
||
Future<void> saveProgram(WorkoutTemplateProgram program) async {}
|
||
}
|
||
|
||
final class _FakeProgramRepository implements ProgramRepository {
|
||
@override
|
||
Future<Program?> findById(String id) async => null;
|
||
|
||
@override
|
||
Future<List<Program>> listActive() async => const [];
|
||
|
||
@override
|
||
Future<void> replaceExercises(Program program, DateTime deletedAt) async {}
|
||
|
||
@override
|
||
Future<void> save(Program program) async {}
|
||
|
||
@override
|
||
Future<void> saveExercise(ProgramExercise exercise) async {}
|
||
}
|
||
|
||
final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||
ActiveWorkoutSession? session;
|
||
|
||
@override
|
||
Future<ActiveWorkoutSession?> findById(String id) async {
|
||
return session?.metadata.id == id ? session : null;
|
||
}
|
||
|
||
@override
|
||
Future<ActiveWorkoutSession?> findOpen() async => session;
|
||
|
||
@override
|
||
Future<ActiveRestState?> findRestStateById(String id) async => null;
|
||
|
||
@override
|
||
Future<ActiveScoreStopwatchState?> findScoreStopwatchState({
|
||
required String sessionId,
|
||
required int programIndex,
|
||
required int exerciseIndex,
|
||
required int setIndex,
|
||
}) async => null;
|
||
|
||
@override
|
||
Future<ActiveManualScoreState?> findManualScoreState({
|
||
required String sessionId,
|
||
required int programIndex,
|
||
required int exerciseIndex,
|
||
required int setIndex,
|
||
}) async => null;
|
||
|
||
@override
|
||
Future<ActiveSetTimerState?> findSetTimerState({
|
||
required String sessionId,
|
||
required int programIndex,
|
||
required int exerciseIndex,
|
||
required int setIndex,
|
||
}) async => null;
|
||
|
||
@override
|
||
Future<ActiveExerciseStepProgressState?> findExerciseStepProgressState({
|
||
required String sessionId,
|
||
required int programIndex,
|
||
required int exerciseIndex,
|
||
required int setIndex,
|
||
}) async => null;
|
||
|
||
@override
|
||
Future<List<ActiveRestState>> listRestStates(String sessionId) async {
|
||
return const [];
|
||
}
|
||
|
||
@override
|
||
Future<List<ActiveScoreStopwatchState>> listScoreStopwatchStates(
|
||
String sessionId,
|
||
) async {
|
||
return const [];
|
||
}
|
||
|
||
@override
|
||
Future<List<ActiveManualScoreState>> listManualScoreStates(
|
||
String sessionId,
|
||
) async {
|
||
return const [];
|
||
}
|
||
|
||
@override
|
||
Future<List<ActiveSetTimerState>> listSetTimerStates(String sessionId) async {
|
||
return const [];
|
||
}
|
||
|
||
@override
|
||
Future<List<ActiveSetResult>> listSetResults(String sessionId) async {
|
||
return const [];
|
||
}
|
||
|
||
@override
|
||
Future<List<ActiveExerciseStepProgressState>> listExerciseStepProgressStates(
|
||
String sessionId,
|
||
) async {
|
||
return const [];
|
||
}
|
||
|
||
@override
|
||
Future<List<ActiveExerciseStepResult>> listExerciseStepResults(
|
||
String sessionId,
|
||
) async {
|
||
return const [];
|
||
}
|
||
|
||
@override
|
||
Future<void> save(ActiveWorkoutSession session) async {
|
||
this.session = session;
|
||
}
|
||
|
||
@override
|
||
Future<void> saveRestState(ActiveRestState restState) async {}
|
||
|
||
@override
|
||
Future<void> saveScoreStopwatchState(ActiveScoreStopwatchState state) async {}
|
||
|
||
@override
|
||
Future<void> saveManualScoreState(ActiveManualScoreState state) async {}
|
||
|
||
@override
|
||
Future<void> saveSetTimerState(ActiveSetTimerState state) async {}
|
||
|
||
@override
|
||
Future<void> saveExerciseStepProgressState(
|
||
ActiveExerciseStepProgressState state,
|
||
) async {}
|
||
|
||
@override
|
||
Future<void> saveExerciseStepResult(ActiveExerciseStepResult result) async {}
|
||
|
||
@override
|
||
Future<void> deleteScoreStopwatchState({
|
||
required String sessionId,
|
||
required int programIndex,
|
||
required int exerciseIndex,
|
||
required int setIndex,
|
||
required DateTime deletedAt,
|
||
}) async {}
|
||
|
||
@override
|
||
Future<void> deleteManualScoreState({
|
||
required String sessionId,
|
||
required int programIndex,
|
||
required int exerciseIndex,
|
||
required int setIndex,
|
||
required DateTime deletedAt,
|
||
}) async {}
|
||
|
||
@override
|
||
Future<void> saveSetResult(ActiveSetResult result) async {}
|
||
}
|
||
|
||
final class _FakeWorkoutTelemetryRepository
|
||
implements WorkoutTelemetryRepository {
|
||
final samples = <WorkoutTelemetrySample>[];
|
||
|
||
@override
|
||
Future<bool> saveSample(WorkoutTelemetrySample sample) async {
|
||
samples.add(sample);
|
||
return true;
|
||
}
|
||
|
||
@override
|
||
Future<ActiveWorkoutTelemetryWindowState?> findWindowState(
|
||
String sessionId,
|
||
) async => null;
|
||
|
||
@override
|
||
Future<void> saveWindowState(ActiveWorkoutTelemetryWindowState state) async {}
|
||
|
||
@override
|
||
Future<void> deleteWindowState(String sessionId) async {}
|
||
|
||
@override
|
||
Future<List<WorkoutTelemetrySample>> listSamples(String sessionId) async {
|
||
return samples
|
||
.where((sample) => sample.sessionId == sessionId)
|
||
.toList(growable: false);
|
||
}
|
||
|
||
@override
|
||
Future<List<WorkoutTelemetrySample>> listSamplesForScope({
|
||
required String sessionId,
|
||
required WorkoutTelemetryAggregateScope scope,
|
||
int? programIndex,
|
||
int? exerciseIndex,
|
||
int? setIndex,
|
||
int? passageIndex,
|
||
int? stepIndex,
|
||
}) async {
|
||
return samples
|
||
.where(
|
||
(sample) =>
|
||
sample.sessionId == sessionId &&
|
||
_fakeTelemetrySampleMatchesScope(
|
||
sample,
|
||
scope: scope,
|
||
programIndex: programIndex,
|
||
exerciseIndex: exerciseIndex,
|
||
setIndex: setIndex,
|
||
passageIndex: passageIndex,
|
||
stepIndex: stepIndex,
|
||
),
|
||
)
|
||
.toList(growable: false);
|
||
}
|
||
|
||
@override
|
||
Future<void> replaceAggregatesForSession({
|
||
required String sessionId,
|
||
required List<WorkoutTelemetryAggregate> aggregates,
|
||
}) async {}
|
||
|
||
@override
|
||
Future<List<WorkoutTelemetryAggregate>> listAggregates(
|
||
String sessionId,
|
||
) async => const [];
|
||
|
||
@override
|
||
Future<WorkoutTelemetryAggregate?> findAggregate({
|
||
required String sessionId,
|
||
required WorkoutTelemetryAggregateScope scope,
|
||
int? programIndex,
|
||
int? exerciseIndex,
|
||
int? setIndex,
|
||
int? passageIndex,
|
||
int? stepIndex,
|
||
}) async => null;
|
||
}
|
||
|
||
bool _fakeTelemetrySampleMatchesScope(
|
||
WorkoutTelemetrySample sample, {
|
||
required WorkoutTelemetryAggregateScope scope,
|
||
int? programIndex,
|
||
int? exerciseIndex,
|
||
int? setIndex,
|
||
int? passageIndex,
|
||
int? stepIndex,
|
||
}) {
|
||
return switch (scope) {
|
||
WorkoutTelemetryAggregateScope.session => true,
|
||
WorkoutTelemetryAggregateScope.exercise =>
|
||
sample.programIndex == programIndex &&
|
||
sample.exerciseIndex == exerciseIndex,
|
||
WorkoutTelemetryAggregateScope.set =>
|
||
sample.programIndex == programIndex &&
|
||
sample.exerciseIndex == exerciseIndex &&
|
||
sample.setIndex == setIndex,
|
||
WorkoutTelemetryAggregateScope.step =>
|
||
sample.programIndex == programIndex &&
|
||
sample.exerciseIndex == exerciseIndex &&
|
||
sample.setIndex == setIndex &&
|
||
(passageIndex == null || sample.passageIndex == passageIndex) &&
|
||
sample.stepIndex == stepIndex,
|
||
};
|
||
}
|