merge(main): popup séance déjà en cours (ticket #33)
Fusionne feature/#33-popup-seance-deja-en-cours — analyze propre, 46/46 tests verts, build APK debug validé. Ticket laissé en statut 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:
@ -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.
|
||||
@ -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
|
||||
@ -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",
|
||||
|
||||
@ -111,6 +111,19 @@ final class _WorkoutTemplateListScreenState
|
||||
}
|
||||
|
||||
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(
|
||||
template.metadata.id,
|
||||
);
|
||||
|
||||
@ -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<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