chore(wip): consolidation intermédiaire multi-tickets (sprints Statistiques, UI, Bug resolution, Serveur-client)

Regroupe l'état de travail en cours réalisé dans un même worktree sur
plusieurs tickets/sprints (#85, #136, #145, #155-160, #162-164),
mélangeant des tickets QA et inProgress. Ne constitue pas une feature
terminée : commit de sauvegarde avant triage/split par ticket en
branches feature/* dédiées. Exclut les dossiers d'environnement de
build locaux et le heap dump parasite (.gitignore mis à jour).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 16:48:54 +02:00
parent 58272e354a
commit 917777e18b
279 changed files with 13546 additions and 674 deletions

View File

@ -895,6 +895,81 @@ void main() {
expect(repository.restStates.single.skippedAt, isNotNull);
});
testWidgets('les mutations externes de repos resynchronisent lécran', (
tester,
) async {
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
final repository = _FakeActiveSessionRepository();
final session = ActiveWorkoutSession(
metadata: _metadata('session-1'),
sourceWorkoutTemplateId: 'template-1',
status: ActiveWorkoutStatus.running,
startedAt: DateTime.utc(2026, 7, 17, 12),
lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
elapsedActiveMs: 0,
currentProgramIndex: 0,
currentExerciseIndex: 0,
currentSetIndex: 0,
resolvedTemplateSnapshotJson: _sessionSnapshot(
setsCount: 2,
restSeconds: 60,
),
);
repository.session = session;
await tester.pumpWidget(
MaterialApp(
home: WorkoutExecutionScreen(
initialSession: session,
activeUseCases: _activeUseCases(repository, clock),
closeUseCase: _closeUseCase(repository, clock),
historyUseCases: _historyUseCases(clock),
workoutTemplateUseCases: _workoutTemplateUseCases(),
),
),
);
await tester.pump();
expect(find.text('Repos avant la prochaine série'), findsNothing);
repository.restStates.add(
ActiveRestState(
metadata: _metadata('rest-1'),
activeWorkoutSessionId: 'session-1',
afterProgramIndex: 0,
afterExerciseIndex: 0,
afterSetIndex: 0,
plannedRestSeconds: 60,
adjustedRestSeconds: 60,
startedAt: DateTime.now().toUtc(),
),
);
await tester.pump(const Duration(seconds: 1));
await tester.pump();
expect(find.text('Repos avant la prochaine série'), findsOneWidget);
repository.restStates.clear();
repository.session = ActiveWorkoutSession(
metadata: session.metadata,
sourceWorkoutTemplateId: session.sourceWorkoutTemplateId,
status: ActiveWorkoutStatus.running,
startedAt: session.startedAt,
lastPersistedAt: session.lastPersistedAt,
elapsedActiveMs: session.elapsedActiveMs,
currentProgramIndex: 0,
currentExerciseIndex: 0,
currentSetIndex: 1,
resolvedTemplateSnapshotJson: session.resolvedTemplateSnapshotJson,
);
await tester.pump(const Duration(seconds: 1));
await tester.pump();
expect(find.text('Repos avant la prochaine série'), findsNothing);
expect(find.text('Terminer la série'), findsOneWidget);
});
testWidgets('un repos en pause est repris sans compter la durée de pause', (
tester,
) async {
@ -1629,6 +1704,139 @@ void main() {
expect(find.byType(SingleChildScrollView), findsWidgets);
});
testWidgets('répétitions et chrono score détape tiennent sans overflow', (
tester,
) async {
await tester.binding.setSurfaceSize(const Size(400, 700));
addTearDown(() => tester.binding.setSurfaceSize(null));
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
final repository = _FakeActiveSessionRepository();
final session = ActiveWorkoutSession(
metadata: _metadata('session-1'),
sourceWorkoutTemplateId: 'template-1',
status: ActiveWorkoutStatus.running,
startedAt: DateTime.utc(2026, 7, 17, 12),
lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
elapsedActiveMs: 0,
currentProgramIndex: 0,
currentExerciseIndex: 0,
currentSetIndex: 0,
resolvedTemplateSnapshotJson: _sessionSnapshot(
targetReps: 12,
exerciseSteps: [
_step(
'step-1',
0,
'Pompes tempo',
ExerciseStepType.reps,
12,
hasScore: true,
scoreInputMode: ScoreInputMode.stopwatch,
),
],
),
);
repository.session = session;
await tester.pumpWidget(
MaterialApp(
home: WorkoutExecutionScreen(
initialSession: session,
activeUseCases: _activeUseCases(repository, clock),
stepUseCases: _stepUseCases(repository, clock),
stepAudioCuePlayer: _FakeStepAudioCuePlayer(),
closeUseCase: _closeUseCase(repository, clock),
historyUseCases: _historyUseCases(clock),
workoutTemplateUseCases: _workoutTemplateUseCases(),
),
),
);
await tester.pump();
expect(tester.takeException(), isNull);
expect(find.text('RÉPÉTITIONS'), findsOneWidget);
expect(find.text('CHRONO SCORE'), findsOneWidget);
expect(find.byTooltip('Lancer'), findsOneWidget);
expect(find.byTooltip('Réinitialiser'), findsOneWidget);
expect(
find.widgetWithText(OutlinedButton, 'Passer létape'),
findsOneWidget,
);
expect(find.widgetWithText(FilledButton, 'Étape suivante'), findsOneWidget);
expect(find.widgetWithText(TextButton, 'Réinitialiser'), findsNothing);
});
testWidgets('chrono décompte et score libre tiennent avec stepper compact', (
tester,
) async {
await tester.binding.setSurfaceSize(const Size(400, 700));
addTearDown(() => tester.binding.setSurfaceSize(null));
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
final repository = _FakeActiveSessionRepository();
final session = ActiveWorkoutSession(
metadata: _metadata('session-1'),
sourceWorkoutTemplateId: 'template-1',
status: ActiveWorkoutStatus.running,
startedAt: DateTime.utc(2026, 7, 17, 12),
lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
elapsedActiveMs: 0,
currentProgramIndex: 0,
currentExerciseIndex: 0,
currentSetIndex: 0,
resolvedTemplateSnapshotJson: _sessionSnapshot(
timeEnabled: false,
repsEnabled: false,
scoreEnabled: false,
exerciseSteps: [
_step(
'step-1',
0,
'Maintien défensif',
ExerciseStepType.time,
10,
hasScore: true,
scoreInputMode: ScoreInputMode.manual,
scoreLabel: 'Paniers',
scoreUnit: 'points',
defaultTargetScore: 5,
),
],
),
);
repository.session = session;
await tester.pumpWidget(
MaterialApp(
home: WorkoutExecutionScreen(
initialSession: session,
activeUseCases: _activeUseCases(repository, clock),
stepUseCases: _stepUseCases(repository, clock),
stepAudioCuePlayer: _FakeStepAudioCuePlayer(),
closeUseCase: _closeUseCase(repository, clock),
historyUseCases: _historyUseCases(clock),
workoutTemplateUseCases: _workoutTemplateUseCases(),
),
),
);
await tester.pump();
expect(tester.takeException(), isNull);
expect(find.text('CHRONO'), findsOneWidget);
expect(find.text('SCORE'), findsOneWidget);
expect(find.text('00:10'), findsOneWidget);
expect(find.text('5'), findsOneWidget);
expect(find.byTooltip('Diminuer'), findsOneWidget);
expect(find.byTooltip('Augmenter'), findsOneWidget);
expect(find.widgetWithText(TextField, 'Score de létape'), findsNothing);
expect(
find.widgetWithText(OutlinedButton, 'Passer létape'),
findsOneWidget,
);
expect(find.widgetWithText(FilledButton, 'Étape suivante'), findsNothing);
});
testWidgets('lécran actif avec séquence longue reste sans scroll', (
tester,
) async {
@ -1790,6 +1998,24 @@ void main() {
expect(find.text('00:03'), findsOneWidget);
expect(audio.shortBeeps, 1);
clock.value = DateTime.utc(2026, 7, 17, 12, 0, 3);
await tester.pump(const Duration(milliseconds: 100));
expect(find.text('00:02'), findsOneWidget);
expect(audio.shortBeeps, 2);
clock.value = DateTime.utc(2026, 7, 17, 12, 0, 4);
await tester.pump(const Duration(milliseconds: 100));
expect(find.text('00:01'), findsOneWidget);
expect(audio.shortBeeps, 3);
clock.value = DateTime.utc(2026, 7, 17, 12, 0, 5, 200);
await tester.pump(const Duration(milliseconds: 200));
await tester.pump();
expect(audio.longBeeps, 1);
});
testWidgets(
@ -2727,6 +2953,9 @@ final class _FakeWorkoutTemplateRepository
@override
Future<void> save(WorkoutTemplate template) async {}
@override
Future<void> saveAll(List<WorkoutTemplate> templates) async {}
@override
Future<void> saveOverride(WorkoutTemplateExerciseOverride override) async {}
@ -2750,8 +2979,11 @@ final class _FakeWorkoutHistoryRepository implements WorkoutHistoryRepository {
@override
Future<void> patchHeartRateSummary({
required String historyId,
int? minHeartRateBpm,
required double averageHeartRateBpm,
required int maxHeartRateBpm,
double? totalDistanceMeters,
double? totalCaloriesKcal,
required DateTime patchedAt,
}) async {}