Files
GameTime/test/presentation/history_screen_test.dart
Blomios fe3608cc4a feat(server): implemente synchronisation et serveur avec fixtures (#187)
- 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>
2026-08-04 23:07:31 +02:00

899 lines
26 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 '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,
};
}