fix(ui): bandeau de reprise de séance en cours (ticket #32)
Ajoute/corrige le bandeau de reprise sur home_screen.dart, avec les ajustements de bootstrap applicatif nécessaires, et sa couverture de tests. flutter analyze propre, 45/45 tests verts, build APK debug validé. Ticket laissé en QA : validation manuelle sur APK par l'utilisateur avant clôture. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
417
test/presentation/home_screen_test.dart
Normal file
417
test/presentation/home_screen_test.dart
Normal file
@ -0,0 +1,417 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:gametime/application/application.dart';
|
||||
import 'package:gametime/application/app_bootstrap.dart';
|
||||
import 'package:gametime/domain/domain.dart';
|
||||
import 'package:gametime/presentation/home_screen.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('le bandeau de reprise apparaît au retour sur l’accueil', (
|
||||
tester,
|
||||
) async {
|
||||
final navigatorKey = GlobalKey<NavigatorState>();
|
||||
final activeRepository = _FakeActiveSessionRepository();
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
navigatorKey: navigatorKey,
|
||||
navigatorObservers: [homeRouteObserver],
|
||||
home: HomeScreen(bootstrap: _FakeBootstrap(activeRepository)),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
expect(find.textContaining('Séance en cours'), findsNothing);
|
||||
|
||||
unawaited(
|
||||
navigatorKey.currentState!.push<void>(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const Scaffold(body: Text('Séance active')),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 350));
|
||||
|
||||
activeRepository.session = ActiveWorkoutSession(
|
||||
metadata: _metadata('session-1'),
|
||||
sourceWorkoutTemplateId: 'template-1',
|
||||
status: ActiveWorkoutStatus.paused,
|
||||
startedAt: DateTime.utc(2026, 7, 17, 12),
|
||||
pausedAt: DateTime.utc(2026, 7, 17, 12, 5),
|
||||
lastPersistedAt: DateTime.utc(2026, 7, 17, 12, 5),
|
||||
elapsedActiveMs: 300000,
|
||||
currentProgramIndex: 0,
|
||||
currentExerciseIndex: 0,
|
||||
currentSetIndex: 1,
|
||||
resolvedTemplateSnapshotJson: _sessionSnapshot(),
|
||||
);
|
||||
|
||||
navigatorKey.currentState!.pop();
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 350));
|
||||
await tester.pump();
|
||||
|
||||
expect(
|
||||
find.text('Séance en cours — Reprendre · Squat, série 2/3'),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('le bandeau de reprise disparaît au retour après abandon', (
|
||||
tester,
|
||||
) async {
|
||||
final navigatorKey = GlobalKey<NavigatorState>();
|
||||
final activeRepository = _FakeActiveSessionRepository()
|
||||
..session = ActiveWorkoutSession(
|
||||
metadata: _metadata('session-1'),
|
||||
sourceWorkoutTemplateId: 'template-1',
|
||||
status: ActiveWorkoutStatus.paused,
|
||||
startedAt: DateTime.utc(2026, 7, 17, 12),
|
||||
pausedAt: DateTime.utc(2026, 7, 17, 12, 5),
|
||||
lastPersistedAt: DateTime.utc(2026, 7, 17, 12, 5),
|
||||
elapsedActiveMs: 300000,
|
||||
currentProgramIndex: 0,
|
||||
currentExerciseIndex: 0,
|
||||
currentSetIndex: 1,
|
||||
resolvedTemplateSnapshotJson: _sessionSnapshot(),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
navigatorKey: navigatorKey,
|
||||
navigatorObservers: [homeRouteObserver],
|
||||
home: HomeScreen(bootstrap: _FakeBootstrap(activeRepository)),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
expect(find.textContaining('Séance en cours'), findsOneWidget);
|
||||
|
||||
unawaited(
|
||||
navigatorKey.currentState!.push<void>(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const Scaffold(body: Text('Séance active')),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 350));
|
||||
|
||||
activeRepository.session = null;
|
||||
navigatorKey.currentState!.pop();
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 350));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.textContaining('Séance en cours'), findsNothing);
|
||||
});
|
||||
}
|
||||
|
||||
final class _FakeBootstrap implements AppDependencies {
|
||||
_FakeBootstrap(_FakeActiveSessionRepository activeRepository)
|
||||
: activeWorkoutSessionUseCases = ActiveWorkoutSessionUseCases(
|
||||
sessionRepository: activeRepository,
|
||||
templateRepository: _FakeWorkoutTemplateRepository(),
|
||||
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
|
||||
ids: _FakeIds(),
|
||||
originDeviceId: 'device-1',
|
||||
),
|
||||
closeWorkoutSessionUseCase = CloseWorkoutSessionUseCase(
|
||||
sessionRepository: activeRepository,
|
||||
historyRepository: _FakeWorkoutHistoryRepository(),
|
||||
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
|
||||
ids: _FakeIds(),
|
||||
originDeviceId: 'device-1',
|
||||
),
|
||||
exerciseUseCases = ExerciseUseCases(
|
||||
repository: _FakeExerciseRepository(),
|
||||
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
|
||||
ids: _FakeIds(),
|
||||
originDeviceId: 'device-1',
|
||||
),
|
||||
mediaUseCases = MediaUseCases(
|
||||
mediaRepository: _FakeMediaAssetRepository(),
|
||||
exerciseRepository: _FakeExerciseRepository(),
|
||||
storage: _FakeMediaStorage(),
|
||||
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
|
||||
ids: _FakeIds(),
|
||||
originDeviceId: 'device-1',
|
||||
),
|
||||
programUseCases = ProgramUseCases(
|
||||
programRepository: _FakeProgramRepository(),
|
||||
exerciseRepository: _FakeExerciseRepository(),
|
||||
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
|
||||
ids: _FakeIds(),
|
||||
originDeviceId: 'device-1',
|
||||
),
|
||||
workoutTemplateUseCases = WorkoutTemplateUseCases(
|
||||
templateRepository: _FakeWorkoutTemplateRepository(),
|
||||
programRepository: _FakeProgramRepository(),
|
||||
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
|
||||
ids: _FakeIds(),
|
||||
originDeviceId: 'device-1',
|
||||
),
|
||||
workoutHistoryUseCases = WorkoutHistoryUseCases(
|
||||
repository: _FakeWorkoutHistoryRepository(),
|
||||
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
|
||||
);
|
||||
|
||||
@override
|
||||
final ExerciseUseCases exerciseUseCases;
|
||||
|
||||
@override
|
||||
final MediaUseCases mediaUseCases;
|
||||
|
||||
@override
|
||||
final ProgramUseCases programUseCases;
|
||||
|
||||
@override
|
||||
final WorkoutTemplateUseCases workoutTemplateUseCases;
|
||||
|
||||
@override
|
||||
final ActiveWorkoutSessionUseCases activeWorkoutSessionUseCases;
|
||||
|
||||
@override
|
||||
final CloseWorkoutSessionUseCase closeWorkoutSessionUseCase;
|
||||
|
||||
@override
|
||||
final WorkoutHistoryUseCases workoutHistoryUseCases;
|
||||
}
|
||||
|
||||
String _sessionSnapshot() {
|
||||
return jsonEncode({
|
||||
'name': 'Séance jambes',
|
||||
'programs': [
|
||||
{
|
||||
'id': 'template-program-1',
|
||||
'programNameSnapshot': 'Programme jambes',
|
||||
'programSnapshotJson': jsonEncode({
|
||||
'exercises': [
|
||||
{
|
||||
'id': 'exercise-1',
|
||||
'exerciseNameSnapshot': 'Squat',
|
||||
'setsCount': 3,
|
||||
'timeEnabled': true,
|
||||
'repsEnabled': false,
|
||||
'scoreEnabled': false,
|
||||
'restSecondsOverride': 0,
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
'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 {
|
||||
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 _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||||
ActiveWorkoutSession? session;
|
||||
final restStates = <ActiveRestState>[];
|
||||
final scoreStopwatchStates = <ActiveScoreStopwatchState>[];
|
||||
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<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 restStates;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ActiveScoreStopwatchState>> listScoreStopwatchStates(
|
||||
String sessionId,
|
||||
) async {
|
||||
return scoreStopwatchStates;
|
||||
}
|
||||
|
||||
@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 {
|
||||
restStates.add(restState);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> saveScoreStopwatchState(ActiveScoreStopwatchState state) async {
|
||||
scoreStopwatchStates.add(state);
|
||||
}
|
||||
|
||||
@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 {
|
||||
results.add(result);
|
||||
}
|
||||
}
|
||||
|
||||
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 _FakeMediaAssetRepository implements MediaAssetRepository {
|
||||
@override
|
||||
Future<MediaAsset?> findById(String id) async => null;
|
||||
|
||||
@override
|
||||
Future<List<MediaAsset>> listActive() async => const [];
|
||||
|
||||
@override
|
||||
Future<void> save(MediaAsset mediaAsset) async {}
|
||||
}
|
||||
|
||||
final class _FakeMediaStorage implements LocalMediaStorage {
|
||||
@override
|
||||
Future<void> deleteByLocalUri(String localUri) async {}
|
||||
|
||||
@override
|
||||
Future<StoredMediaFile> importFile({
|
||||
required String sourcePath,
|
||||
required MediaKind kind,
|
||||
required String stableFileName,
|
||||
}) async {
|
||||
return StoredMediaFile(
|
||||
localUri: 'file:///$stableFileName',
|
||||
mimeType: 'image/png',
|
||||
sizeBytes: 0,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Set<String>> listManagedLocalUris() async => const {};
|
||||
}
|
||||
|
||||
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 _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> 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