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>
346 lines
9.1 KiB
Dart
346 lines
9.1 KiB
Dart
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() {
|
||
testWidgets(
|
||
'la vue programme de séance ne permet pas de changer les mesures actives',
|
||
(tester) async {
|
||
final program = WorkoutTemplateProgramDraft(
|
||
clientKey: 'program-1',
|
||
programNameSnapshot: 'Jambes',
|
||
defaultRestSecondsSnapshot: 60,
|
||
programSnapshotJson: '{}',
|
||
exercises: [
|
||
WorkoutTemplateExerciseDraft(
|
||
snapshotExerciseId: 'exercise-1',
|
||
exerciseNameSnapshot: 'Squat',
|
||
setsCount: 3,
|
||
timeEnabled: true,
|
||
repsEnabled: true,
|
||
scoreEnabled: false,
|
||
targetTimeSeconds: 45,
|
||
targetReps: 10,
|
||
),
|
||
],
|
||
);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(home: WorkoutTemplateProgramDetailScreen(program: program)),
|
||
);
|
||
|
||
expect(find.byType(CheckboxListTile), findsNothing);
|
||
expect(find.text('Mesures à suivre'), findsNothing);
|
||
|
||
await tester.enterText(find.byType(TextFormField).first, '5');
|
||
await tester.pump();
|
||
|
||
expect(program.exercises.single.setsCountOverride, 5);
|
||
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 {}
|
||
}
|