fix(ui): popup séance déjà en cours (ticket #33)
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 <noreply@anthropic.com>
This commit is contained in:
@ -111,6 +111,19 @@ final class _WorkoutTemplateListScreenState
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _start(WorkoutTemplate template) async {
|
Future<void> _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(
|
final session = await widget.activeUseCases.startFromTemplate(
|
||||||
template.metadata.id,
|
template.metadata.id,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.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';
|
import 'package:gametime/presentation/presentation.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
@ -39,4 +43,303 @@ void main() {
|
|||||||
expect(find.text('5 séries · Temps + Répétitions'), findsOneWidget);
|
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<WorkoutTemplate> templates;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<WorkoutTemplate?> findById(String id) async {
|
||||||
|
return templates.cast<WorkoutTemplate?>().firstWhere(
|
||||||
|
(template) => template?.metadata.id == id,
|
||||||
|
orElse: () => null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<WorkoutTemplate>> listActive() async => templates;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> replaceComposition(
|
||||||
|
WorkoutTemplate template,
|
||||||
|
DateTime deletedAt,
|
||||||
|
) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> save(WorkoutTemplate template) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> saveOverride(WorkoutTemplateExerciseOverride override) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> saveProgram(WorkoutTemplateProgram program) async {}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||||||
|
ActiveWorkoutSession? session;
|
||||||
|
var saveCount = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ActiveWorkoutSession?> findById(String id) async {
|
||||||
|
return session?.metadata.id == id ? session : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ActiveWorkoutSession?> findOpen() async => session;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ActiveRestState?> findRestStateById(String id) async => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ActiveScoreStopwatchState?> findScoreStopwatchState({
|
||||||
|
required String sessionId,
|
||||||
|
required int programIndex,
|
||||||
|
required int exerciseIndex,
|
||||||
|
required int setIndex,
|
||||||
|
}) async {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<ActiveRestState>> listRestStates(String sessionId) async {
|
||||||
|
return const [];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<ActiveScoreStopwatchState>> listScoreStopwatchStates(
|
||||||
|
String sessionId,
|
||||||
|
) async {
|
||||||
|
return const [];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<ActiveSetResult>> listSetResults(String sessionId) async {
|
||||||
|
return const [];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> save(ActiveWorkoutSession session) async {
|
||||||
|
saveCount += 1;
|
||||||
|
this.session = session;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> saveRestState(ActiveRestState restState) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> saveScoreStopwatchState(ActiveScoreStopwatchState state) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> deleteScoreStopwatchState({
|
||||||
|
required String sessionId,
|
||||||
|
required int programIndex,
|
||||||
|
required int exerciseIndex,
|
||||||
|
required int setIndex,
|
||||||
|
required DateTime deletedAt,
|
||||||
|
}) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> saveSetResult(ActiveSetResult result) async {}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class _FakeProgramRepository implements ProgramRepository {
|
||||||
|
@override
|
||||||
|
Future<Program?> findById(String id) async => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<Program>> listActive() async => const [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> replaceExercises(Program program, DateTime deletedAt) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> save(Program program) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> saveExercise(ProgramExercise exercise) async {}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class _FakeExerciseRepository implements ExerciseRepository {
|
||||||
|
@override
|
||||||
|
Future<Exercise?> findById(String id) async => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> isReferencedByProgram(String id) async => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<Exercise>> listActive() async => const [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> save(Exercise exercise) async {}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class _FakeWorkoutHistoryRepository implements WorkoutHistoryRepository {
|
||||||
|
@override
|
||||||
|
Future<void> delete(String id, DateTime deletedAt) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<WorkoutHistory?> findById(String id) async => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<WorkoutHistory>> listActive() async => const [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> save(WorkoutHistory history) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> saveSetResult(WorkoutHistorySetResult result) async {}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user