feat(data): édition ponctuelle des séries en séance (ticket #21)
Étend le modèle Drift (tables.dart, app_database.dart/.g.dart) et la couche application (entités, use cases, repositories) pour supporter l'édition ponctuelle des séries pendant l'exécution d'une séance, sans modifier le modèle/séance-modèle. build_runner OK, flutter analyze propre, 30/30 tests verts, build APK debug validé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -209,6 +209,123 @@ void main() {
|
||||
|
||||
expect(active?.metadata.id, 'latest-active');
|
||||
});
|
||||
|
||||
test('upsertSetResultAtPosition does not move session cursor', () async {
|
||||
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||||
final session = _sessionWithSnapshot(
|
||||
currentProgramIndex: 0,
|
||||
currentExerciseIndex: 0,
|
||||
currentSetIndex: 1,
|
||||
);
|
||||
final repository = _FakeActiveSessionRepository()..session = session;
|
||||
final useCase = _activeUseCase(repository, clock);
|
||||
|
||||
await useCase.upsertSetResultAtPosition(
|
||||
sessionId: session.metadata.id,
|
||||
programIndex: 0,
|
||||
exerciseIndex: 0,
|
||||
setIndex: 0,
|
||||
status: SetResultStatus.completed,
|
||||
actualReps: 12,
|
||||
);
|
||||
|
||||
expect(repository.session?.currentProgramIndex, 0);
|
||||
expect(repository.session?.currentExerciseIndex, 0);
|
||||
expect(repository.session?.currentSetIndex, 1);
|
||||
expect(repository.results.single.actualReps, 12);
|
||||
});
|
||||
|
||||
test(
|
||||
'upsertSetResultAtPosition preserves existing id and createdAt',
|
||||
() async {
|
||||
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||||
final session = _sessionWithSnapshot(
|
||||
currentProgramIndex: 0,
|
||||
currentExerciseIndex: 0,
|
||||
currentSetIndex: 1,
|
||||
);
|
||||
final existingCreatedAt = DateTime.utc(2026, 7, 17, 10);
|
||||
final existing = ActiveSetResult(
|
||||
metadata: EntityMetadata(
|
||||
id: 'result-1',
|
||||
createdAt: existingCreatedAt,
|
||||
updatedAt: existingCreatedAt,
|
||||
originDeviceId: 'device-1',
|
||||
),
|
||||
activeWorkoutSessionId: session.metadata.id,
|
||||
programSnapshotId: 'program-snapshot-1',
|
||||
exerciseSnapshotId: 'exercise-snapshot-1',
|
||||
programIndex: 0,
|
||||
exerciseIndex: 0,
|
||||
setIndex: 0,
|
||||
actualReps: 8,
|
||||
);
|
||||
final repository = _FakeActiveSessionRepository()
|
||||
..session = session
|
||||
..results.add(existing);
|
||||
final useCase = _activeUseCase(repository, clock);
|
||||
|
||||
final updated = await useCase.upsertSetResultAtPosition(
|
||||
sessionId: session.metadata.id,
|
||||
programIndex: 0,
|
||||
exerciseIndex: 0,
|
||||
setIndex: 0,
|
||||
status: SetResultStatus.completed,
|
||||
actualReps: 12,
|
||||
);
|
||||
|
||||
expect(updated.metadata.id, 'result-1');
|
||||
expect(updated.metadata.createdAt, existingCreatedAt);
|
||||
expect(updated.actualReps, 12);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'listSetResults returns pending, skipped and completed statuses',
|
||||
() async {
|
||||
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||||
final session = _sessionWithSnapshot(
|
||||
currentProgramIndex: 0,
|
||||
currentExerciseIndex: 0,
|
||||
currentSetIndex: 2,
|
||||
setsCount: 3,
|
||||
);
|
||||
final repository = _FakeActiveSessionRepository()
|
||||
..session = session
|
||||
..results.addAll([
|
||||
ActiveSetResult(
|
||||
metadata: _metadata('completed'),
|
||||
activeWorkoutSessionId: session.metadata.id,
|
||||
programSnapshotId: 'program-snapshot-1',
|
||||
exerciseSnapshotId: 'exercise-snapshot-1',
|
||||
programIndex: 0,
|
||||
exerciseIndex: 0,
|
||||
setIndex: 0,
|
||||
actualReps: 10,
|
||||
status: SetResultStatus.completed,
|
||||
),
|
||||
ActiveSetResult(
|
||||
metadata: _metadata('skipped'),
|
||||
activeWorkoutSessionId: session.metadata.id,
|
||||
programSnapshotId: 'program-snapshot-1',
|
||||
exerciseSnapshotId: 'exercise-snapshot-1',
|
||||
programIndex: 0,
|
||||
exerciseIndex: 0,
|
||||
setIndex: 1,
|
||||
status: SetResultStatus.skipped,
|
||||
),
|
||||
]);
|
||||
final useCase = _activeUseCase(repository, clock);
|
||||
|
||||
final states = await useCase.listSetResults(session.metadata.id);
|
||||
|
||||
expect(states.map((state) => state.status), [
|
||||
SetPositionStatus.completed,
|
||||
SetPositionStatus.skipped,
|
||||
SetPositionStatus.pending,
|
||||
]);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
EntityMetadata _metadata(String id) {
|
||||
@ -294,13 +411,17 @@ ActiveWorkoutSessionUseCases _activeUseCase(
|
||||
}
|
||||
|
||||
final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||||
ActiveWorkoutSession? session;
|
||||
final results = <ActiveSetResult>[];
|
||||
final restStates = <String, ActiveRestState>{};
|
||||
|
||||
@override
|
||||
Future<ActiveWorkoutSession?> findById(String id) async => null;
|
||||
Future<ActiveWorkoutSession?> findById(String id) async {
|
||||
return session?.metadata.id == id ? session : null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ActiveWorkoutSession?> findOpen() async => null;
|
||||
Future<ActiveWorkoutSession?> findOpen() async => session;
|
||||
|
||||
@override
|
||||
Future<ActiveRestState?> findRestStateById(String id) async => restStates[id];
|
||||
@ -313,11 +434,16 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ActiveSetResult>> listSetResults(String sessionId) async =>
|
||||
const [];
|
||||
Future<List<ActiveSetResult>> listSetResults(String sessionId) async {
|
||||
return results
|
||||
.where((result) => result.activeWorkoutSessionId == sessionId)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> save(ActiveWorkoutSession session) async {}
|
||||
Future<void> save(ActiveWorkoutSession session) async {
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> saveRestState(ActiveRestState restState) async {
|
||||
@ -325,5 +451,53 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> saveSetResult(ActiveSetResult result) async {}
|
||||
Future<void> saveSetResult(ActiveSetResult result) async {
|
||||
results.removeWhere(
|
||||
(existing) =>
|
||||
existing.activeWorkoutSessionId == result.activeWorkoutSessionId &&
|
||||
existing.programIndex == result.programIndex &&
|
||||
existing.exerciseIndex == result.exerciseIndex &&
|
||||
existing.setIndex == result.setIndex,
|
||||
);
|
||||
results.add(result);
|
||||
}
|
||||
}
|
||||
|
||||
ActiveWorkoutSession _sessionWithSnapshot({
|
||||
required int currentProgramIndex,
|
||||
required int currentExerciseIndex,
|
||||
required int currentSetIndex,
|
||||
int setsCount = 2,
|
||||
}) {
|
||||
return ActiveWorkoutSession(
|
||||
metadata: _metadata('session-1'),
|
||||
status: ActiveWorkoutStatus.running,
|
||||
startedAt: DateTime.utc(2026, 7, 17, 12),
|
||||
lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
|
||||
elapsedActiveMs: 0,
|
||||
currentProgramIndex: currentProgramIndex,
|
||||
currentExerciseIndex: currentExerciseIndex,
|
||||
currentSetIndex: currentSetIndex,
|
||||
resolvedTemplateSnapshotJson: jsonEncode({
|
||||
'programs': [
|
||||
{
|
||||
'id': 'program-snapshot-1',
|
||||
'programNameSnapshot': 'Programme',
|
||||
'programSnapshotJson': jsonEncode({
|
||||
'exercises': [
|
||||
{
|
||||
'id': 'exercise-snapshot-1',
|
||||
'exerciseNameSnapshot': 'Squat',
|
||||
'setsCount': setsCount,
|
||||
'timeEnabled': false,
|
||||
'repsEnabled': true,
|
||||
'scoreEnabled': false,
|
||||
'targetReps': 10,
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user