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>
330 lines
9.8 KiB
Dart
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 {}
|
|
}
|