merge(develop): clôture lot #91 Wear OS - fréquence cardiaque, notifications, finitions montre
Intègre le commit de clôture du lot #91 resté non fusionné. Conflits attendus sur entities.dart, workout_execution_screen.dart et watch_companion_projection_test.dart : arbitrage Architect - garder la base feature/ticket91-wear-os-watch-sync sur ces 3 fichiers puis réinjecter uniquement les invariants chronos utiles (Durée / Temps réalisé). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> # Conflicts: # lib/domain/entities.dart # lib/presentation/workout_execution_screen.dart # test/application/watch_companion_projection_test.dart
This commit is contained in:
@ -69,10 +69,10 @@ void main() {
|
||||
|
||||
expect(projection.phase, WatchSessionPhase.running);
|
||||
expect(projection.dominantTimer?.kind, WatchTimerKind.step);
|
||||
expect(projection.dominantTimer?.accumulatedMs, 0);
|
||||
expect(projection.dominantTimer?.accumulatedMs, 5000);
|
||||
expect(
|
||||
projection.dominantTimer?.startedAtEpochMs,
|
||||
now.subtract(const Duration(seconds: 5)).millisecondsSinceEpoch,
|
||||
now.millisecondsSinceEpoch,
|
||||
);
|
||||
expect(projection.secondaryTimers.map((timer) => timer.kind), [
|
||||
WatchTimerKind.setTimer,
|
||||
@ -110,9 +110,111 @@ void main() {
|
||||
expect(projection.phase, WatchSessionPhase.running);
|
||||
expect(projection.dominantTimer?.kind, WatchTimerKind.setTimer);
|
||||
expect(projection.secondaryTimers, isEmpty);
|
||||
expect(projection.primaryAction, WatchPrimaryAction.pauseSession);
|
||||
expect(
|
||||
projection.secondaryActions,
|
||||
contains(WatchSecondaryAction.finishCurrentSet),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('projects manual score state for the current set', () async {
|
||||
final session = _session(scoreEnabled: true, targetScore: 8);
|
||||
final repository = _FakeActiveSessionRepository()
|
||||
..session = session
|
||||
..manualScoreStates['manual-score'] = ActiveManualScoreState(
|
||||
metadata: _metadata('manual-score'),
|
||||
activeWorkoutSessionId: session.metadata.id,
|
||||
programIndex: 0,
|
||||
exerciseIndex: 0,
|
||||
setIndex: 0,
|
||||
value: 3,
|
||||
updatedAt: DateTime.utc(2026, 7, 25, 12),
|
||||
);
|
||||
final projector = _projector(repository, _clock());
|
||||
|
||||
final projection = await projector.project(revision: 1);
|
||||
|
||||
expect(projection.hasManualScore, isTrue);
|
||||
expect(projection.currentManualScoreValue, 3);
|
||||
expect(projection.canDecrementScore, isTrue);
|
||||
expect(projection.manualScoreTargetValue, 8);
|
||||
expect(projection.manualScoreTargetLabel, 'Cible');
|
||||
expect(projection.manualScoreScope, WatchManualScoreScope.series);
|
||||
});
|
||||
|
||||
test(
|
||||
'projects independent current step manual score before series score',
|
||||
() async {
|
||||
final session = _session(
|
||||
scoreEnabled: true,
|
||||
targetScore: 8,
|
||||
steps: [
|
||||
_step(
|
||||
hasScore: true,
|
||||
scoreLabel: 'Réussites',
|
||||
scoreUnit: 'pts',
|
||||
defaultTargetScore: 5,
|
||||
),
|
||||
],
|
||||
);
|
||||
final repository = _FakeActiveSessionRepository()
|
||||
..session = session
|
||||
..stepProgressStates['step-state'] = _stepState(
|
||||
sessionId: session.metadata.id,
|
||||
status: ActiveExerciseStepProgressStatus.waitingManual,
|
||||
)
|
||||
..stepResults.add(
|
||||
ActiveExerciseStepResult(
|
||||
metadata: _metadata('step-score'),
|
||||
activeWorkoutSessionId: session.metadata.id,
|
||||
programSnapshotId: 'program-snapshot-1',
|
||||
exerciseSnapshotId: 'exercise-snapshot-1',
|
||||
programIndex: 0,
|
||||
exerciseIndex: 0,
|
||||
setIndex: 0,
|
||||
passageIndex: 0,
|
||||
stepIndex: 0,
|
||||
stepSnapshotId: 'step-1',
|
||||
stepNameSnapshot: 'Step 1',
|
||||
stepTypeSnapshot: ExerciseStepType.time,
|
||||
targetValueSnapshot: 1,
|
||||
hasScoreSnapshot: true,
|
||||
scoreInputModeSnapshot: ScoreInputMode.manual,
|
||||
scoreLabelSnapshot: 'Réussites',
|
||||
scoreUnitSnapshot: 'pts',
|
||||
targetScoreSnapshot: 5,
|
||||
status: SetResultStatus.completed,
|
||||
actualScore: 2,
|
||||
),
|
||||
);
|
||||
final projector = _projector(repository, _clock());
|
||||
|
||||
final projection = await projector.project(revision: 1);
|
||||
|
||||
expect(projection.hasManualScore, isTrue);
|
||||
expect(projection.currentManualScoreValue, 2);
|
||||
expect(projection.manualScoreTargetValue, 5);
|
||||
expect(projection.manualScoreScope, WatchManualScoreScope.step);
|
||||
},
|
||||
);
|
||||
|
||||
test('does not project manual score controls for stopwatch score', () async {
|
||||
final repository = _FakeActiveSessionRepository()
|
||||
..session = _session(
|
||||
scoreEnabled: true,
|
||||
scoreInputMode: ScoreInputMode.stopwatch,
|
||||
);
|
||||
final projector = _projector(repository, _clock());
|
||||
|
||||
final projection = await projector.project(revision: 1);
|
||||
|
||||
expect(projection.hasManualScore, isFalse);
|
||||
expect(projection.currentManualScoreValue, isNull);
|
||||
expect(projection.canDecrementScore, isFalse);
|
||||
expect(projection.manualScoreTargetValue, isNull);
|
||||
});
|
||||
|
||||
test('projects paused after a running session is paused', () async {
|
||||
final now = DateTime.utc(2026, 7, 25, 12);
|
||||
final session = _session(
|
||||
@ -307,14 +409,22 @@ void main() {
|
||||
);
|
||||
|
||||
test(
|
||||
'emits projections through stream and publisher with incremented revision',
|
||||
'keeps revision stable when refresh only changes volatile timing fields',
|
||||
() async {
|
||||
final now = DateTime.utc(2026, 7, 25, 12);
|
||||
final clock = _clock(now);
|
||||
final session = _session(steps: [_step(defaultTargetValue: 30)]);
|
||||
final repository = _FakeActiveSessionRepository()
|
||||
..session = _session(steps: [_step()]);
|
||||
..session = session
|
||||
..stepProgressStates['step-state'] = _stepState(
|
||||
sessionId: session.metadata.id,
|
||||
status: ActiveExerciseStepProgressStatus.runningTimer,
|
||||
startedAt: now,
|
||||
);
|
||||
final publisher = _FakeWatchProjectionPublisher();
|
||||
final useCases = WatchCompanionProjectionUseCases(
|
||||
sessionRepository: repository,
|
||||
clock: _clock(),
|
||||
clock: clock,
|
||||
ids: _FakeIds(),
|
||||
originDeviceId: 'device-1',
|
||||
publisher: publisher,
|
||||
@ -323,21 +433,54 @@ void main() {
|
||||
final subscription = useCases.projections.listen(emitted.add);
|
||||
|
||||
final first = await useCases.emitCurrentProjection();
|
||||
clock.value = now.add(const Duration(seconds: 2));
|
||||
final second = await useCases.emitCurrentProjection();
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
expect(first.revision, 1);
|
||||
expect(second.revision, 2);
|
||||
expect(emitted.map((projection) => projection.revision), [1, 2]);
|
||||
expect(second.revision, 1);
|
||||
expect(second.projectedAtEpochMs, greaterThan(first.projectedAtEpochMs));
|
||||
expect(
|
||||
second.dominantTimer?.accumulatedMs,
|
||||
greaterThan(first.dominantTimer?.accumulatedMs ?? 0),
|
||||
);
|
||||
expect(emitted.map((projection) => projection.revision), [1, 1]);
|
||||
expect(publisher.published.map((projection) => projection.revision), [
|
||||
1,
|
||||
2,
|
||||
1,
|
||||
]);
|
||||
|
||||
await subscription.cancel();
|
||||
await useCases.dispose();
|
||||
},
|
||||
);
|
||||
|
||||
test('increments revision when session command state changes', () async {
|
||||
final repository = _FakeActiveSessionRepository()
|
||||
..session = _session(setsCount: 2);
|
||||
final publisher = _FakeWatchProjectionPublisher();
|
||||
final useCases = WatchCompanionProjectionUseCases(
|
||||
sessionRepository: repository,
|
||||
clock: _clock(),
|
||||
ids: _FakeIds(),
|
||||
originDeviceId: 'device-1',
|
||||
publisher: publisher,
|
||||
);
|
||||
|
||||
final first = await useCases.emitCurrentProjection();
|
||||
repository.session = repository.session!.copyWith(currentSetIndex: 1);
|
||||
final second = await useCases.emitCurrentProjection();
|
||||
|
||||
expect(first.revision, 1);
|
||||
expect(second.revision, 2);
|
||||
expect(second.seriesIndex, 2);
|
||||
expect(publisher.published.map((projection) => projection.revision), [
|
||||
1,
|
||||
2,
|
||||
]);
|
||||
|
||||
await useCases.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
WatchSessionProjectionProjector _projector(
|
||||
@ -366,6 +509,7 @@ ActiveWorkoutSession _session({
|
||||
bool repsEnabled = true,
|
||||
bool scoreEnabled = false,
|
||||
int? targetTimeSeconds,
|
||||
double? targetScore,
|
||||
ScoreInputMode scoreInputMode = ScoreInputMode.manual,
|
||||
bool? autoStartNextTimedStepSnapshot = true,
|
||||
List<ExerciseStep> steps = const [],
|
||||
@ -380,6 +524,7 @@ ActiveWorkoutSession _session({
|
||||
'scoreEnabled': scoreEnabled,
|
||||
'targetTimeSeconds': targetTimeSeconds,
|
||||
'targetReps': repsEnabled ? setsCount : null,
|
||||
'targetScore': targetScore,
|
||||
'scoreInputModeSnapshot': scoreInputMode.name,
|
||||
'exerciseStepsSnapshot': steps
|
||||
.map((step) => step.toSnapshotJson())
|
||||
@ -428,6 +573,10 @@ ExerciseStep _step({
|
||||
String id = 'step-1',
|
||||
int position = 0,
|
||||
int defaultTargetValue = 1,
|
||||
bool hasScore = false,
|
||||
String? scoreLabel,
|
||||
String? scoreUnit,
|
||||
double? defaultTargetScore,
|
||||
}) {
|
||||
return ExerciseStep(
|
||||
id: id,
|
||||
@ -435,6 +584,10 @@ ExerciseStep _step({
|
||||
name: 'Step ${position + 1}',
|
||||
type: ExerciseStepType.time,
|
||||
defaultTargetValue: defaultTargetValue,
|
||||
hasScore: hasScore,
|
||||
scoreLabel: scoreLabel,
|
||||
scoreUnit: scoreUnit,
|
||||
defaultTargetScore: defaultTargetScore,
|
||||
);
|
||||
}
|
||||
|
||||
@ -539,6 +692,7 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||||
final restStates = <String, ActiveRestState>{};
|
||||
final setTimerStates = <String, ActiveSetTimerState>{};
|
||||
final scoreStopwatchStates = <String, ActiveScoreStopwatchState>{};
|
||||
final manualScoreStates = <String, ActiveManualScoreState>{};
|
||||
final stepProgressStates = <String, ActiveExerciseStepProgressState>{};
|
||||
final stepResults = <ActiveExerciseStepResult>[];
|
||||
|
||||
@ -553,6 +707,17 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||||
scoreStopwatchStates.clear();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteManualScoreState({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
required DateTime deletedAt,
|
||||
}) async {
|
||||
manualScoreStates.clear();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ActiveWorkoutSession?> findById(String id) async {
|
||||
return session?.metadata.id == id ? session : null;
|
||||
@ -596,6 +761,21 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||||
}).firstOrNull;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ActiveManualScoreState?> findManualScoreState({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
}) async {
|
||||
return manualScoreStates.values.where((state) {
|
||||
return state.activeWorkoutSessionId == sessionId &&
|
||||
state.programIndex == programIndex &&
|
||||
state.exerciseIndex == exerciseIndex &&
|
||||
state.setIndex == setIndex;
|
||||
}).firstOrNull;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ActiveSetTimerState?> findSetTimerState({
|
||||
required String sessionId,
|
||||
@ -645,6 +825,15 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ActiveManualScoreState>> listManualScoreStates(
|
||||
String sessionId,
|
||||
) async {
|
||||
return manualScoreStates.values
|
||||
.where((state) => state.activeWorkoutSessionId == sessionId)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ActiveSetResult>> listSetResults(String sessionId) async {
|
||||
return results
|
||||
@ -686,6 +875,11 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||||
scoreStopwatchStates[state.metadata.id] = state;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> saveManualScoreState(ActiveManualScoreState state) async {
|
||||
manualScoreStates[state.metadata.id] = state;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> saveSetResult(ActiveSetResult result) async {
|
||||
results.add(result);
|
||||
|
||||
Reference in New Issue
Block a user