Files
GameTime/test/application/use_cases_test.dart
Blomios 9f9d0dbe3b fix(persistence): correctifs post-QA sur les repositories (ticket #13)
Corrige les ports, use cases, entités et l'implémentation Drift des
repositories suite aux retours de QA. flutter analyze propre, 24/24
tests verts, build APK debug validé.

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

330 lines
9.8 KiB
Dart

import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:gametime/application/application.dart';
import 'package:gametime/domain/domain.dart';
void main() {
test('Exercise requires at least one active measure', () {
expect(
() => Exercise(
metadata: _metadata('exercise-1'),
name: 'Push up',
hasTimeMeasure: false,
hasRepsMeasure: false,
hasScoreMeasure: false,
),
throwsA(isA<DomainException>()),
);
});
test('Workout template use case rejects active measure overrides', () async {
final useCase = WorkoutTemplateUseCases(
templateRepository: _FakeWorkoutTemplateRepository(),
programRepository: _FakeProgramRepository(),
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
ids: _FakeIds(),
originDeviceId: 'device-1',
);
await expectLater(
useCase.overrideExerciseTargets(
workoutTemplateProgramId: 'template-program-1',
snapshotProgramExerciseId: 'exercise-snapshot-1',
attemptedMeasureOverride: {WorkoutMeasure.reps},
),
throwsA(isA<DomainException>()),
);
});
test(
'Workout template use case rejects target overrides for inactive measures',
() async {
final useCase = WorkoutTemplateUseCases(
templateRepository: _FakeWorkoutTemplateRepository(),
programRepository: _FakeProgramRepository(),
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
ids: _FakeIds(),
originDeviceId: 'device-1',
);
await expectLater(
useCase.saveConfigured(
name: 'Séance tir',
programs: [
WorkoutTemplateProgramConfig(
clientKey: 'program-1',
programNameSnapshot: 'Programme tir',
defaultRestSecondsSnapshot: 30,
programSnapshotJson: jsonEncode({
'exercises': [
{
'id': 'exercise-snapshot-1',
'exerciseNameSnapshot': 'Lancers francs',
'setsCount': 3,
'timeEnabled': true,
'repsEnabled': false,
'scoreEnabled': false,
'targetTimeSeconds': 60,
'targetReps': null,
'targetScore': null,
},
],
}),
),
],
overrides: const [
WorkoutTemplateExerciseOverrideConfig(
workoutTemplateProgramClientKey: 'program-1',
snapshotProgramExerciseId: 'exercise-snapshot-1',
targetRepsOverride: 10,
),
],
),
throwsA(isA<DomainException>()),
);
},
);
test('Active session elapsed time is based on persisted timestamps', () {
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
final session = ActiveWorkoutSession(
metadata: _metadata('session-1'),
status: ActiveWorkoutStatus.running,
startedAt: clock.now(),
lastPersistedAt: clock.now(),
elapsedActiveMs: 0,
currentProgramIndex: 0,
currentExerciseIndex: 0,
currentSetIndex: 0,
resolvedTemplateSnapshotJson: '{}',
);
clock.value = clock.value.add(const Duration(seconds: 30));
final paused = session.pause(clock.now());
expect(paused.elapsedActiveMs, 30000);
clock.value = clock.value.add(const Duration(minutes: 5));
final resumed = paused.resume(clock.now());
expect(resumed.elapsedActiveMillisecondsAt(clock.now()), 30000);
clock.value = clock.value.add(const Duration(seconds: 15));
expect(resumed.elapsedActiveMillisecondsAt(clock.now()), 45000);
});
test('adjustRestSeconds persists adjusted rest duration', () async {
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
final repository = _FakeActiveSessionRepository()
..restStates['rest-1'] = ActiveRestState(
metadata: _metadata('rest-1'),
activeWorkoutSessionId: 'session-1',
afterProgramIndex: 0,
afterExerciseIndex: 0,
afterSetIndex: 0,
plannedRestSeconds: 60,
adjustedRestSeconds: 60,
startedAt: clock.now(),
);
final useCase = _activeUseCase(repository, clock);
final adjusted = await useCase.adjustRestSeconds(
restStateId: 'rest-1',
deltaSeconds: -75,
);
expect(adjusted.adjustedRestSeconds, 0);
expect(repository.restStates['rest-1']!.adjustedRestSeconds, 0);
});
test('skipRest persists skippedAt timestamp', () async {
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
final repository = _FakeActiveSessionRepository()
..restStates['rest-1'] = ActiveRestState(
metadata: _metadata('rest-1'),
activeWorkoutSessionId: 'session-1',
afterProgramIndex: 0,
afterExerciseIndex: 0,
afterSetIndex: 0,
plannedRestSeconds: 60,
adjustedRestSeconds: 60,
startedAt: clock.now(),
);
clock.value = clock.value.add(const Duration(seconds: 10));
final useCase = _activeUseCase(repository, clock);
final skipped = await useCase.skipRest(restStateId: 'rest-1');
expect(skipped.skippedAt, clock.now());
expect(repository.restStates['rest-1']!.skippedAt, clock.now());
});
test('findActiveRest returns latest non-ended non-skipped rest', () async {
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
final repository = _FakeActiveSessionRepository()
..restStates['ended'] = ActiveRestState(
metadata: _metadata('ended'),
activeWorkoutSessionId: 'session-1',
afterProgramIndex: 0,
afterExerciseIndex: 0,
afterSetIndex: 0,
plannedRestSeconds: 60,
adjustedRestSeconds: 60,
startedAt: clock.now(),
endedAt: clock.now().add(const Duration(seconds: 60)),
)
..restStates['older-active'] = ActiveRestState(
metadata: _metadata('older-active'),
activeWorkoutSessionId: 'session-1',
afterProgramIndex: 0,
afterExerciseIndex: 1,
afterSetIndex: 0,
plannedRestSeconds: 60,
adjustedRestSeconds: 60,
startedAt: clock.now().add(const Duration(seconds: 30)),
)
..restStates['latest-active'] = ActiveRestState(
metadata: _metadata('latest-active'),
activeWorkoutSessionId: 'session-1',
afterProgramIndex: 0,
afterExerciseIndex: 2,
afterSetIndex: 0,
plannedRestSeconds: 60,
adjustedRestSeconds: 60,
startedAt: clock.now().add(const Duration(seconds: 90)),
)
..restStates['skipped'] = ActiveRestState(
metadata: _metadata('skipped'),
activeWorkoutSessionId: 'session-1',
afterProgramIndex: 0,
afterExerciseIndex: 3,
afterSetIndex: 0,
plannedRestSeconds: 60,
adjustedRestSeconds: 60,
startedAt: clock.now().add(const Duration(seconds: 120)),
skippedAt: clock.now().add(const Duration(seconds: 125)),
);
final useCase = _activeUseCase(repository, clock);
final active = await useCase.findActiveRest(sessionId: 'session-1');
expect(active?.metadata.id, 'latest-active');
});
}
EntityMetadata _metadata(String id) {
return EntityMetadata(
id: id,
createdAt: DateTime.utc(2026, 7, 17),
updatedAt: DateTime.utc(2026, 7, 17),
originDeviceId: 'device-1',
);
}
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 _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> 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 {}
}
ActiveWorkoutSessionUseCases _activeUseCase(
_FakeActiveSessionRepository repository,
_FakeClock clock,
) {
return ActiveWorkoutSessionUseCases(
sessionRepository: repository,
templateRepository: _FakeWorkoutTemplateRepository(),
clock: clock,
ids: _FakeIds(),
originDeviceId: 'device-1',
);
}
final class _FakeActiveSessionRepository implements ActiveSessionRepository {
final restStates = <String, ActiveRestState>{};
@override
Future<ActiveWorkoutSession?> findById(String id) async => null;
@override
Future<ActiveWorkoutSession?> findOpen() async => null;
@override
Future<ActiveRestState?> findRestStateById(String id) async => restStates[id];
@override
Future<List<ActiveRestState>> listRestStates(String sessionId) async {
return restStates.values
.where((rest) => rest.activeWorkoutSessionId == sessionId)
.toList();
}
@override
Future<List<ActiveSetResult>> listSetResults(String sessionId) async =>
const [];
@override
Future<void> save(ActiveWorkoutSession session) async {}
@override
Future<void> saveRestState(ActiveRestState restState) async {
restStates[restState.metadata.id] = restState;
}
@override
Future<void> saveSetResult(ActiveSetResult result) async {}
}