Files
GameTime/test/infrastructure/drift_repositories_test.dart
Blomios 75f52200a2 feat(exécution): refonte des chronos de séance et carte exercice actif (ticket #90)
Réorganise l'écran d'exécution autour d'une carte "Exercice actif" sous
le compteur SÉRIE X/Y (nom, médias, temps de série, action "Démarrer
l'exercice"). Le timer de série devient un état persistant dédié (plus
un DateTime UI volatile), pause-aware, et "Démarrer l'exercice" lance
en une action tous les chronos qui doivent démarrer en début de série
(série, première étape temps, score chrono si stopwatch).

Implémentation restée non commitée depuis sa réalisation ; QA a validé
les vérifications statiques exécutables en sandbox (dart analyze,
git diff --check). flutter analyze / flutter test restent à relancer
dans un environnement avec cache Flutter SDK accessible.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 22:26:28 +02:00

877 lines
28 KiB
Dart

import 'dart:convert';
import 'package:drift/drift.dart' as drift;
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:gametime/application/application.dart';
import 'package:gametime/domain/domain.dart';
import 'package:gametime/infrastructure/local/local.dart' as local;
void main() {
late local.AppDatabase database;
late local.DriftExerciseRepository exerciseRepository;
late local.DriftProgramRepository programRepository;
late local.DriftActiveSessionRepository activeRepository;
late local.DriftWorkoutTemplateRepository templateRepository;
late local.DriftWorkoutHistoryRepository historyRepository;
setUp(() {
database = local.AppDatabase(NativeDatabase.memory());
exerciseRepository = local.DriftExerciseRepository(database);
programRepository = local.DriftProgramRepository(database);
activeRepository = local.DriftActiveSessionRepository(database);
templateRepository = local.DriftWorkoutTemplateRepository(database);
historyRepository = local.DriftWorkoutHistoryRepository(database);
});
tearDown(() async {
await database.close();
});
test('exercise mutations are written to change log', () async {
final now = DateTime.utc(2026, 7, 17, 12);
final exercise = Exercise(
metadata: _metadata('exercise-1', now),
name: 'Squat',
hasTimeMeasure: false,
hasRepsMeasure: true,
hasScoreMeasure: false,
defaultTargetReps: 10,
);
await exerciseRepository.save(exercise);
await exerciseRepository.save(
exercise.copyWith(
metadata: _metadata(
'exercise-1',
now.add(const Duration(seconds: 1)),
1,
),
name: 'Front squat',
),
);
await exerciseRepository.save(
exercise
.copyWith(
metadata: _metadata(
'exercise-1',
now.add(const Duration(seconds: 2)),
2,
),
name: 'Front squat',
)
.archive(now.add(const Duration(seconds: 2))),
);
final changes =
await (database.select(database.changeLogEntries)
..where((table) => table.entityId.equals('exercise-1'))
..orderBy([
(table) => drift.OrderingTerm.asc(table.localRevision),
]))
.get();
expect(changes.map((change) => change.operation), [
'insert',
'update',
'update',
]);
expect(changes.every((change) => change.entityType == 'Exercise'), isTrue);
});
test('exercise repository accepts zero manual default score', () async {
final exercise = Exercise(
metadata: _metadata('exercise-score-zero', DateTime.utc(2026, 7, 17, 12)),
name: 'Score nul',
hasTimeMeasure: false,
hasRepsMeasure: false,
hasScoreMeasure: true,
scoreLabel: 'Score',
scoreUnit: 'pts',
defaultTargetScore: 0,
);
await exerciseRepository.save(exercise);
final restored = await exerciseRepository.findById(exercise.metadata.id);
expect(restored, isNotNull);
expect(restored!.defaultTargetScore, 0);
});
test(
'step chaining settings round-trip through drift repositories',
() async {
final now = DateTime.utc(2026, 7, 17, 12);
final exercise = Exercise(
metadata: _metadata('exercise-chain-1', now),
name: 'Circuit',
hasTimeMeasure: false,
hasRepsMeasure: true,
hasScoreMeasure: false,
defaultTargetReps: 10,
autoStartNextTimedStep: false,
);
await exerciseRepository.save(exercise);
final restoredExercise = await exerciseRepository.findById(
exercise.metadata.id,
);
expect(restoredExercise!.autoStartNextTimedStep, isFalse);
final programExercise = _programExercise(
'program-exercise-chain-1',
now,
programId: 'program-chain-1',
position: 0,
autoStartNextTimedStepSnapshot: false,
autoStartNextTimedStepOverride: true,
);
await programRepository.save(
Program(
metadata: _metadata('program-chain-1', now),
name: 'Programme',
defaultRestSeconds: 60,
exercises: [programExercise],
),
);
final restoredProgram = await programRepository.findById(
'program-chain-1',
);
expect(
restoredProgram!.exercises.single.autoStartNextTimedStepSnapshot,
isFalse,
);
expect(
restoredProgram.exercises.single.autoStartNextTimedStepOverride,
isTrue,
);
final templateProgram = WorkoutTemplateProgram(
metadata: _metadata('template-program-chain-1', now),
workoutTemplateId: 'template-chain-1',
sourceProgramId: 'program-chain-1',
position: 0,
programNameSnapshot: 'Programme',
defaultRestSecondsSnapshot: 60,
programSnapshotJson: jsonEncode({
'exercises': [programExercise.toSnapshotJson()],
}),
);
await templateRepository.save(
WorkoutTemplate(
metadata: _metadata('template-chain-1', now),
name: 'Séance',
programs: [templateProgram],
overrides: [
WorkoutTemplateExerciseOverride(
metadata: _metadata('override-chain-1', now),
workoutTemplateProgramId: templateProgram.metadata.id,
snapshotProgramExerciseId: programExercise.metadata.id,
),
WorkoutTemplateExerciseOverride(
metadata: _metadata('override-chain-2', now),
workoutTemplateProgramId: templateProgram.metadata.id,
snapshotProgramExerciseId: 'program-exercise-chain-2',
autoStartNextTimedStepOverride: false,
),
],
),
);
final restoredTemplate = await templateRepository.findById(
'template-chain-1',
);
expect(
restoredTemplate!.overrides
.singleWhere(
(override) => override.metadata.id == 'override-chain-1',
)
.autoStartNextTimedStepOverride,
isNull,
);
expect(
restoredTemplate.overrides
.singleWhere(
(override) => override.metadata.id == 'override-chain-2',
)
.autoStartNextTimedStepOverride,
isFalse,
);
},
);
test('exercise repository round-trips configured steps', () async {
final now = DateTime.utc(2026, 7, 17, 12);
final exercise = Exercise(
metadata: _metadata('exercise-steps-1', now),
name: 'Burpee complexe',
hasTimeMeasure: false,
hasRepsMeasure: true,
hasScoreMeasure: false,
defaultTargetReps: 1,
steps: [
ExerciseStep(
id: 'step-1',
position: 0,
name: 'Planche',
type: ExerciseStepType.time,
defaultTargetValue: 20,
),
ExerciseStep(
id: 'step-2',
position: 1,
name: 'Sauts',
type: ExerciseStepType.reps,
defaultTargetValue: 8,
hasScore: true,
scoreLabel: 'Amplitude',
scoreUnit: 'pts',
defaultTargetScore: 0,
),
ExerciseStep(
id: 'step-3',
position: 2,
name: 'Sprint final',
type: ExerciseStepType.time,
defaultTargetValue: 10,
hasScore: true,
scoreInputMode: ScoreInputMode.stopwatch,
defaultTargetScoreTimeMs: 12000,
),
],
);
await exerciseRepository.save(exercise);
final restored = await exerciseRepository.findById(exercise.metadata.id);
expect(restored, isNotNull);
expect(restored!.steps, hasLength(3));
expect(restored.steps.map((step) => step.name), [
'Planche',
'Sauts',
'Sprint final',
]);
expect(restored.steps[1].defaultTargetScore, 0);
expect(restored.steps[2].scoreInputMode, ScoreInputMode.stopwatch);
expect(restored.steps[2].defaultTargetScoreTimeMs, 12000);
});
group('exercise repository round-trips exercise option combinations', () {
final now = DateTime.utc(2026, 7, 17, 12);
final cases = <_ExerciseRoundTripCase>[
_ExerciseRoundTripCase(
label: 'reps without score or steps',
exercise: Exercise(
metadata: _metadata('exercise-combo-reps', now),
name: 'Pompes',
description: 'Simple reps target',
hasTimeMeasure: false,
hasRepsMeasure: true,
hasScoreMeasure: false,
defaultTargetReps: 12,
),
),
_ExerciseRoundTripCase(
label: 'reps without score and with steps',
exercise: Exercise(
metadata: _metadata('exercise-combo-reps-steps', now),
name: 'Complexe poids du corps',
hasTimeMeasure: false,
hasRepsMeasure: true,
hasScoreMeasure: false,
defaultTargetReps: 1,
steps: [
_exerciseStep(
id: 'combo-reps-step-1',
position: 0,
name: 'Pompes',
type: ExerciseStepType.reps,
defaultTargetValue: 10,
),
_exerciseStep(
id: 'combo-reps-step-2',
position: 1,
name: 'Gainage',
type: ExerciseStepType.time,
defaultTargetValue: 30,
),
],
),
),
_ExerciseRoundTripCase(
label: 'manual score without steps',
exercise: Exercise(
metadata: _metadata('exercise-combo-manual-score', now),
name: 'Charge max',
hasTimeMeasure: false,
hasRepsMeasure: false,
hasScoreMeasure: true,
scoreLabel: 'Charge',
scoreUnit: 'kg',
defaultTargetScore: 0,
),
),
_ExerciseRoundTripCase(
label: 'manual score with unscored steps',
exercise: Exercise(
metadata: _metadata('exercise-combo-manual-score-steps', now),
name: 'Technique haltères',
hasTimeMeasure: false,
hasRepsMeasure: true,
hasScoreMeasure: true,
scoreLabel: 'Charge',
scoreUnit: 'kg',
defaultTargetReps: 8,
defaultTargetScore: 12.5,
steps: [
_exerciseStep(
id: 'combo-manual-score-step-1',
position: 0,
name: 'Installation',
type: ExerciseStepType.time,
defaultTargetValue: 15,
),
_exerciseStep(
id: 'combo-manual-score-step-2',
position: 1,
name: 'Serie',
type: ExerciseStepType.reps,
defaultTargetValue: 8,
),
],
),
),
_ExerciseRoundTripCase(
label: 'manual exercise score with manual scored steps',
exercise: Exercise(
metadata: _metadata('exercise-combo-manual-step-score', now),
name: 'Circuit precision',
hasTimeMeasure: true,
hasRepsMeasure: true,
hasScoreMeasure: true,
scoreLabel: 'Qualite',
scoreUnit: 'pts',
defaultTargetTimeSeconds: 45,
defaultTargetReps: 10,
defaultTargetScore: 80,
autoStartNextTimedStep: false,
steps: [
_exerciseStep(
id: 'combo-manual-step-score-1',
position: 0,
name: 'Bloc reps',
type: ExerciseStepType.reps,
defaultTargetValue: 10,
hasScore: true,
scoreLabel: 'Amplitude',
scoreUnit: 'pts',
defaultTargetScore: 0,
),
_exerciseStep(
id: 'combo-manual-step-score-2',
position: 1,
name: 'Bloc temps',
type: ExerciseStepType.time,
defaultTargetValue: 35,
hasScore: true,
scoreLabel: 'Tenue',
scoreUnit: 's',
defaultTargetScore: 35,
),
],
),
),
_ExerciseRoundTripCase(
label: 'stopwatch score without steps',
exercise: Exercise(
metadata: _metadata('exercise-combo-stopwatch-score', now),
name: 'Sprint chrono',
hasTimeMeasure: true,
hasRepsMeasure: false,
hasScoreMeasure: true,
scoreInputMode: ScoreInputMode.stopwatch,
defaultTargetTimeSeconds: 20,
defaultTargetScoreTimeMs: 11500,
),
),
_ExerciseRoundTripCase(
label: 'stopwatch score with stopwatch scored steps',
exercise: Exercise(
metadata: _metadata('exercise-combo-stopwatch-score-steps', now),
name: 'Sprint fractionne',
imageMediaIds: const ['media-start', 'media-finish'],
iconMediaId: 'media-start',
videoMediaId: 'video-demo',
hasTimeMeasure: true,
hasRepsMeasure: true,
hasScoreMeasure: true,
scoreInputMode: ScoreInputMode.stopwatch,
defaultTargetTimeSeconds: 60,
defaultTargetReps: 4,
defaultTargetScoreTimeMs: 42000,
steps: [
_exerciseStep(
id: 'combo-stopwatch-step-1',
position: 0,
name: 'Acceleration',
type: ExerciseStepType.time,
defaultTargetValue: 10,
hasScore: true,
scoreInputMode: ScoreInputMode.stopwatch,
defaultTargetScoreTimeMs: 9500,
),
_exerciseStep(
id: 'combo-stopwatch-step-2',
position: 1,
name: 'Recuperation active',
type: ExerciseStepType.reps,
defaultTargetValue: 6,
),
],
),
),
];
for (final roundTripCase in cases) {
test(roundTripCase.label, () async {
await exerciseRepository.save(roundTripCase.exercise);
final restored = await exerciseRepository.findById(
roundTripCase.exercise.metadata.id,
);
expect(restored, isNotNull);
_expectExerciseEquals(restored!, roundTripCase.exercise);
});
}
});
test(
'program exercise step snapshot is preserved when source exercise changes',
() async {
final now = DateTime.utc(2026, 7, 17, 12);
final source = Exercise(
metadata: _metadata('exercise-source-1', now),
name: 'Complexe original',
hasTimeMeasure: false,
hasRepsMeasure: true,
hasScoreMeasure: false,
defaultTargetReps: 1,
steps: [
ExerciseStep(
id: 'step-original-1',
position: 0,
name: 'Phase originale A',
type: ExerciseStepType.time,
defaultTargetValue: 30,
),
ExerciseStep(
id: 'step-original-2',
position: 1,
name: 'Phase originale B',
type: ExerciseStepType.reps,
defaultTargetValue: 12,
),
],
);
await exerciseRepository.save(source);
await programRepository.save(
Program(
metadata: _metadata('program-steps-1', now),
name: 'Programme étapes',
defaultRestSeconds: 60,
),
);
final useCase = ProgramUseCases(
programRepository: programRepository,
exerciseRepository: exerciseRepository,
templateRepository: templateRepository,
clock: _FakeClock(now),
ids: _FakeIds(),
originDeviceId: 'device-1',
);
final snapshot = await useCase.addExercise(
programId: 'program-steps-1',
exerciseId: source.metadata.id,
position: 0,
setsCount: 1,
enabledMeasures: const {WorkoutMeasure.reps},
);
await exerciseRepository.save(
source.copyWith(
metadata: _metadata(
source.metadata.id,
now.add(const Duration(minutes: 1)),
1,
),
steps: [
ExerciseStep(
id: 'step-updated-1',
position: 0,
name: 'Phase modifiée',
type: ExerciseStepType.time,
defaultTargetValue: 45,
),
],
),
);
final restoredProgram = await programRepository.findById(
'program-steps-1',
);
final restoredSnapshot = restoredProgram!.exercises.single;
final snapshotJson = snapshot.toSnapshotJson();
expect(restoredSnapshot.exerciseStepsSnapshot, hasLength(2));
expect(
restoredSnapshot.exerciseStepsSnapshot.first.name,
'Phase originale A',
);
expect(
restoredSnapshot.exerciseStepsSnapshot.last.type,
ExerciseStepType.reps,
);
expect(
(snapshotJson['exerciseStepsSnapshot'] as List).map(
(step) => (step as Map)['name'],
),
['Phase originale A', 'Phase originale B'],
);
},
);
test(
'removing a program exercise writes a soft delete change log entry',
() async {
final now = DateTime.utc(2026, 7, 17, 12);
final first = _programExercise('program-exercise-1', now, position: 0);
final second = _programExercise('program-exercise-2', now, position: 1);
await programRepository.save(
Program(
metadata: _metadata('program-1', now),
name: 'Jambes',
defaultRestSeconds: 60,
exercises: [first, second],
),
);
final deletedAt = now.add(const Duration(minutes: 1));
await programRepository.replaceExercises(
Program(
metadata: _metadata('program-1', deletedAt, 1),
name: 'Jambes',
defaultRestSeconds: 60,
exercises: [first],
),
deletedAt,
);
final deletedRow = await (database.select(
database.programExercises,
)..where((table) => table.id.equals('program-exercise-2'))).getSingle();
final change =
await (database.select(database.changeLogEntries)..where(
(table) =>
table.entityType.equals('ProgramExercise') &
table.entityId.equals('program-exercise-2') &
table.operation.equals('softDelete'),
))
.getSingleOrNull();
expect(deletedRow.deletedAt?.toUtc(), deletedAt);
expect(deletedRow.syncState, 'deleted');
expect(deletedRow.localRevision, 1);
expect(change, isNotNull);
expect(change!.localRevision, 1);
},
);
test(
'running session elapsed time survives repository reconstruction',
() async {
final persistedAt = DateTime.utc(2026, 7, 17, 12);
final session = ActiveWorkoutSession(
metadata: _metadata('session-1', persistedAt),
sourceWorkoutTemplateId: null,
status: ActiveWorkoutStatus.running,
startedAt: persistedAt.subtract(const Duration(seconds: 30)),
lastPersistedAt: persistedAt,
elapsedActiveMs: 30000,
currentProgramIndex: 0,
currentExerciseIndex: 0,
currentSetIndex: 0,
resolvedTemplateSnapshotJson: _resolvedSnapshot(),
);
await activeRepository.save(session);
final afterAppKillRepository = local.DriftActiveSessionRepository(
database,
);
final afterAppKillUseCases = ActiveWorkoutSessionUseCases(
sessionRepository: afterAppKillRepository,
templateRepository: templateRepository,
clock: _FakeClock(persistedAt.add(const Duration(seconds: 20))),
ids: _FakeIds(),
originDeviceId: 'device-1',
);
final restored = await afterAppKillRepository.findOpen();
expect(restored, isNotNull);
expect(afterAppKillUseCases.elapsedActiveMilliseconds(restored!), 50000);
},
);
test(
'closing a session stores autonomous history rows with set snapshots',
() async {
final now = DateTime.utc(2026, 7, 17, 12);
final template = WorkoutTemplate(
metadata: _metadata('template-1', now),
name: 'Séance jambes',
);
await templateRepository.save(template);
final session = ActiveWorkoutSession(
metadata: _metadata('session-1', now),
sourceWorkoutTemplateId: template.metadata.id,
status: ActiveWorkoutStatus.running,
startedAt: now.subtract(const Duration(minutes: 10)),
lastPersistedAt: now,
elapsedActiveMs: 600000,
currentProgramIndex: 0,
currentExerciseIndex: 0,
currentSetIndex: 0,
resolvedTemplateSnapshotJson: _resolvedSnapshot(),
);
await activeRepository.save(session);
await activeRepository.saveSetResult(
ActiveSetResult(
metadata: _metadata('active-result-1', now),
activeWorkoutSessionId: session.metadata.id,
programSnapshotId: 'program-snapshot-1',
exerciseSnapshotId: 'exercise-snapshot-1',
programIndex: 0,
exerciseIndex: 0,
setIndex: 0,
actualTimeMs: 45000,
actualReps: 10,
actualScore: 80,
scoreLabelSnapshot: 'Charge',
scoreUnitSnapshot: 'kg',
completedAt: now,
),
);
final closeUseCase = CloseWorkoutSessionUseCase(
sessionRepository: activeRepository,
historyRepository: historyRepository,
clock: _FakeClock(now.add(const Duration(seconds: 5))),
ids: _FakeIds(),
originDeviceId: 'device-1',
);
final history = await closeUseCase.close(
sessionId: session.metadata.id,
nameSnapshot: 'Séance jambes',
completed: true,
);
await templateRepository.save(
WorkoutTemplate(
metadata: template.metadata.markDeleted(now),
name: template.name,
),
);
final restored = await historyRepository.findById(history.metadata.id);
expect(restored, isNotNull);
expect(restored!.nameSnapshot, 'Séance jambes');
expect(restored.results, hasLength(1));
expect(restored.results.single.programNameSnapshot, 'Programme jambes');
expect(restored.results.single.exerciseNameSnapshot, 'Squat');
expect(restored.results.single.actualTimeMs, 45000);
expect(restored.results.single.actualReps, 10);
expect(restored.results.single.actualScore, 80);
expect(restored.results.single.scoreUnitSnapshot, 'kg');
},
);
}
String _resolvedSnapshot() {
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,
'targetScore': 80,
'scoreLabelSnapshot': 'Charge',
'scoreUnitSnapshot': 'kg',
'restSecondsOverride': 0,
},
],
}),
},
],
'overrides': const [],
});
}
EntityMetadata _metadata(String id, DateTime now, [int localRevision = 0]) {
return EntityMetadata(
id: id,
createdAt: now,
updatedAt: now,
originDeviceId: 'device-1',
localRevision: localRevision,
);
}
ProgramExercise _programExercise(
String id,
DateTime now, {
String programId = 'program-1',
required int position,
bool autoStartNextTimedStepSnapshot = true,
bool? autoStartNextTimedStepOverride,
}) {
return ProgramExercise(
metadata: _metadata(id, now),
programId: programId,
position: position,
exerciseNameSnapshot: 'Exercise $position',
autoStartNextTimedStepSnapshot: autoStartNextTimedStepSnapshot,
autoStartNextTimedStepOverride: autoStartNextTimedStepOverride,
availableTimeSnapshot: false,
availableRepsSnapshot: true,
availableScoreSnapshot: false,
setsCount: 1,
timeEnabled: false,
repsEnabled: true,
scoreEnabled: false,
);
}
ExerciseStep _exerciseStep({
required String id,
required int position,
required String name,
required ExerciseStepType type,
required int defaultTargetValue,
bool hasScore = false,
ScoreInputMode scoreInputMode = ScoreInputMode.manual,
String? scoreLabel,
String? scoreUnit,
double? defaultTargetScore,
int? defaultTargetScoreTimeMs,
}) {
return ExerciseStep(
id: id,
position: position,
name: name,
type: type,
defaultTargetValue: defaultTargetValue,
hasScore: hasScore,
scoreInputMode: scoreInputMode,
scoreLabel: scoreLabel,
scoreUnit: scoreUnit,
defaultTargetScore: defaultTargetScore,
defaultTargetScoreTimeMs: defaultTargetScoreTimeMs,
);
}
void _expectExerciseEquals(Exercise actual, Exercise expected) {
_expectMetadataEquals(actual.metadata, expected.metadata);
expect(actual.name, expected.name);
expect(actual.description, expected.description);
expect(actual.imageMediaIds, expected.imageMediaIds);
expect(actual.iconMediaId, expected.iconMediaId);
expect(actual.videoMediaId, expected.videoMediaId);
expect(actual.hasTimeMeasure, expected.hasTimeMeasure);
expect(actual.hasRepsMeasure, expected.hasRepsMeasure);
expect(actual.hasScoreMeasure, expected.hasScoreMeasure);
expect(actual.scoreInputMode, expected.scoreInputMode);
expect(actual.scoreLabel, expected.scoreLabel);
expect(actual.scoreUnit, expected.scoreUnit);
expect(actual.defaultTargetTimeSeconds, expected.defaultTargetTimeSeconds);
expect(actual.defaultTargetReps, expected.defaultTargetReps);
expect(actual.defaultTargetScore, expected.defaultTargetScore);
expect(actual.defaultTargetScoreTimeMs, expected.defaultTargetScoreTimeMs);
expect(actual.autoStartNextTimedStep, expected.autoStartNextTimedStep);
expect(actual.archivedAt?.toUtc(), expected.archivedAt?.toUtc());
expect(actual.steps, hasLength(expected.steps.length));
for (var index = 0; index < expected.steps.length; index++) {
_expectExerciseStepEquals(actual.steps[index], expected.steps[index]);
}
}
void _expectExerciseStepEquals(ExerciseStep actual, ExerciseStep expected) {
expect(actual.id, expected.id);
expect(actual.position, expected.position);
expect(actual.name, expected.name);
expect(actual.type, expected.type);
expect(actual.defaultTargetValue, expected.defaultTargetValue);
expect(actual.hasScore, expected.hasScore);
expect(actual.scoreInputMode, expected.scoreInputMode);
expect(actual.scoreLabel, expected.scoreLabel);
expect(actual.scoreUnit, expected.scoreUnit);
expect(actual.defaultTargetScore, expected.defaultTargetScore);
expect(actual.defaultTargetScoreTimeMs, expected.defaultTargetScoreTimeMs);
}
void _expectMetadataEquals(EntityMetadata actual, EntityMetadata expected) {
expect(actual.id, expected.id);
expect(actual.createdAt.toUtc(), expected.createdAt.toUtc());
expect(actual.updatedAt.toUtc(), expected.updatedAt.toUtc());
expect(actual.deletedAt?.toUtc(), expected.deletedAt?.toUtc());
expect(actual.schemaVersion, expected.schemaVersion);
expect(actual.syncState, expected.syncState);
expect(actual.localRevision, expected.localRevision);
expect(actual.originDeviceId, expected.originDeviceId);
expect(actual.futureOwnerProfileId, expected.futureOwnerProfileId);
expect(actual.lastSyncedAt?.toUtc(), expected.lastSyncedAt?.toUtc());
expect(actual.remoteRevision, expected.remoteRevision);
}
final class _ExerciseRoundTripCase {
const _ExerciseRoundTripCase({required this.label, required this.exercise});
final String label;
final Exercise exercise;
}
final class _FakeClock implements Clock {
const _FakeClock(this.value);
final DateTime value;
@override
DateTime now() => value;
}
final class _FakeIds implements IdGenerator {
var _next = 0;
@override
String newId() {
_next += 1;
return 'id-$_next';
}
}