feat(exercice): résolution effective et auto-advance des chronos d'étapes (ticket #76)
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 <noreply@anthropic.com>
This commit is contained in:
@ -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<ExerciseStep> 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<ExerciseStep> _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,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user