feat(ui): écran Exécution de séance

Ajoute l'écran d'exécution de séance (presentation/workout_execution_screen.dart),
avec les ajustements application/domaine/repositories nécessaires et la
navigation depuis l'accueil et l'écran séance-modèle. flutter analyze
propre, 15/15 tests verts, build APK debug validé.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 19:08:23 +02:00
parent 6bc09fe1ca
commit bfd727a6a2
9 changed files with 1159 additions and 8 deletions

View File

@ -0,0 +1,268 @@
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('le temps affiché est recalculé depuis les horodatages', (
tester,
) async {
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12, 0, 15));
final session = ActiveWorkoutSession(
metadata: _metadata('session-1'),
sourceWorkoutTemplateId: 'template-1',
status: ActiveWorkoutStatus.running,
startedAt: DateTime.utc(2026, 7, 17, 11, 59),
lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
elapsedActiveMs: 30000,
currentProgramIndex: 0,
currentExerciseIndex: 0,
currentSetIndex: 0,
resolvedTemplateSnapshotJson: _sessionSnapshot(),
);
await tester.pumpWidget(
MaterialApp(
home: WorkoutExecutionScreen(
initialSession: session,
activeUseCases: _activeUseCases(
_FakeActiveSessionRepository(),
clock,
),
closeUseCase: _closeUseCase(_FakeActiveSessionRepository(), clock),
),
),
);
expect(find.text('00:45'), findsOneWidget);
});
testWidgets('les mesures cumulées respectent la hiérarchie visuelle', (
tester,
) async {
final scoreController = TextEditingController();
addTearDown(scoreController.dispose);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: SetMeasureInput(
exercise: const ExecutionExercise(
id: 'exercise-1',
name: 'Squat',
setsCount: 3,
timeEnabled: true,
repsEnabled: true,
scoreEnabled: true,
restSeconds: 0,
scoreUnit: 'kg',
),
reps: 0,
scoreController: scoreController,
onRepsChanged: (_) {},
),
),
),
);
final timeY = tester.getTopLeft(find.text('Temps')).dy;
final repsY = tester.getTopLeft(find.text('Répétitions')).dy;
final scoreY = tester.getTopLeft(find.text('Score (kg)')).dy;
expect(timeY, lessThan(repsY));
expect(repsY, lessThan(scoreY));
});
testWidgets('aucun repos ne saffiche après la toute dernière série', (
tester,
) async {
final plan = WorkoutExecutionPlan.fromSession(
ActiveWorkoutSession(
metadata: _metadata('session-1'),
sourceWorkoutTemplateId: 'template-1',
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: _sessionSnapshot(
setsCount: 1,
restSeconds: 60,
),
),
);
const position = ExecutionPosition(
programIndex: 0,
exerciseIndex: 0,
setIndex: 0,
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Text(
plan.shouldShowRestAfter(position) ? 'Repos' : 'Pas de repos',
),
),
);
expect(find.text('Pas de repos'), findsOneWidget);
});
}
ActiveWorkoutSessionUseCases _activeUseCases(
_FakeActiveSessionRepository repository,
_FakeClock clock,
) {
return ActiveWorkoutSessionUseCases(
sessionRepository: repository,
templateRepository: _FakeWorkoutTemplateRepository(),
clock: clock,
ids: _FakeIds(),
originDeviceId: 'device-1',
);
}
CloseWorkoutSessionUseCase _closeUseCase(
_FakeActiveSessionRepository repository,
_FakeClock clock,
) {
return CloseWorkoutSessionUseCase(
sessionRepository: repository,
historyRepository: _FakeWorkoutHistoryRepository(),
clock: clock,
ids: _FakeIds(),
originDeviceId: 'device-1',
);
}
String _sessionSnapshot({int setsCount = 3, int restSeconds = 0}) {
return jsonEncode({
'name': 'Séance jambes',
'programs': [
{
'id': 'template-program-1',
'programNameSnapshot': 'Programme jambes',
'programSnapshotJson': jsonEncode({
'exercises': [
{
'id': 'exercise-1',
'exerciseNameSnapshot': 'Squat',
'setsCount': setsCount,
'timeEnabled': true,
'repsEnabled': true,
'scoreEnabled': true,
'targetTimeSeconds': 45,
'targetReps': 10,
'targetScore': 80,
'scoreUnitSnapshot': 'kg',
'restSecondsOverride': restSeconds,
},
],
}),
},
],
'overrides': const [],
});
}
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 {
_FakeClock(this.value);
DateTime value;
@override
DateTime now() => value;
}
final class _FakeIds implements IdGenerator {
var _next = 0;
@override
String newId() {
_next += 1;
return 'id-$_next';
}
}
final class _FakeActiveSessionRepository implements ActiveSessionRepository {
ActiveWorkoutSession? session;
final results = <ActiveSetResult>[];
@override
Future<ActiveWorkoutSession?> findById(String id) async {
return session?.metadata.id == id ? session : null;
}
@override
Future<ActiveWorkoutSession?> findOpen() async => session;
@override
Future<List<ActiveRestState>> listRestStates(String sessionId) async {
return const [];
}
@override
Future<List<ActiveSetResult>> listSetResults(String sessionId) async {
return results;
}
@override
Future<void> save(ActiveWorkoutSession session) async {
this.session = session;
}
@override
Future<void> saveRestState(ActiveRestState restState) async {}
@override
Future<void> saveSetResult(ActiveSetResult result) async {
results.add(result);
}
}
final class _FakeWorkoutTemplateRepository
implements WorkoutTemplateRepository {
@override
Future<WorkoutTemplate?> findById(String id) async => null;
@override
Future<List<WorkoutTemplate>> listActive() async => const [];
@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 _FakeWorkoutHistoryRepository implements WorkoutHistoryRepository {
@override
Future<void> save(WorkoutHistory history) async {}
@override
Future<void> saveSetResult(WorkoutHistorySetResult result) async {}
}