From ea1e240c87a03fb15535ad9c9d46cb85f4487297 Mon Sep 17 00:00:00 2001 From: Blomios Date: Mon, 20 Jul 2026 13:56:49 +0200 Subject: [PATCH] =?UTF-8?q?feat(exercice):=20r=C3=A9solution=20effective?= =?UTF-8?q?=20et=20auto-advance=20des=20chronos=20d'=C3=A9tapes=20(ticket?= =?UTF-8?q?=20#76)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajoute la résolution effective de l'auto-enchaînement (application/use_cases.dart) et son support dans l'adapter Drift (infrastructure/local/drift_repositories.dart). flutter pub get OK, dart format appliqué, analyze propre (mêmes infos préexistantes), tests verts, build APK debug validé. Co-Authored-By: Claude Opus 4.8 --- lib/application/use_cases.dart | 115 ++++- .../local/drift_repositories.dart | 3 +- test/application/use_cases_test.dart | 462 +++++++++++++++--- .../drift_repositories_test.dart | 185 +++---- 4 files changed, 596 insertions(+), 169 deletions(-) diff --git a/lib/application/use_cases.dart b/lib/application/use_cases.dart index 1edae8a..8852541 100644 --- a/lib/application/use_cases.dart +++ b/lib/application/use_cases.dart @@ -1214,10 +1214,8 @@ final class ProgramUseCases { exerciseImageMediaIdsSnapshot: input.exerciseImageMediaIdsSnapshot, exerciseVideoMediaIdSnapshot: input.exerciseVideoMediaIdSnapshot, exerciseStepsSnapshot: input.exerciseStepsSnapshot, - autoStartNextTimedStepSnapshot: - input.autoStartNextTimedStepSnapshot, - autoStartNextTimedStepOverride: - input.autoStartNextTimedStepOverride, + autoStartNextTimedStepSnapshot: input.autoStartNextTimedStepSnapshot, + autoStartNextTimedStepOverride: input.autoStartNextTimedStepOverride, exerciseArchivedSnapshot: input.exerciseArchivedSnapshot, availableTimeSnapshot: input.availableTimeSnapshot, availableRepsSnapshot: input.availableRepsSnapshot, @@ -1478,8 +1476,7 @@ final class WorkoutTemplateUseCases { targetRepsOverride: input.targetRepsOverride, targetScoreOverride: input.targetScoreOverride, targetScoreTimeMsOverride: input.targetScoreTimeMsOverride, - autoStartNextTimedStepOverride: - input.autoStartNextTimedStepOverride, + autoStartNextTimedStepOverride: input.autoStartNextTimedStepOverride, ), ); } @@ -2387,16 +2384,43 @@ final class ActiveExerciseStepUseCases { setIndex: setIndex, ); final now = clock.now(); - final state = await _autoAdvanceElapsedTimers( - context, - await _requiredStepProgressState( - sessionId: sessionId, - programIndex: programIndex, - exerciseIndex: exerciseIndex, - setIndex: setIndex, - ), - now, + final currentState = await _requiredStepProgressState( + sessionId: sessionId, + programIndex: programIndex, + exerciseIndex: exerciseIndex, + setIndex: setIndex, ); + final currentStep = _currentStep(context, currentState); + if (currentState.status == ActiveExerciseStepProgressStatus.runningTimer && + currentStep.type == ExerciseStepType.time) { + final targetMs = currentStep.defaultTargetValue * 1000; + final elapsedMs = currentState.elapsedMillisecondsAt(now); + if (elapsedMs >= targetMs) { + final completed = _stepResult( + context: context, + state: currentState, + step: currentStep, + status: SetResultStatus.completed, + now: now, + actualTimeMs: targetMs, + actualScore: actualScore, + actualScoreTimeMs: actualScoreTimeMs, + note: note, + ); + await sessionRepository.saveExerciseStepResult(completed); + await sessionRepository.saveExerciseStepProgressState( + _advanceState( + context, + currentState, + now, + runningTimerOverflowMs: elapsedMs - targetMs, + completedStep: currentStep, + ), + ); + return completed; + } + } + final state = await _autoAdvanceElapsedTimers(context, currentState, now); final step = _currentStep(context, state); if (state.status == ActiveExerciseStepProgressStatus.sequenceComplete) { throw const DomainException('Exercise step sequence is complete.'); @@ -2419,7 +2443,7 @@ final class ActiveExerciseStepUseCases { ); await sessionRepository.saveExerciseStepResult(completed); await sessionRepository.saveExerciseStepProgressState( - _advanceState(context, state, now), + _advanceState(context, state, now, completedStep: step), ); return completed; } @@ -2453,7 +2477,7 @@ final class ActiveExerciseStepUseCases { ); await sessionRepository.saveExerciseStepResult(skipped); await sessionRepository.saveExerciseStepProgressState( - _advanceState(context, state, now), + _advanceState(context, state, now, completedStep: step), ); return skipped; } @@ -2491,7 +2515,7 @@ final class ActiveExerciseStepUseCases { ); skipped.add(result); await sessionRepository.saveExerciseStepResult(result); - state = _advanceState(context, state, now); + state = _advanceState(context, state, now, completedStep: step); } await sessionRepository.saveExerciseStepProgressState(state); return skipped; @@ -2528,7 +2552,7 @@ final class ActiveExerciseStepUseCases { ); skipped.add(result); await sessionRepository.saveExerciseStepResult(result); - state = _advanceState(context, state, now); + state = _advanceState(context, state, now, completedStep: step); } await sessionRepository.saveExerciseStepProgressState(state); return skipped; @@ -2581,6 +2605,7 @@ final class ActiveExerciseStepUseCases { expectedPassages: snapshot.repsEnabled ? (snapshot.targetReps ?? 1).clamp(1, 1 << 31) : 1, + autoStartNextTimedStep: snapshot.autoStartNextTimedStepEffective, ); } @@ -2638,6 +2663,7 @@ final class ActiveExerciseStepUseCases { current, now, runningTimerOverflowMs: overflowMs, + completedStep: step, ); await sessionRepository.saveExerciseStepProgressState(current); } @@ -2649,6 +2675,7 @@ final class ActiveExerciseStepUseCases { ActiveExerciseStepProgressState state, DateTime now, { int runningTimerOverflowMs = 0, + ExerciseStep? completedStep, }) { var nextPassage = state.currentPassageIndex; var nextStep = state.currentStepIndex + 1; @@ -2666,8 +2693,15 @@ final class ActiveExerciseStepUseCases { ); } final step = context.steps[nextStep]; + final shouldStopBeforeNextTimedStep = + !context.autoStartNextTimedStep && + completedStep?.type == ExerciseStepType.time && + step.type == ExerciseStepType.time; + final resolvedOverflowMs = shouldStopBeforeNextTimedStep + ? 0 + : runningTimerOverflowMs; final status = step.type == ExerciseStepType.time - ? runningTimerOverflowMs > 0 + ? resolvedOverflowMs > 0 ? ActiveExerciseStepProgressStatus.runningTimer : ActiveExerciseStepProgressStatus.stoppedTimer : ActiveExerciseStepProgressStatus.waitingManual; @@ -2678,7 +2712,7 @@ final class ActiveExerciseStepUseCases { currentStepSnapshotId: step.id, status: status, startedAt: status == ActiveExerciseStepProgressStatus.runningTimer - ? now.subtract(Duration(milliseconds: runningTimerOverflowMs)) + ? now.subtract(Duration(milliseconds: resolvedOverflowMs)) : null, accumulatedMs: 0, lastTransitionAt: now, @@ -3320,6 +3354,15 @@ _ResolvedExerciseSnapshot? _findExerciseSnapshot({ } final exercise = exercises[exerciseIndex] as Map; final id = exercise['id'] as String? ?? 'exercise-$exerciseIndex'; + final autoStartNextTimedStepEffective = + _templateStepChainingOverride( + decoded, + workoutTemplateProgramId: programSnapshotId, + snapshotProgramExerciseId: id, + ) ?? + (exercise['autoStartNextTimedStepOverride'] as bool?) ?? + (exercise['autoStartNextTimedStepSnapshot'] as bool?) ?? + true; return _ResolvedExerciseSnapshot( programSnapshotId: programSnapshotId, exerciseSnapshotId: id, @@ -3338,9 +3381,31 @@ _ResolvedExerciseSnapshot? _findExerciseSnapshot({ scoreLabelSnapshot: exercise['scoreLabelSnapshot'] as String?, scoreUnitSnapshot: exercise['scoreUnitSnapshot'] as String?, steps: _exerciseStepsFromSnapshot(exercise['exerciseStepsSnapshot']), + autoStartNextTimedStepEffective: autoStartNextTimedStepEffective, ); } +bool? _templateStepChainingOverride( + Map resolvedTemplateSnapshot, { + required String workoutTemplateProgramId, + required String snapshotProgramExerciseId, +}) { + final overrides = + (resolvedTemplateSnapshot['overrides'] as List? ?? const []); + for (final raw in overrides) { + if (raw is! Map) { + continue; + } + final override = Map.from(raw); + if (override['workoutTemplateProgramId'] == workoutTemplateProgramId && + override['snapshotProgramExerciseId'] == snapshotProgramExerciseId) { + final value = override['autoStartNextTimedStepOverride']; + return value is bool ? value : null; + } + } + return null; +} + List<_SetPositionSnapshot> _listSetSnapshots( String resolvedTemplateSnapshotJson, ) { @@ -3422,6 +3487,7 @@ final class _StepSequenceContext { required this.setIndex, required this.steps, required this.expectedPassages, + required this.autoStartNextTimedStep, }); final String sessionId; @@ -3432,6 +3498,7 @@ final class _StepSequenceContext { final int setIndex; final List steps; final int expectedPassages; + final bool autoStartNextTimedStep; } List _historyResultsFromActiveResults({ @@ -3523,6 +3590,10 @@ Map _exerciseSnapshotsById( scoreLabelSnapshot: exercise['scoreLabelSnapshot'] as String?, scoreUnitSnapshot: exercise['scoreUnitSnapshot'] as String?, steps: _exerciseStepsFromSnapshot(exercise['exerciseStepsSnapshot']), + autoStartNextTimedStepEffective: + (exercise['autoStartNextTimedStepOverride'] as bool?) ?? + (exercise['autoStartNextTimedStepSnapshot'] as bool?) ?? + true, ); } } @@ -3546,6 +3617,7 @@ final class _ResolvedExerciseSnapshot { this.scoreLabelSnapshot, this.scoreUnitSnapshot, this.steps = const [], + this.autoStartNextTimedStepEffective = true, }); final String programSnapshotId; @@ -3563,6 +3635,7 @@ final class _ResolvedExerciseSnapshot { final String? scoreLabelSnapshot; final String? scoreUnitSnapshot; final List steps; + final bool autoStartNextTimedStepEffective; } List _exerciseStepsFromSnapshot(Object? value) { diff --git a/lib/infrastructure/local/drift_repositories.dart b/lib/infrastructure/local/drift_repositories.dart index 5085468..cd27ddf 100644 --- a/lib/infrastructure/local/drift_repositories.dart +++ b/lib/infrastructure/local/drift_repositories.dart @@ -3047,8 +3047,7 @@ domain.Exercise _exerciseFromPayload(RemoteSyncedItem item) { defaultTargetReps: payload['defaultTargetReps'] as int?, defaultTargetScore: (payload['defaultTargetScore'] as num?)?.toDouble(), defaultTargetScoreTimeMs: payload['defaultTargetScoreTimeMs'] as int?, - autoStartNextTimedStep: - payload['autoStartNextTimedStep'] as bool? ?? true, + autoStartNextTimedStep: payload['autoStartNextTimedStep'] as bool? ?? true, steps: _stepsFromPayload(payload['steps']), archivedAt: _dateTimeFromPayload(payload['archivedAt']), ); diff --git a/test/application/use_cases_test.dart b/test/application/use_cases_test.dart index 4bb6a3f..c70cda7 100644 --- a/test/application/use_cases_test.dart +++ b/test/application/use_cases_test.dart @@ -479,49 +479,52 @@ void main() { ); }); - test('program exercise snapshots step chaining setting from exercise', () async { - final exerciseRepository = _FakeExerciseRepository() - ..exercise = Exercise( - metadata: _metadata('exercise-1'), - name: 'Circuit', - hasTimeMeasure: false, - hasRepsMeasure: true, - hasScoreMeasure: false, - defaultTargetReps: 10, - autoStartNextTimedStep: false, + test( + 'program exercise snapshots step chaining setting from exercise', + () async { + final exerciseRepository = _FakeExerciseRepository() + ..exercise = Exercise( + metadata: _metadata('exercise-1'), + name: 'Circuit', + hasTimeMeasure: false, + hasRepsMeasure: true, + hasScoreMeasure: false, + defaultTargetReps: 10, + autoStartNextTimedStep: false, + ); + final programRepository = _FakeProgramRepository() + ..programs.add( + Program( + metadata: _metadata('program-1'), + name: 'Programme', + defaultRestSeconds: 60, + ), + ); + final useCase = ProgramUseCases( + programRepository: programRepository, + exerciseRepository: exerciseRepository, + templateRepository: _FakeWorkoutTemplateRepository(), + clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)), + ids: _FakeIds(), + originDeviceId: 'device-1', ); - final programRepository = _FakeProgramRepository() - ..programs.add( - Program( - metadata: _metadata('program-1'), - name: 'Programme', - defaultRestSeconds: 60, - ), + + final snapshot = await useCase.addExercise( + programId: 'program-1', + exerciseId: 'exercise-1', + position: 0, + setsCount: 3, + enabledMeasures: const {WorkoutMeasure.reps}, ); - final useCase = ProgramUseCases( - programRepository: programRepository, - exerciseRepository: exerciseRepository, - templateRepository: _FakeWorkoutTemplateRepository(), - clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)), - ids: _FakeIds(), - originDeviceId: 'device-1', - ); - final snapshot = await useCase.addExercise( - programId: 'program-1', - exerciseId: 'exercise-1', - position: 0, - setsCount: 3, - enabledMeasures: const {WorkoutMeasure.reps}, - ); - - expect(snapshot.autoStartNextTimedStepSnapshot, isFalse); - expect(snapshot.autoStartNextTimedStepOverride, isNull); - expect( - snapshot.toSnapshotJson()['autoStartNextTimedStepSnapshot'], - isFalse, - ); - }); + expect(snapshot.autoStartNextTimedStepSnapshot, isFalse); + expect(snapshot.autoStartNextTimedStepOverride, isNull); + expect( + snapshot.toSnapshotJson()['autoStartNextTimedStepSnapshot'], + isFalse, + ); + }, + ); test('program saveConfigured preserves step chaining override', () async { final programRepository = _FakeProgramRepository(); @@ -596,10 +599,7 @@ void main() { ], ); - expect( - template.overrides.single.autoStartNextTimedStepOverride, - isFalse, - ); + expect(template.overrides.single.autoStartNextTimedStepOverride, isFalse); }); test('delete exercise keeps existing program snapshots unchanged', () async { @@ -1230,6 +1230,318 @@ void main() { }, ); + test( + 'step chaining resolution uses template override before program override', + () async { + final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); + final session = _sessionWithSnapshot( + currentProgramIndex: 0, + currentExerciseIndex: 0, + currentSetIndex: 0, + setsCount: 1, + targetReps: 1, + autoStartNextTimedStepSnapshot: true, + autoStartNextTimedStepOverride: true, + templateAutoStartNextTimedStepOverride: false, + exerciseSteps: _twoTimedSteps(), + ); + final repository = _FakeActiveSessionRepository()..session = session; + final useCase = ActiveExerciseStepUseCases( + sessionRepository: repository, + clock: clock, + ids: _FakeIds(), + originDeviceId: 'device-1', + ); + + await useCase.startOrResumeProgress( + sessionId: session.metadata.id, + programIndex: 0, + exerciseIndex: 0, + setIndex: 0, + ); + await useCase.startTimer( + sessionId: session.metadata.id, + programIndex: 0, + exerciseIndex: 0, + setIndex: 0, + ); + clock.value = clock.value.add(const Duration(milliseconds: 2500)); + + final view = await useCase.startOrResumeProgress( + sessionId: session.metadata.id, + programIndex: 0, + exerciseIndex: 0, + setIndex: 0, + ); + + expect(view.state.status, ActiveExerciseStepProgressStatus.stoppedTimer); + expect(view.state.currentStepIndex, 1); + expect(view.state.accumulatedMs, 0); + expect(repository.stepResults, hasLength(1)); + }, + ); + + test( + 'step chaining resolution uses program override before exercise snapshot', + () async { + final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); + final session = _sessionWithSnapshot( + currentProgramIndex: 0, + currentExerciseIndex: 0, + currentSetIndex: 0, + setsCount: 1, + targetReps: 1, + autoStartNextTimedStepSnapshot: false, + autoStartNextTimedStepOverride: true, + exerciseSteps: _twoTimedSteps(), + ); + final repository = _FakeActiveSessionRepository()..session = session; + final useCase = ActiveExerciseStepUseCases( + sessionRepository: repository, + clock: clock, + ids: _FakeIds(), + originDeviceId: 'device-1', + ); + + await useCase.startOrResumeProgress( + sessionId: session.metadata.id, + programIndex: 0, + exerciseIndex: 0, + setIndex: 0, + ); + await useCase.startTimer( + sessionId: session.metadata.id, + programIndex: 0, + exerciseIndex: 0, + setIndex: 0, + ); + clock.value = clock.value.add(const Duration(milliseconds: 2500)); + + final view = await useCase.startOrResumeProgress( + sessionId: session.metadata.id, + programIndex: 0, + exerciseIndex: 0, + setIndex: 0, + ); + + expect( + view.state.status, + ActiveExerciseStepProgressStatus.sequenceComplete, + ); + expect(repository.stepResults, hasLength(2)); + }, + ); + + test( + 'step chaining defaults to true when snapshot fields are absent', + () async { + final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); + final session = _sessionWithSnapshot( + currentProgramIndex: 0, + currentExerciseIndex: 0, + currentSetIndex: 0, + setsCount: 1, + targetReps: 1, + autoStartNextTimedStepSnapshot: null, + exerciseSteps: _twoTimedSteps(), + ); + final repository = _FakeActiveSessionRepository()..session = session; + final useCase = ActiveExerciseStepUseCases( + sessionRepository: repository, + clock: clock, + ids: _FakeIds(), + originDeviceId: 'device-1', + ); + + await useCase.startOrResumeProgress( + sessionId: session.metadata.id, + programIndex: 0, + exerciseIndex: 0, + setIndex: 0, + ); + await useCase.startTimer( + sessionId: session.metadata.id, + programIndex: 0, + exerciseIndex: 0, + setIndex: 0, + ); + clock.value = clock.value.add(const Duration(milliseconds: 2500)); + + final view = await useCase.startOrResumeProgress( + sessionId: session.metadata.id, + programIndex: 0, + exerciseIndex: 0, + setIndex: 0, + ); + + expect( + view.state.status, + ActiveExerciseStepProgressStatus.sequenceComplete, + ); + expect(repository.stepResults, hasLength(2)); + }, + ); + + test( + 'completeCurrentStep leaves next timed step stopped when chaining disabled', + () async { + final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); + final session = _sessionWithSnapshot( + currentProgramIndex: 0, + currentExerciseIndex: 0, + currentSetIndex: 0, + setsCount: 1, + targetReps: 1, + autoStartNextTimedStepSnapshot: false, + exerciseSteps: _twoTimedSteps(), + ); + final repository = _FakeActiveSessionRepository()..session = session; + final useCase = ActiveExerciseStepUseCases( + sessionRepository: repository, + clock: clock, + ids: _FakeIds(), + originDeviceId: 'device-1', + ); + + await useCase.startOrResumeProgress( + sessionId: session.metadata.id, + programIndex: 0, + exerciseIndex: 0, + setIndex: 0, + ); + await useCase.startTimer( + sessionId: session.metadata.id, + programIndex: 0, + exerciseIndex: 0, + setIndex: 0, + ); + clock.value = clock.value.add(const Duration(milliseconds: 500)); + await useCase.completeCurrentStep( + sessionId: session.metadata.id, + programIndex: 0, + exerciseIndex: 0, + setIndex: 0, + ); + + final view = await useCase.readProgress( + sessionId: session.metadata.id, + programIndex: 0, + exerciseIndex: 0, + setIndex: 0, + ); + + expect(view.state.status, ActiveExerciseStepProgressStatus.stoppedTimer); + expect(view.state.currentStepIndex, 1); + expect(view.state.startedAt, isNull); + expect(view.state.accumulatedMs, 0); + }, + ); + + test( + 'completeCurrentStep starts next timed step when chaining is enabled', + () async { + final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); + final session = _sessionWithSnapshot( + currentProgramIndex: 0, + currentExerciseIndex: 0, + currentSetIndex: 0, + setsCount: 1, + targetReps: 1, + autoStartNextTimedStepSnapshot: true, + exerciseSteps: _twoTimedSteps(), + ); + final repository = _FakeActiveSessionRepository()..session = session; + final useCase = ActiveExerciseStepUseCases( + sessionRepository: repository, + clock: clock, + ids: _FakeIds(), + originDeviceId: 'device-1', + ); + + await useCase.startOrResumeProgress( + sessionId: session.metadata.id, + programIndex: 0, + exerciseIndex: 0, + setIndex: 0, + ); + await useCase.startTimer( + sessionId: session.metadata.id, + programIndex: 0, + exerciseIndex: 0, + setIndex: 0, + ); + clock.value = clock.value.add(const Duration(milliseconds: 1500)); + await useCase.completeCurrentStep( + sessionId: session.metadata.id, + programIndex: 0, + exerciseIndex: 0, + setIndex: 0, + ); + + final view = await useCase.readProgress( + sessionId: session.metadata.id, + programIndex: 0, + exerciseIndex: 0, + setIndex: 0, + ); + + expect(view.state.status, ActiveExerciseStepProgressStatus.runningTimer); + expect(view.state.currentStepIndex, 1); + expect(view.state.elapsedMillisecondsAt(clock.now()), 500); + }, + ); + + test('startTimer manually starts a stopped next timed step', () async { + final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); + final session = _sessionWithSnapshot( + currentProgramIndex: 0, + currentExerciseIndex: 0, + currentSetIndex: 0, + setsCount: 1, + targetReps: 1, + autoStartNextTimedStepSnapshot: false, + exerciseSteps: _twoTimedSteps(), + ); + final repository = _FakeActiveSessionRepository()..session = session; + final useCase = ActiveExerciseStepUseCases( + sessionRepository: repository, + clock: clock, + ids: _FakeIds(), + originDeviceId: 'device-1', + ); + + await useCase.startOrResumeProgress( + sessionId: session.metadata.id, + programIndex: 0, + exerciseIndex: 0, + setIndex: 0, + ); + await useCase.startTimer( + sessionId: session.metadata.id, + programIndex: 0, + exerciseIndex: 0, + setIndex: 0, + ); + clock.value = clock.value.add(const Duration(milliseconds: 1500)); + await useCase.startOrResumeProgress( + sessionId: session.metadata.id, + programIndex: 0, + exerciseIndex: 0, + setIndex: 0, + ); + + final running = await useCase.startTimer( + sessionId: session.metadata.id, + programIndex: 0, + exerciseIndex: 0, + setIndex: 0, + ); + + expect(running.status, ActiveExerciseStepProgressStatus.runningTimer); + expect(running.currentStepIndex, 1); + expect(running.startedAt, clock.now()); + }); + test('AuthUseCases register stores token and account session', () async { final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); final tokenStore = _FakeAuthTokenStore(); @@ -2622,7 +2934,26 @@ ActiveWorkoutSession _sessionWithSnapshot({ int setsCount = 2, int targetReps = 10, List exerciseSteps = const [], + bool? autoStartNextTimedStepSnapshot = true, + bool? autoStartNextTimedStepOverride, + bool? templateAutoStartNextTimedStepOverride, }) { + final exerciseSnapshot = { + 'id': 'exercise-snapshot-1', + 'exerciseNameSnapshot': 'Squat', + 'setsCount': setsCount, + 'timeEnabled': false, + 'repsEnabled': true, + 'scoreEnabled': false, + 'targetReps': targetReps, + 'exerciseStepsSnapshot': exerciseSteps + .map((step) => step.toSnapshotJson()) + .toList(), + if (autoStartNextTimedStepSnapshot != null) + 'autoStartNextTimedStepSnapshot': autoStartNextTimedStepSnapshot, + if (autoStartNextTimedStepOverride != null) + 'autoStartNextTimedStepOverride': autoStartNextTimedStepOverride, + }; return ActiveWorkoutSession( metadata: _metadata('session-1'), status: ActiveWorkoutStatus.running, @@ -2638,23 +2969,38 @@ ActiveWorkoutSession _sessionWithSnapshot({ 'id': 'program-snapshot-1', 'programNameSnapshot': 'Programme', 'programSnapshotJson': jsonEncode({ - 'exercises': [ - { - 'id': 'exercise-snapshot-1', - 'exerciseNameSnapshot': 'Squat', - 'setsCount': setsCount, - 'timeEnabled': false, - 'repsEnabled': true, - 'scoreEnabled': false, - 'targetReps': targetReps, - 'exerciseStepsSnapshot': exerciseSteps - .map((step) => step.toSnapshotJson()) - .toList(), - }, - ], + 'exercises': [exerciseSnapshot], }), }, ], + 'overrides': [ + if (templateAutoStartNextTimedStepOverride != null) + { + 'workoutTemplateProgramId': 'program-snapshot-1', + 'snapshotProgramExerciseId': 'exercise-snapshot-1', + 'autoStartNextTimedStepOverride': + templateAutoStartNextTimedStepOverride, + }, + ], }), ); } + +List _twoTimedSteps() { + return [ + _step( + id: 'step-1', + position: 0, + name: 'Gauche', + type: ExerciseStepType.time, + defaultTargetValue: 1, + ), + _step( + id: 'step-2', + position: 1, + name: 'Droite', + type: ExerciseStepType.time, + defaultTargetValue: 1, + ), + ]; +} diff --git a/test/infrastructure/drift_repositories_test.dart b/test/infrastructure/drift_repositories_test.dart index bfaab8f..d2c0094 100644 --- a/test/infrastructure/drift_repositories_test.dart +++ b/test/infrastructure/drift_repositories_test.dart @@ -98,99 +98,108 @@ void main() { 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); + 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 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 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 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 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, - ); - }); + 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);