merge(main): correctif de reprise en temps réel de la séance (ticket #14)
Fusionne feature/#14-repos-reprise-temps-reel — analyze propre, 26/26 tests verts, build APK debug validé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
issueRef: "#14"
|
issueRef: "#14"
|
||||||
version: 2
|
version: 3
|
||||||
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
updatedAt: 1784319234693
|
updatedAt: 1784319492849
|
||||||
---
|
---
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
id: "bf12e886-4849-4095-afc9-aa37c61fa257"
|
id: "bf12e886-4849-4095-afc9-aa37c61fa257"
|
||||||
number: 14
|
number: 14
|
||||||
title: "[DevFrontend] Repos persistant/reprenable + temps réel par série"
|
title: "[DevFrontend] Repos persistant/reprenable + temps réel par série"
|
||||||
status: "inProgress"
|
status: "closed"
|
||||||
priority: "high"
|
priority: "high"
|
||||||
sprint: null
|
sprint: null
|
||||||
links: [{"target":"#9","kind":"relatesTo"}]
|
links: [{"target":"#9","kind":"relatesTo"}]
|
||||||
@ -10,8 +10,8 @@ agentRefs: [{"agentId":"9933c93a-b8a1-4164-a3bb-7063fdad747d","role":"assigned"}
|
|||||||
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
createdAt: 1784318996196
|
createdAt: 1784318996196
|
||||||
updatedAt: 1784319234693
|
updatedAt: 1784319492849
|
||||||
version: 2
|
version: 3
|
||||||
---
|
---
|
||||||
Deux corrections sur lib/presentation/workout_execution_screen.dart, trouvées par QA (ticket #11) :
|
Deux corrections sur lib/presentation/workout_execution_screen.dart, trouvées par QA (ticket #11) :
|
||||||
1. Les ajustements ±15s du minuteur de repos ne sont modifiés qu'en mémoire (_remainingRestSeconds) et jamais re-persistés — après un kill d'app pendant un repos, l'écran redémarre sur l'exercice actif au lieu de reprendre le décompte de repos exact. À l'ouverture de l'écran, il faut relire l'état de repos actif éventuel (listRestStates / le nouveau use case du ticket dédié) et calculer le temps restant depuis les horodatages persistés, pas repartir de zéro.
|
1. Les ajustements ±15s du minuteur de repos ne sont modifiés qu'en mémoire (_remainingRestSeconds) et jamais re-persistés — après un kill d'app pendant un repos, l'écran redémarre sur l'exercice actif au lieu de reprendre le décompte de repos exact. À l'ouverture de l'écran, il faut relire l'état de repos actif éventuel (listRestStates / le nouveau use case du ticket dédié) et calculer le temps restant depuis les horodatages persistés, pas repartir de zéro.
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
issueRef: "#15"
|
issueRef: "#15"
|
||||||
version: 1
|
version: 2
|
||||||
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
updatedAt: 1784318999121
|
updatedAt: 1784319493575
|
||||||
---
|
---
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
id: "3463569f-e837-4af9-8156-4968e178d4e1"
|
id: "3463569f-e837-4af9-8156-4968e178d4e1"
|
||||||
number: 15
|
number: 15
|
||||||
title: "[DevFrontend] Sélecteur de fichier pour l'import média (remplace la saisie manuelle de chemin)"
|
title: "[DevFrontend] Sélecteur de fichier pour l'import média (remplace la saisie manuelle de chemin)"
|
||||||
status: "open"
|
status: "inProgress"
|
||||||
priority: "medium"
|
priority: "medium"
|
||||||
sprint: null
|
sprint: null
|
||||||
links: [{"target":"#6","kind":"relatesTo"}]
|
links: [{"target":"#6","kind":"relatesTo"}]
|
||||||
@ -10,7 +10,7 @@ agentRefs: [{"agentId":"9933c93a-b8a1-4164-a3bb-7063fdad747d","role":"assigned"}
|
|||||||
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
createdAt: 1784318999121
|
createdAt: 1784318999121
|
||||||
updatedAt: 1784318999121
|
updatedAt: 1784319493575
|
||||||
version: 1
|
version: 2
|
||||||
---
|
---
|
||||||
L'écran de création/édition d'exercice (lib/presentation/exercise_library_screen.dart) fait actuellement saisir manuellement un chemin de fichier pour importer une image/vidéo. Remplacer par un vrai sélecteur de fichiers via le package `image_picker` (à ajouter en dépendance dans pubspec.yaml — nouvelle dépendance, signalée et acceptée par l'utilisateur). Garder MediaUseCases.importMedia tel quel côté logique métier, seul le déclenchement du chemin source change (sélection graphique au lieu de TextField).
|
L'écran de création/édition d'exercice (lib/presentation/exercise_library_screen.dart) fait actuellement saisir manuellement un chemin de fichier pour importer une image/vidéo. Remplacer par un vrai sélecteur de fichiers via le package `image_picker` (à ajouter en dépendance dans pubspec.yaml — nouvelle dépendance, signalée et acceptée par l'utilisateur). Garder MediaUseCases.importMedia tel quel côté logique métier, seul le déclenchement du chemin source change (sélection graphique au lieu de TextField).
|
||||||
@ -161,25 +161,25 @@
|
|||||||
"issueRef": "#14",
|
"issueRef": "#14",
|
||||||
"path": "14",
|
"path": "14",
|
||||||
"title": "[DevFrontend] Repos persistant/reprenable + temps réel par série",
|
"title": "[DevFrontend] Repos persistant/reprenable + temps réel par série",
|
||||||
"status": "inProgress",
|
"status": "closed",
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
"sprint": null,
|
"sprint": null,
|
||||||
"assignedAgentIds": [
|
"assignedAgentIds": [
|
||||||
"9933c93a-b8a1-4164-a3bb-7063fdad747d"
|
"9933c93a-b8a1-4164-a3bb-7063fdad747d"
|
||||||
],
|
],
|
||||||
"updatedAt": 1784319234693
|
"updatedAt": 1784319492849
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"issueRef": "#15",
|
"issueRef": "#15",
|
||||||
"path": "15",
|
"path": "15",
|
||||||
"title": "[DevFrontend] Sélecteur de fichier pour l'import média (remplace la saisie manuelle de chemin)",
|
"title": "[DevFrontend] Sélecteur de fichier pour l'import média (remplace la saisie manuelle de chemin)",
|
||||||
"status": "open",
|
"status": "inProgress",
|
||||||
"priority": "medium",
|
"priority": "medium",
|
||||||
"sprint": null,
|
"sprint": null,
|
||||||
"assignedAgentIds": [
|
"assignedAgentIds": [
|
||||||
"9933c93a-b8a1-4164-a3bb-7063fdad747d"
|
"9933c93a-b8a1-4164-a3bb-7063fdad747d"
|
||||||
],
|
],
|
||||||
"updatedAt": 1784318999121
|
"updatedAt": 1784319493575
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -36,6 +36,8 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
Timer? _ticker;
|
Timer? _ticker;
|
||||||
Timer? _restTicker;
|
Timer? _restTicker;
|
||||||
WorkoutHistory? _completedHistory;
|
WorkoutHistory? _completedHistory;
|
||||||
|
String? _activeRestStateId;
|
||||||
|
late DateTime _seriesStartedAt;
|
||||||
var _reps = 0;
|
var _reps = 0;
|
||||||
final _scoreController = TextEditingController();
|
final _scoreController = TextEditingController();
|
||||||
var _remainingRestSeconds = 0;
|
var _remainingRestSeconds = 0;
|
||||||
@ -56,9 +58,11 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
_mode = _session.status == ActiveWorkoutStatus.running
|
_mode = _session.status == ActiveWorkoutStatus.running
|
||||||
? WorkoutExecutionMode.active
|
? WorkoutExecutionMode.active
|
||||||
: WorkoutExecutionMode.paused;
|
: WorkoutExecutionMode.paused;
|
||||||
|
_seriesStartedAt = DateTime.now().toUtc();
|
||||||
_ticker = Timer.periodic(const Duration(seconds: 1), (_) {
|
_ticker = Timer.periodic(const Duration(seconds: 1), (_) {
|
||||||
if (mounted) setState(() {});
|
if (mounted) setState(() {});
|
||||||
});
|
});
|
||||||
|
unawaited(_restoreActiveRest());
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -158,20 +162,14 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: OutlinedButton(
|
child: OutlinedButton(
|
||||||
onPressed: () => setState(() {
|
onPressed: () => _adjustRest(-15),
|
||||||
_remainingRestSeconds = (_remainingRestSeconds - 15)
|
|
||||||
.clamp(0, 9999)
|
|
||||||
.toInt();
|
|
||||||
}),
|
|
||||||
child: const Text('-15 s'),
|
child: const Text('-15 s'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: OutlinedButton(
|
child: OutlinedButton(
|
||||||
onPressed: () => setState(() {
|
onPressed: () => _adjustRest(15),
|
||||||
_remainingRestSeconds += 15;
|
|
||||||
}),
|
|
||||||
child: const Text('+15 s'),
|
child: const Text('+15 s'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -267,6 +265,8 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
Future<void> _resume() async {
|
Future<void> _resume() async {
|
||||||
_session = await widget.activeUseCases.resume(_session.metadata.id);
|
_session = await widget.activeUseCases.resume(_session.metadata.id);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
final restoredRest = await _restoreActiveRest();
|
||||||
|
if (!mounted || restoredRest) return;
|
||||||
setState(() => _mode = WorkoutExecutionMode.active);
|
setState(() => _mode = WorkoutExecutionMode.active);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -302,6 +302,10 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
|
|
||||||
Future<void> _finishSet({required bool skipped}) async {
|
Future<void> _finishSet({required bool skipped}) async {
|
||||||
final score = double.tryParse(_scoreController.text.trim());
|
final score = double.tryParse(_scoreController.text.trim());
|
||||||
|
final actualTimeMs = DateTime.now()
|
||||||
|
.toUtc()
|
||||||
|
.difference(_seriesStartedAt)
|
||||||
|
.inMilliseconds;
|
||||||
await widget.activeUseCases.recordCurrentSetResult(
|
await widget.activeUseCases.recordCurrentSetResult(
|
||||||
sessionId: _session.metadata.id,
|
sessionId: _session.metadata.id,
|
||||||
programSnapshotId: _plan.programAt(_position).id,
|
programSnapshotId: _plan.programAt(_position).id,
|
||||||
@ -312,7 +316,9 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
actualReps: skipped ? null : (_exercise.repsEnabled ? _reps : null),
|
actualReps: skipped ? null : (_exercise.repsEnabled ? _reps : null),
|
||||||
actualScore: skipped ? null : (_exercise.scoreEnabled ? score : null),
|
actualScore: skipped ? null : (_exercise.scoreEnabled ? score : null),
|
||||||
scoreUnitSnapshot: _exercise.scoreUnit,
|
scoreUnitSnapshot: _exercise.scoreUnit,
|
||||||
actualTimeMs: skipped ? null : (_exercise.timeEnabled ? 0 : null),
|
actualTimeMs: skipped
|
||||||
|
? null
|
||||||
|
: (_exercise.timeEnabled ? actualTimeMs : null),
|
||||||
);
|
);
|
||||||
|
|
||||||
final next = _plan.nextPosition(_position);
|
final next = _plan.nextPosition(_position);
|
||||||
@ -322,7 +328,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
}
|
}
|
||||||
final shouldRest = _plan.shouldShowRestAfter(_position);
|
final shouldRest = _plan.shouldShowRestAfter(_position);
|
||||||
if (shouldRest) {
|
if (shouldRest) {
|
||||||
await widget.activeUseCases.startRestAfterSet(
|
final rest = await widget.activeUseCases.startRestAfterSet(
|
||||||
sessionId: _session.metadata.id,
|
sessionId: _session.metadata.id,
|
||||||
afterProgramIndex: _position.programIndex,
|
afterProgramIndex: _position.programIndex,
|
||||||
afterExerciseIndex: _position.exerciseIndex,
|
afterExerciseIndex: _position.exerciseIndex,
|
||||||
@ -330,6 +336,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
plannedRestSeconds: _exercise.restSeconds,
|
plannedRestSeconds: _exercise.restSeconds,
|
||||||
);
|
);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
_activeRestStateId = rest.metadata.id;
|
||||||
_remainingRestSeconds = _exercise.restSeconds;
|
_remainingRestSeconds = _exercise.restSeconds;
|
||||||
_startRestTicker(next);
|
_startRestTicker(next);
|
||||||
setState(() => _mode = WorkoutExecutionMode.rest);
|
setState(() => _mode = WorkoutExecutionMode.rest);
|
||||||
@ -344,16 +351,73 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
if (_remainingRestSeconds <= 1) {
|
if (_remainingRestSeconds <= 1) {
|
||||||
timer.cancel();
|
timer.cancel();
|
||||||
unawaited(_moveTo(next));
|
unawaited(_finishRestAndMove(next));
|
||||||
} else {
|
} else {
|
||||||
setState(() => _remainingRestSeconds -= 1);
|
setState(() => _remainingRestSeconds -= 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> _restoreActiveRest() async {
|
||||||
|
final rest = await widget.activeUseCases.findActiveRest(
|
||||||
|
sessionId: _session.metadata.id,
|
||||||
|
);
|
||||||
|
if (!mounted || rest == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final after = ExecutionPosition(
|
||||||
|
programIndex: rest.afterProgramIndex,
|
||||||
|
exerciseIndex: rest.afterExerciseIndex,
|
||||||
|
setIndex: rest.afterSetIndex,
|
||||||
|
);
|
||||||
|
final next = _plan.nextPosition(after);
|
||||||
|
final remaining = _remainingSecondsFor(rest);
|
||||||
|
_activeRestStateId = rest.metadata.id;
|
||||||
|
if (next == null || remaining <= 0) {
|
||||||
|
await _finishRestAndMove(next);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
_remainingRestSeconds = remaining;
|
||||||
|
_startRestTicker(next);
|
||||||
|
if (!mounted) return true;
|
||||||
|
setState(() => _mode = WorkoutExecutionMode.rest);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int _remainingSecondsFor(ActiveRestState rest) {
|
||||||
|
final elapsedSeconds = DateTime.now()
|
||||||
|
.toUtc()
|
||||||
|
.difference(rest.startedAt.toUtc())
|
||||||
|
.inSeconds;
|
||||||
|
return (rest.adjustedRestSeconds - elapsedSeconds).clamp(0, 9999).toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _adjustRest(int deltaSeconds) async {
|
||||||
|
final restStateId = _activeRestStateId;
|
||||||
|
setState(() {
|
||||||
|
_remainingRestSeconds = (_remainingRestSeconds + deltaSeconds)
|
||||||
|
.clamp(0, 9999)
|
||||||
|
.toInt();
|
||||||
|
});
|
||||||
|
if (restStateId == null) return;
|
||||||
|
await widget.activeUseCases.adjustRestSeconds(
|
||||||
|
restStateId: restStateId,
|
||||||
|
deltaSeconds: deltaSeconds,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _skipRest() async {
|
Future<void> _skipRest() async {
|
||||||
_restTicker?.cancel();
|
_restTicker?.cancel();
|
||||||
final next = _plan.nextPosition(_position);
|
final next = _plan.nextPosition(_position);
|
||||||
|
await _finishRestAndMove(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _finishRestAndMove(ExecutionPosition? next) async {
|
||||||
|
final restStateId = _activeRestStateId;
|
||||||
|
_activeRestStateId = null;
|
||||||
|
if (restStateId != null) {
|
||||||
|
await widget.activeUseCases.skipRest(restStateId: restStateId);
|
||||||
|
}
|
||||||
if (next == null) {
|
if (next == null) {
|
||||||
await _complete();
|
await _complete();
|
||||||
return;
|
return;
|
||||||
@ -364,6 +428,8 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
Future<void> _moveTo(ExecutionPosition position) async {
|
Future<void> _moveTo(ExecutionPosition position) async {
|
||||||
_scoreController.clear();
|
_scoreController.clear();
|
||||||
_reps = 0;
|
_reps = 0;
|
||||||
|
_activeRestStateId = null;
|
||||||
|
_seriesStartedAt = DateTime.now().toUtc();
|
||||||
_session = await widget.activeUseCases.updateProgress(
|
_session = await widget.activeUseCases.updateProgress(
|
||||||
sessionId: _session.metadata.id,
|
sessionId: _session.metadata.id,
|
||||||
programIndex: position.programIndex,
|
programIndex: position.programIndex,
|
||||||
|
|||||||
@ -115,6 +115,108 @@ void main() {
|
|||||||
|
|
||||||
expect(find.text('Pas de repos'), findsOneWidget);
|
expect(find.text('Pas de repos'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('un repos actif est repris avec le temps restant recalculé', (
|
||||||
|
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;
|
||||||
|
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().subtract(const Duration(seconds: 15)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
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'), findsOneWidget);
|
||||||
|
expect(find.text('00:45'), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.tap(find.text('+15 s'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.text('01:00'), findsOneWidget);
|
||||||
|
expect(repository.restStates.single.adjustedRestSeconds, 75);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Ignorer le repos'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(repository.restStates.single.skippedAt, isNotNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('la mesure temps enregistre une durée réelle de série', (
|
||||||
|
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: 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
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(const Duration(seconds: 2));
|
||||||
|
await tester.tap(find.text('Terminer la série'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(repository.results.single.actualTimeMs, greaterThan(0));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ActiveWorkoutSessionUseCases _activeUseCases(
|
ActiveWorkoutSessionUseCases _activeUseCases(
|
||||||
@ -221,6 +323,7 @@ final class _FakeIds implements IdGenerator {
|
|||||||
final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||||||
ActiveWorkoutSession? session;
|
ActiveWorkoutSession? session;
|
||||||
final results = <ActiveSetResult>[];
|
final results = <ActiveSetResult>[];
|
||||||
|
final restStates = <ActiveRestState>[];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<ActiveWorkoutSession?> findById(String id) async {
|
Future<ActiveWorkoutSession?> findById(String id) async {
|
||||||
@ -231,11 +334,20 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
|||||||
Future<ActiveWorkoutSession?> findOpen() async => session;
|
Future<ActiveWorkoutSession?> findOpen() async => session;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<ActiveRestState?> findRestStateById(String id) async => null;
|
Future<ActiveRestState?> findRestStateById(String id) async {
|
||||||
|
for (final restState in restStates) {
|
||||||
|
if (restState.metadata.id == id) {
|
||||||
|
return restState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<ActiveRestState>> listRestStates(String sessionId) async {
|
Future<List<ActiveRestState>> listRestStates(String sessionId) async {
|
||||||
return const [];
|
return restStates
|
||||||
|
.where((restState) => restState.activeWorkoutSessionId == sessionId)
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -249,7 +361,16 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> saveRestState(ActiveRestState restState) async {}
|
Future<void> saveRestState(ActiveRestState restState) async {
|
||||||
|
final index = restStates.indexWhere(
|
||||||
|
(saved) => saved.metadata.id == restState.metadata.id,
|
||||||
|
);
|
||||||
|
if (index == -1) {
|
||||||
|
restStates.add(restState);
|
||||||
|
} else {
|
||||||
|
restStates[index] = restState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> saveSetResult(ActiveSetResult result) async {
|
Future<void> saveSetResult(ActiveSetResult result) async {
|
||||||
|
|||||||
Reference in New Issue
Block a user