chore(wip): lot #165-169 validé QA + rework foreground #163 (KO - preuve device/toolchain insuffisante)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 17:59:43 +02:00
parent 917777e18b
commit bc533d6c45
32 changed files with 898 additions and 123 deletions

View File

@ -98,6 +98,44 @@ void main() {
expect(env.repository.stepResults, hasLength(1));
});
test('routes completeCurrentStep to completed reps step', () async {
final session = _session(
steps: [_step(type: ExerciseStepType.reps, defaultTargetValue: 12)],
);
final env = _env(
session: session,
projection: _projection(
stepType: WatchStepType.reps,
stepTargetValue: 12,
),
);
env.repository.stepProgressStates['step-state'] = _stepState(
sessionId: session.metadata.id,
);
final ack = await env.dispatch(WatchCommandType.completeCurrentStep);
expect(ack, WatchCommandAck.accepted);
expect(env.repository.stepResults.single.status, SetResultStatus.completed);
expect(env.repository.stepResults.single.actualReps, 12);
});
test(
'rejects completeCurrentStep when current projection is not reps',
() async {
final session = _session(steps: [_step()]);
final env = _env(session: session, projection: _projection());
env.repository.stepProgressStates['step-state'] = _stepState(
sessionId: session.metadata.id,
);
final ack = await env.dispatch(WatchCommandType.completeCurrentStep);
expect(ack, WatchCommandAck.rejectedNotApplicable);
expect(env.repository.stepResults, isEmpty);
},
);
test('routes skipCurrentPassage to step use case', () async {
final session = _session(
targetReps: 2,
@ -542,6 +580,8 @@ WatchSessionProjection _projection({
double? currentManualScoreValue,
bool canDecrementScore = false,
WatchManualScoreScope? manualScoreScope,
WatchStepType? stepType,
int? stepTargetValue,
}) {
return WatchSessionProjection(
deviceSessionId: deviceSessionId,
@ -557,6 +597,8 @@ WatchSessionProjection _projection({
hasManualScore: hasManualScore,
currentManualScoreValue: currentManualScoreValue,
canDecrementScore: canDecrementScore,
stepType: stepType,
stepTargetValue: stepTargetValue,
manualScoreScope:
manualScoreScope ??
(hasManualScore ? WatchManualScoreScope.series : null),
@ -617,6 +659,8 @@ ActiveWorkoutSession _session({
ExerciseStep _step({
String id = 'step-1',
int position = 0,
ExerciseStepType type = ExerciseStepType.time,
int defaultTargetValue = 1,
bool hasScore = false,
String? scoreLabel,
String? scoreUnit,
@ -626,8 +670,8 @@ ExerciseStep _step({
id: id,
position: position,
name: 'Step ${position + 1}',
type: ExerciseStepType.time,
defaultTargetValue: 1,
type: type,
defaultTargetValue: defaultTargetValue,
hasScore: hasScore,
scoreLabel: scoreLabel,
scoreUnit: scoreUnit,
@ -701,6 +745,8 @@ final class _FakeProjectionSource implements WatchProjectionSource {
seriesIndex: projection.seriesIndex,
seriesTotal: projection.seriesTotal,
exerciseName: projection.exerciseName,
stepType: projection.stepType,
stepTargetValue: projection.stepTargetValue,
primaryAction: projection.primaryAction,
secondaryActions: projection.secondaryActions,
hasManualScore: projection.hasManualScore,

View File

@ -40,6 +40,20 @@ void main() {
expect(projection.primaryAction, WatchPrimaryAction.startCurrentExercise);
});
test('projects reps step target for direct watch validation', () async {
final repository = _FakeActiveSessionRepository()
..session = _session(
steps: [_step(type: ExerciseStepType.reps, defaultTargetValue: 12)],
);
final projector = _projector(repository, _clock());
final projection = await projector.project(revision: 1);
expect(projection.stepType, WatchStepType.reps);
expect(projection.stepTargetValue, 12);
expect(projection.dominantTimer, isNull);
});
test('projects running with step timer before stopwatch score', () async {
final now = DateTime.utc(2026, 7, 25, 12);
final session = _session(
@ -299,6 +313,42 @@ void main() {
},
);
test(
'projects nextTimerReady for a stopped timed step after watch skip with chaining enabled',
() async {
final now = DateTime.utc(2026, 7, 25, 12);
final session = _session(
autoStartNextTimedStepSnapshot: true,
steps: [
_step(id: 'step-1'),
_step(id: 'step-2', position: 1),
],
);
final repository = _FakeActiveSessionRepository()
..session = session
..stepProgressStates['step-state'] = _stepState(
sessionId: session.metadata.id,
stepId: 'step-2',
stepIndex: 1,
status: ActiveExerciseStepProgressStatus.stoppedTimer,
lastTransitionAt: now,
);
final projector = _projector(repository, _clock(now));
final projection = await projector.project(revision: 4);
expect(projection.phase, WatchSessionPhase.nextTimerReady);
expect(projection.stepIndex, 2);
expect(projection.stepName, 'Step 2');
expect(projection.statusLabel, 'Chrono suivant prêt');
expect(
projection.primaryAction,
WatchPrimaryAction.startPreparedTimedStep,
);
expect(projection.dominantTimer?.runState, WatchTimerRunState.stopped);
},
);
test('projects restRunning after finishing a set with rest', () async {
final now = DateTime.utc(2026, 7, 25, 12);
final session = _session(currentSetIndex: 1);
@ -406,7 +456,7 @@ void main() {
final projection = await projector.project(revision: 6);
expect(projection.phase, WatchSessionPhase.betweenSetsReady);
expect(projection.statusLabel, 'Prêt pour la série suivante');
expect(projection.statusLabel, isNull);
expect(projection.primaryAction, WatchPrimaryAction.startCurrentExercise);
},
);
@ -594,6 +644,7 @@ ActiveWorkoutSession _session({
ExerciseStep _step({
String id = 'step-1',
int position = 0,
ExerciseStepType type = ExerciseStepType.time,
int defaultTargetValue = 1,
bool hasScore = false,
String? scoreLabel,
@ -604,7 +655,7 @@ ExerciseStep _step({
id: id,
position: position,
name: 'Step ${position + 1}',
type: ExerciseStepType.time,
type: type,
defaultTargetValue: defaultTargetValue,
hasScore: hasScore,
scoreLabel: scoreLabel,

View File

@ -71,7 +71,7 @@ void main() {
);
});
testWidgets('le détail affiche la fréquence cardiaque quand elle existe', (
testWidgets('le détail affiche les stats montre quand elles existent', (
tester,
) async {
await tester.pumpWidget(
@ -79,8 +79,11 @@ void main() {
home: HistoryDetailScreen(
history: _history(
id: 'history-1',
minHeartRateBpm: 88,
averageHeartRateBpm: 126.4,
maxHeartRateBpm: 171,
totalDistanceMeters: 1234,
totalCaloriesKcal: 83,
),
historyUseCases: _historyUseCases(_FakeWorkoutHistoryRepository()),
workoutTemplateUseCases: _workoutTemplateUseCases(),
@ -90,11 +93,22 @@ void main() {
),
);
expect(find.text('Stats montre'), findsOneWidget);
expect(find.text('Fréquence cardiaque'), findsOneWidget);
expect(find.text('Min'), findsAtLeastNWidgets(1));
expect(find.text('Moyenne'), findsOneWidget);
expect(find.text('Max'), findsOneWidget);
expect(find.text('Max'), findsAtLeastNWidgets(1));
expect(find.text('Distance'), findsOneWidget);
expect(find.text('Calories'), findsOneWidget);
expect(
find.byKey(const ValueKey('history-watch-stats-graph')),
findsOneWidget,
);
expect(find.text('88 bpm', findRichText: true), findsOneWidget);
expect(find.text('126 bpm', findRichText: true), findsOneWidget);
expect(find.text('171 bpm', findRichText: true), findsOneWidget);
expect(find.text('1.23 km', findRichText: true), findsOneWidget);
expect(find.text('83 kcal', findRichText: true), findsOneWidget);
});
testWidgets('le détail affiche le score chrono comme temps réalisé', (
@ -256,8 +270,11 @@ WorkoutHistory _history({
bool stopwatchScore = false,
bool withStepResults = false,
bool emptySnapshot = false,
int? minHeartRateBpm,
double? averageHeartRateBpm,
int? maxHeartRateBpm,
double? totalDistanceMeters,
double? totalCaloriesKcal,
}) {
final start = startedAt ?? DateTime.utc(2026, 7, 17, 10);
return WorkoutHistory(
@ -268,8 +285,11 @@ WorkoutHistory _history({
startedAt: start,
endedAt: start.add(const Duration(minutes: 30)),
totalActiveMs: 1800000,
minHeartRateBpm: minHeartRateBpm,
averageHeartRateBpm: averageHeartRateBpm,
maxHeartRateBpm: maxHeartRateBpm,
totalDistanceMeters: totalDistanceMeters,
totalCaloriesKcal: totalCaloriesKcal,
completed: true,
historySnapshotJson: jsonEncode({
'sessionId': 'session-1',