feat(ui): écran Création/édition de programme

Ajoute l'écran de programme (presentation/program_screen.dart) et un
écran d'accueil (presentation/home_screen.dart) pour la navigation,
avec les ajustements application/domaine/repositories nécessaires.
flutter analyze propre, 11/11 tests verts, build APK debug validé.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 18:51:49 +02:00
parent 00ae95f5c0
commit 1ae6248fb4
11 changed files with 1218 additions and 11 deletions

View File

@ -109,6 +109,12 @@ 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 {}

View File

@ -0,0 +1,198 @@
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(
'ajoute un exercice au programme avec sa configuration par défaut',
(tester) async {
final exerciseRepository = _FakeExerciseRepository()
..exercises.add(
Exercise(
metadata: _metadata('exercise-1'),
name: 'Squat',
hasTimeMeasure: true,
hasRepsMeasure: true,
hasScoreMeasure: false,
),
);
await tester.pumpWidget(
MaterialApp(
home: ProgramFormScreen(
programUseCases: _programUseCases(
_FakeProgramRepository(),
exerciseRepository,
),
exerciseUseCases: _exerciseUseCases(exerciseRepository),
),
),
);
await tester.tap(find.text('Ajouter un exercice'));
await tester.pumpAndSettle();
await tester.tap(find.text('Squat'));
await tester.pumpAndSettle();
expect(find.text('Squat'), findsOneWidget);
expect(
find.text('3 séries · Temps + Répétitions · Repos 60 s'),
findsOneWidget,
);
expect(find.widgetWithText(CheckboxListTile, 'Temps'), findsOneWidget);
expect(
find.widgetWithText(CheckboxListTile, 'Répétitions'),
findsOneWidget,
);
},
);
testWidgets(
'limite les mesures aux mesures autorisées par lexercice source',
(tester) async {
final exerciseRepository = _FakeExerciseRepository()
..exercises.add(
Exercise(
metadata: _metadata('exercise-1'),
name: 'Pompes',
hasTimeMeasure: false,
hasRepsMeasure: true,
hasScoreMeasure: false,
),
);
await tester.pumpWidget(
MaterialApp(
home: ProgramFormScreen(
programUseCases: _programUseCases(
_FakeProgramRepository(),
exerciseRepository,
),
exerciseUseCases: _exerciseUseCases(exerciseRepository),
),
),
);
await tester.tap(find.text('Ajouter un exercice'));
await tester.pumpAndSettle();
await tester.tap(find.text('Pompes'));
await tester.pumpAndSettle();
expect(
find.widgetWithText(CheckboxListTile, 'Répétitions'),
findsOneWidget,
);
expect(find.widgetWithText(CheckboxListTile, 'Temps'), findsNothing);
expect(find.widgetWithText(CheckboxListTile, 'Score'), findsNothing);
},
);
}
ExerciseUseCases _exerciseUseCases(_FakeExerciseRepository repository) {
return ExerciseUseCases(
repository: repository,
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
ids: _FakeIds(),
originDeviceId: 'device-1',
);
}
ProgramUseCases _programUseCases(
_FakeProgramRepository programRepository,
_FakeExerciseRepository exerciseRepository,
) {
return ProgramUseCases(
programRepository: programRepository,
exerciseRepository: exerciseRepository,
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
ids: _FakeIds(),
originDeviceId: 'device-1',
);
}
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 _FakeExerciseRepository implements ExerciseRepository {
final exercises = <Exercise>[];
final saved = <Exercise>[];
@override
Future<Exercise?> findById(String id) async {
for (final exercise in exercises) {
if (exercise.metadata.id == id) {
return exercise;
}
}
return null;
}
@override
Future<bool> isReferencedByProgram(String id) async => false;
@override
Future<List<Exercise>> listActive() async => exercises;
@override
Future<void> save(Exercise exercise) async {
saved.add(exercise);
}
}
final class _FakeProgramRepository implements ProgramRepository {
final programs = <Program>[];
@override
Future<Program?> findById(String id) async {
for (final program in programs) {
if (program.metadata.id == id) {
return program;
}
}
return null;
}
@override
Future<List<Program>> listActive() async => programs;
@override
Future<void> replaceExercises(Program program, DateTime deletedAt) async {
programs.removeWhere((item) => item.metadata.id == program.metadata.id);
programs.add(program);
}
@override
Future<void> save(Program program) async {
programs.add(program);
}
@override
Future<void> saveExercise(ProgramExercise exercise) async {}
}