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 {} }