From 82226677a9cdeebd0a411e214cc0a2c58c2c28ae Mon Sep 17 00:00:00 2001 From: Blomios Date: Sat, 18 Jul 2026 09:57:07 +0200 Subject: [PATCH 1/2] =?UTF-8?q?fix(ui):=20popup=20s=C3=A9ance=20d=C3=A9j?= =?UTF-8?q?=C3=A0=20en=20cours=20(ticket=20#33)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Corrige le popup averti quand une séance est déjà en cours sur workout_template_screen.dart. flutter analyze propre, 46/46 tests verts, build APK debug validé. Ticket laissé en QA : validation manuelle sur APK par l'utilisateur avant clôture. Clôture le sprint Bug resolution (tickets #24, #29, #31, #32, #33). Co-Authored-By: Claude Opus 4.8 --- lib/presentation/workout_template_screen.dart | 13 + .../workout_template_screen_test.dart | 303 ++++++++++++++++++ 2 files changed, 316 insertions(+) diff --git a/lib/presentation/workout_template_screen.dart b/lib/presentation/workout_template_screen.dart index 9666990..a95d7ed 100644 --- a/lib/presentation/workout_template_screen.dart +++ b/lib/presentation/workout_template_screen.dart @@ -111,6 +111,19 @@ final class _WorkoutTemplateListScreenState } Future _start(WorkoutTemplate template) async { + final openSession = await widget.activeUseCases.findOpen(); + if (!mounted) return; + if (openSession != null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Une séance est déjà en cours. Termine-la ou reprends-la avant ' + 'd’en lancer une nouvelle.', + ), + ), + ); + return; + } final session = await widget.activeUseCases.startFromTemplate( template.metadata.id, ); diff --git a/test/presentation/workout_template_screen_test.dart b/test/presentation/workout_template_screen_test.dart index 6e3dfd1..bb323a7 100644 --- a/test/presentation/workout_template_screen_test.dart +++ b/test/presentation/workout_template_screen_test.dart @@ -1,5 +1,9 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:gametime/application/application.dart'; +import 'package:gametime/domain/domain.dart'; import 'package:gametime/presentation/presentation.dart'; void main() { @@ -39,4 +43,303 @@ void main() { expect(find.text('5 séries · Temps + Répétitions'), findsOneWidget); }, ); + + testWidgets( + 'lancer une séance alors qu’une autre est en cours affiche un message', + (tester) async { + final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); + final templateRepository = _FakeWorkoutTemplateRepository([ + _workoutTemplate(), + ]); + final activeRepository = _FakeActiveSessionRepository() + ..session = _activeSession(); + final programRepository = _FakeProgramRepository(); + final historyRepository = _FakeWorkoutHistoryRepository(); + + await tester.pumpWidget( + MaterialApp( + home: WorkoutTemplateListScreen( + workoutTemplateUseCases: WorkoutTemplateUseCases( + templateRepository: templateRepository, + programRepository: programRepository, + clock: clock, + ids: _FakeIds(), + originDeviceId: 'device-1', + ), + programUseCases: ProgramUseCases( + programRepository: programRepository, + exerciseRepository: _FakeExerciseRepository(), + clock: clock, + ids: _FakeIds(), + originDeviceId: 'device-1', + ), + activeUseCases: ActiveWorkoutSessionUseCases( + sessionRepository: activeRepository, + templateRepository: templateRepository, + clock: clock, + ids: _FakeIds(), + originDeviceId: 'device-1', + ), + closeUseCase: CloseWorkoutSessionUseCase( + sessionRepository: activeRepository, + historyRepository: historyRepository, + clock: clock, + ids: _FakeIds(), + originDeviceId: 'device-1', + ), + historyUseCases: WorkoutHistoryUseCases( + repository: historyRepository, + clock: clock, + ), + ), + ), + ); + await tester.pump(); + await tester.pump(); + + await tester.tap(find.text('Lancer')); + await tester.pump(); + await tester.pump(); + + expect( + find.textContaining('Une séance est déjà en cours.'), + findsOneWidget, + ); + expect(activeRepository.saveCount, 0); + }, + ); +} + +WorkoutTemplate _workoutTemplate() { + return WorkoutTemplate( + metadata: _metadata('template-1'), + name: 'Séance jambes', + programs: [ + WorkoutTemplateProgram( + metadata: _metadata('template-program-1'), + workoutTemplateId: 'template-1', + position: 0, + programNameSnapshot: 'Programme jambes', + defaultRestSecondsSnapshot: 60, + programSnapshotJson: _programSnapshotJson(), + ), + ], + ); +} + +ActiveWorkoutSession _activeSession() { + return ActiveWorkoutSession( + metadata: _metadata('session-1'), + sourceWorkoutTemplateId: 'template-active', + 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: jsonEncode({ + 'name': 'Séance active', + 'programs': [ + { + 'id': 'template-program-1', + 'programNameSnapshot': 'Programme jambes', + 'programSnapshotJson': _programSnapshotJson(), + }, + ], + 'overrides': const [], + }), + ); +} + +String _programSnapshotJson() { + return jsonEncode({ + 'exercises': [ + { + 'id': 'exercise-1', + 'exerciseNameSnapshot': 'Squat', + 'setsCount': 3, + 'timeEnabled': true, + 'repsEnabled': false, + 'scoreEnabled': false, + 'restSecondsOverride': 60, + }, + ], + }); +} + +EntityMetadata _metadata(String id) { + return EntityMetadata( + id: id, + createdAt: DateTime.utc(2026, 7, 17), + updatedAt: DateTime.utc(2026, 7, 17), + originDeviceId: 'device-1', + ); +} + +final class _FakeClock implements Clock { + const _FakeClock(this.value); + + final DateTime value; + + @override + DateTime now() => value; +} + +final class _FakeIds implements IdGenerator { + var _next = 0; + + @override + String newId() { + _next += 1; + return 'id-$_next'; + } +} + +final class _FakeWorkoutTemplateRepository + implements WorkoutTemplateRepository { + _FakeWorkoutTemplateRepository(this.templates); + + final List templates; + + @override + Future findById(String id) async { + return templates.cast().firstWhere( + (template) => template?.metadata.id == id, + orElse: () => null, + ); + } + + @override + Future> listActive() async => templates; + + @override + Future replaceComposition( + WorkoutTemplate template, + DateTime deletedAt, + ) async {} + + @override + Future save(WorkoutTemplate template) async {} + + @override + Future saveOverride(WorkoutTemplateExerciseOverride override) async {} + + @override + Future saveProgram(WorkoutTemplateProgram program) async {} +} + +final class _FakeActiveSessionRepository implements ActiveSessionRepository { + ActiveWorkoutSession? session; + var saveCount = 0; + + @override + Future findById(String id) async { + return session?.metadata.id == id ? session : null; + } + + @override + Future findOpen() async => session; + + @override + Future findRestStateById(String id) async => null; + + @override + Future findScoreStopwatchState({ + required String sessionId, + required int programIndex, + required int exerciseIndex, + required int setIndex, + }) async { + return null; + } + + @override + Future> listRestStates(String sessionId) async { + return const []; + } + + @override + Future> listScoreStopwatchStates( + String sessionId, + ) async { + return const []; + } + + @override + Future> listSetResults(String sessionId) async { + return const []; + } + + @override + Future save(ActiveWorkoutSession session) async { + saveCount += 1; + this.session = session; + } + + @override + Future saveRestState(ActiveRestState restState) async {} + + @override + Future saveScoreStopwatchState(ActiveScoreStopwatchState state) async {} + + @override + Future deleteScoreStopwatchState({ + required String sessionId, + required int programIndex, + required int exerciseIndex, + required int setIndex, + required DateTime deletedAt, + }) async {} + + @override + Future saveSetResult(ActiveSetResult result) async {} +} + +final class _FakeProgramRepository implements ProgramRepository { + @override + Future findById(String id) async => null; + + @override + Future> listActive() async => const []; + + @override + Future replaceExercises(Program program, DateTime deletedAt) async {} + + @override + Future save(Program program) async {} + + @override + Future saveExercise(ProgramExercise exercise) async {} +} + +final class _FakeExerciseRepository implements ExerciseRepository { + @override + Future findById(String id) async => null; + + @override + Future isReferencedByProgram(String id) async => false; + + @override + Future> listActive() async => const []; + + @override + Future save(Exercise exercise) async {} +} + +final class _FakeWorkoutHistoryRepository implements WorkoutHistoryRepository { + @override + Future delete(String id, DateTime deletedAt) async {} + + @override + Future findById(String id) async => null; + + @override + Future> listActive() async => const []; + + @override + Future save(WorkoutHistory history) async {} + + @override + Future saveSetResult(WorkoutHistorySetResult result) async {} } From 6345f6d405d0db990e9f08bb116bc4fb31fbbbb4 Mon Sep 17 00:00:00 2001 From: Blomios Date: Sat, 18 Jul 2026 09:57:10 +0200 Subject: [PATCH 2/2] =?UTF-8?q?docs(ideai):=20met=20=C3=A0=20jour=20le=20t?= =?UTF-8?q?icket=20#33=20=E2=80=94=20correctif=20appliqu=C3=A9,=20en=20att?= =?UTF-8?q?ente=20de=20validation=20QA=20utilisateur?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- .ideai/tickets/33/carnet.md | 9 ++++++--- .ideai/tickets/33/issue.md | 8 ++++---- .ideai/tickets/index.json | 4 ++-- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/.ideai/tickets/33/carnet.md b/.ideai/tickets/33/carnet.md index 261b02f..f46e954 100644 --- a/.ideai/tickets/33/carnet.md +++ b/.ideai/tickets/33/carnet.md @@ -1,6 +1,9 @@ --- issueRef: "#33" -version: 4 -updatedBy: {"kind":"user"} -updatedAt: 1784329182115 +version: 6 +updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} +updatedAt: 1784361411170 --- +Correctif : tenter de lancer une séance alors qu'une autre est déjà active affiche maintenant un SnackBar explicite ("Une séance est déjà en cours. Termine-la ou reprends-la avant d'en lancer une nouvelle."), au lieu de ne rien faire silencieusement. Vérifié : analyze propre, 46/46 tests verts, build APK réussi. + +À tester sur le téléphone : lancer une séance, puis sans la terminer, tenter d'en lancer une deuxième — le message doit s'afficher. \ No newline at end of file diff --git a/.ideai/tickets/33/issue.md b/.ideai/tickets/33/issue.md index 961649b..3bd5934 100644 --- a/.ideai/tickets/33/issue.md +++ b/.ideai/tickets/33/issue.md @@ -2,15 +2,15 @@ id: "a588ff04-9e55-4492-ac76-5298ba94e5c7" number: 33 title: "[Bug] Impossible de lancer une seance" -status: "open" +status: "qa" priority: "medium" sprint: "abc4f969-b169-45f7-988c-daeeab762201" links: [] agentRefs: [] createdBy: {"kind":"user"} -updatedBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} createdAt: 1784329082771 -updatedAt: 1784329182115 -version: 4 +updatedAt: 1784361411170 +version: 6 --- Si une seance est déjà en cours je ne peux poas en lancer une autre. Je trouve ça bien, j'aimerais juste qu'une petite popup me le dise car on a actuellement aucun retour pour ça \ No newline at end of file diff --git a/.ideai/tickets/index.json b/.ideai/tickets/index.json index a4db73c..85776a2 100644 --- a/.ideai/tickets/index.json +++ b/.ideai/tickets/index.json @@ -371,11 +371,11 @@ "issueRef": "#33", "path": "33", "title": "[Bug] Impossible de lancer une seance", - "status": "open", + "status": "qa", "priority": "medium", "sprint": "abc4f969-b169-45f7-988c-daeeab762201", "assignedAgentIds": [], - "updatedAt": 1784329182115 + "updatedAt": 1784361411170 }, { "issueRef": "#34",