feat(domain): fondation domain du score chronométré (ticket #25)
Étend le modèle Drift (tables.dart, app_database.dart/.g.dart), les entités du domaine, les ports et use cases pour poser les fondations du score chronométré. build_runner OK, flutter analyze propre, 39/39 tests verts, build APK debug validé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -326,6 +326,124 @@ void main() {
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'score stopwatch start stop resume accumulates elapsed duration',
|
||||
() async {
|
||||
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||||
final session = _sessionWithSnapshot(
|
||||
currentProgramIndex: 0,
|
||||
currentExerciseIndex: 0,
|
||||
currentSetIndex: 0,
|
||||
);
|
||||
final repository = _FakeActiveSessionRepository()..session = session;
|
||||
final useCase = _activeUseCase(repository, clock);
|
||||
|
||||
await useCase.startScoreStopwatch(
|
||||
sessionId: session.metadata.id,
|
||||
programIndex: 0,
|
||||
exerciseIndex: 0,
|
||||
setIndex: 0,
|
||||
);
|
||||
clock.value = clock.value.add(const Duration(seconds: 5));
|
||||
final stopped = await useCase.stopScoreStopwatch(
|
||||
sessionId: session.metadata.id,
|
||||
programIndex: 0,
|
||||
exerciseIndex: 0,
|
||||
setIndex: 0,
|
||||
);
|
||||
clock.value = clock.value.add(const Duration(seconds: 10));
|
||||
await useCase.resumeScoreStopwatch(
|
||||
sessionId: session.metadata.id,
|
||||
programIndex: 0,
|
||||
exerciseIndex: 0,
|
||||
setIndex: 0,
|
||||
);
|
||||
clock.value = clock.value.add(const Duration(seconds: 3));
|
||||
final stoppedAgain = await useCase.stopScoreStopwatch(
|
||||
sessionId: session.metadata.id,
|
||||
programIndex: 0,
|
||||
exerciseIndex: 0,
|
||||
setIndex: 0,
|
||||
);
|
||||
|
||||
expect(stopped.accumulatedMs, 5000);
|
||||
expect(stoppedAgain.accumulatedMs, 8000);
|
||||
},
|
||||
);
|
||||
|
||||
test('score stopwatch state survives use case reconstruction', () async {
|
||||
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||||
final session = _sessionWithSnapshot(
|
||||
currentProgramIndex: 0,
|
||||
currentExerciseIndex: 0,
|
||||
currentSetIndex: 0,
|
||||
);
|
||||
final repository = _FakeActiveSessionRepository()..session = session;
|
||||
|
||||
await _activeUseCase(repository, clock).startScoreStopwatch(
|
||||
sessionId: session.metadata.id,
|
||||
programIndex: 0,
|
||||
exerciseIndex: 0,
|
||||
setIndex: 0,
|
||||
);
|
||||
clock.value = clock.value.add(const Duration(seconds: 4));
|
||||
|
||||
final reconstructed = _activeUseCase(repository, clock);
|
||||
final stopped = await reconstructed.stopScoreStopwatch(
|
||||
sessionId: session.metadata.id,
|
||||
programIndex: 0,
|
||||
exerciseIndex: 0,
|
||||
setIndex: 0,
|
||||
);
|
||||
|
||||
expect(stopped.accumulatedMs, 4000);
|
||||
});
|
||||
|
||||
test('score result enforces manual xor stopwatch values', () {
|
||||
expect(
|
||||
() => ActiveSetResult(
|
||||
metadata: _metadata('result-1'),
|
||||
activeWorkoutSessionId: 'session-1',
|
||||
programSnapshotId: 'program-1',
|
||||
exerciseSnapshotId: 'exercise-1',
|
||||
programIndex: 0,
|
||||
exerciseIndex: 0,
|
||||
setIndex: 0,
|
||||
actualScore: 12,
|
||||
actualScoreTimeMs: 12000,
|
||||
),
|
||||
throwsA(isA<DomainException>()),
|
||||
);
|
||||
expect(
|
||||
() => ActiveSetResult(
|
||||
metadata: _metadata('result-2'),
|
||||
activeWorkoutSessionId: 'session-1',
|
||||
programSnapshotId: 'program-1',
|
||||
exerciseSnapshotId: 'exercise-1',
|
||||
programIndex: 0,
|
||||
exerciseIndex: 0,
|
||||
setIndex: 0,
|
||||
actualScore: 12,
|
||||
scoreInputModeSnapshot: ScoreInputMode.stopwatch,
|
||||
),
|
||||
throwsA(isA<DomainException>()),
|
||||
);
|
||||
expect(
|
||||
() => ActiveSetResult(
|
||||
metadata: _metadata('result-3'),
|
||||
activeWorkoutSessionId: 'session-1',
|
||||
programSnapshotId: 'program-1',
|
||||
exerciseSnapshotId: 'exercise-1',
|
||||
programIndex: 0,
|
||||
exerciseIndex: 0,
|
||||
setIndex: 0,
|
||||
actualScoreTimeMs: 12000,
|
||||
scoreInputModeSnapshot: ScoreInputMode.manual,
|
||||
),
|
||||
throwsA(isA<DomainException>()),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
EntityMetadata _metadata(String id) {
|
||||
@ -414,6 +532,7 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||||
ActiveWorkoutSession? session;
|
||||
final results = <ActiveSetResult>[];
|
||||
final restStates = <String, ActiveRestState>{};
|
||||
final scoreStopwatchStates = <String, ActiveScoreStopwatchState>{};
|
||||
|
||||
@override
|
||||
Future<ActiveWorkoutSession?> findById(String id) async {
|
||||
@ -426,6 +545,25 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||||
@override
|
||||
Future<ActiveRestState?> findRestStateById(String id) async => restStates[id];
|
||||
|
||||
@override
|
||||
Future<ActiveScoreStopwatchState?> findScoreStopwatchState({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
}) async {
|
||||
return scoreStopwatchStates.values
|
||||
.where(
|
||||
(state) =>
|
||||
state.activeWorkoutSessionId == sessionId &&
|
||||
state.programIndex == programIndex &&
|
||||
state.exerciseIndex == exerciseIndex &&
|
||||
state.setIndex == setIndex &&
|
||||
state.metadata.deletedAt == null,
|
||||
)
|
||||
.firstOrNull;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ActiveRestState>> listRestStates(String sessionId) async {
|
||||
return restStates.values
|
||||
@ -433,6 +571,19 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ActiveScoreStopwatchState>> listScoreStopwatchStates(
|
||||
String sessionId,
|
||||
) async {
|
||||
return scoreStopwatchStates.values
|
||||
.where(
|
||||
(state) =>
|
||||
state.activeWorkoutSessionId == sessionId &&
|
||||
state.metadata.deletedAt == null,
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ActiveSetResult>> listSetResults(String sessionId) async {
|
||||
return results
|
||||
@ -450,6 +601,31 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||||
restStates[restState.metadata.id] = restState;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> saveScoreStopwatchState(ActiveScoreStopwatchState state) async {
|
||||
scoreStopwatchStates[state.metadata.id] = state;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteScoreStopwatchState({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
required DateTime deletedAt,
|
||||
}) async {
|
||||
final state = await findScoreStopwatchState(
|
||||
sessionId: sessionId,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
);
|
||||
if (state == null) {
|
||||
return;
|
||||
}
|
||||
scoreStopwatchStates.remove(state.metadata.id);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> saveSetResult(ActiveSetResult result) async {
|
||||
results.removeWhere(
|
||||
|
||||
@ -287,11 +287,26 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||||
@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<List<ActiveRestState>> listRestStates(String sessionId) async {
|
||||
return const [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ActiveScoreStopwatchState>> listScoreStopwatchStates(
|
||||
String sessionId,
|
||||
) async {
|
||||
return const [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ActiveSetResult>> listSetResults(String sessionId) async {
|
||||
return const [];
|
||||
@ -303,6 +318,18 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||||
@override
|
||||
Future<void> saveRestState(ActiveRestState restState) async {}
|
||||
|
||||
@override
|
||||
Future<void> saveScoreStopwatchState(ActiveScoreStopwatchState state) async {}
|
||||
|
||||
@override
|
||||
Future<void> deleteScoreStopwatchState({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
required DateTime deletedAt,
|
||||
}) async {}
|
||||
|
||||
@override
|
||||
Future<void> saveSetResult(ActiveSetResult result) async {}
|
||||
}
|
||||
|
||||
@ -545,6 +545,7 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||||
ActiveWorkoutSession? session;
|
||||
final results = <ActiveSetResult>[];
|
||||
final restStates = <ActiveRestState>[];
|
||||
final scoreStopwatchStates = <ActiveScoreStopwatchState>[];
|
||||
|
||||
@override
|
||||
Future<ActiveWorkoutSession?> findById(String id) async {
|
||||
@ -564,6 +565,25 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ActiveScoreStopwatchState?> findScoreStopwatchState({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
}) async {
|
||||
for (final state in scoreStopwatchStates) {
|
||||
if (state.activeWorkoutSessionId == sessionId &&
|
||||
state.programIndex == programIndex &&
|
||||
state.exerciseIndex == exerciseIndex &&
|
||||
state.setIndex == setIndex &&
|
||||
state.metadata.deletedAt == null) {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ActiveRestState>> listRestStates(String sessionId) async {
|
||||
return restStates
|
||||
@ -571,6 +591,15 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ActiveScoreStopwatchState>> listScoreStopwatchStates(
|
||||
String sessionId,
|
||||
) async {
|
||||
return scoreStopwatchStates
|
||||
.where((state) => state.activeWorkoutSessionId == sessionId)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ActiveSetResult>> listSetResults(String sessionId) async {
|
||||
return results;
|
||||
@ -593,6 +622,27 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> saveScoreStopwatchState(ActiveScoreStopwatchState state) async {
|
||||
final index = scoreStopwatchStates.indexWhere(
|
||||
(saved) => saved.metadata.id == state.metadata.id,
|
||||
);
|
||||
if (index == -1) {
|
||||
scoreStopwatchStates.add(state);
|
||||
} else {
|
||||
scoreStopwatchStates[index] = state;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteScoreStopwatchState({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
required DateTime deletedAt,
|
||||
}) async {}
|
||||
|
||||
@override
|
||||
Future<void> saveSetResult(ActiveSetResult result) async {
|
||||
final index = results.indexWhere(
|
||||
|
||||
Reference in New Issue
Block a user