fix(qa): corrections issues de la passe QA fonctionnelle

Corrige les bugs identifiés lors de la QA finale sur use_cases.dart et
drift_repositories.dart, ajoute la couverture de tests d'infrastructure
manquante (test/infrastructure/drift_repositories_test.dart).
flutter analyze propre, 21/21 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:28:38 +02:00
parent 9d91d66820
commit 7b6d21450b
4 changed files with 399 additions and 1 deletions

View File

@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:gametime/application/application.dart';
import 'package:gametime/domain/domain.dart';
@ -35,6 +37,55 @@ void main() {
);
});
test(
'Workout template use case rejects target overrides for inactive measures',
() async {
final useCase = WorkoutTemplateUseCases(
templateRepository: _FakeWorkoutTemplateRepository(),
programRepository: _FakeProgramRepository(),
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
ids: _FakeIds(),
originDeviceId: 'device-1',
);
await expectLater(
useCase.saveConfigured(
name: 'Séance tir',
programs: [
WorkoutTemplateProgramConfig(
clientKey: 'program-1',
programNameSnapshot: 'Programme tir',
defaultRestSecondsSnapshot: 30,
programSnapshotJson: jsonEncode({
'exercises': [
{
'id': 'exercise-snapshot-1',
'exerciseNameSnapshot': 'Lancers francs',
'setsCount': 3,
'timeEnabled': true,
'repsEnabled': false,
'scoreEnabled': false,
'targetTimeSeconds': 60,
'targetReps': null,
'targetScore': null,
},
],
}),
),
],
overrides: const [
WorkoutTemplateExerciseOverrideConfig(
workoutTemplateProgramClientKey: 'program-1',
snapshotProgramExerciseId: 'exercise-snapshot-1',
targetRepsOverride: 10,
),
],
),
throwsA(isA<DomainException>()),
);
},
);
test('Active session elapsed time is based on persisted timestamps', () {
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
final session = ActiveWorkoutSession(

View File

@ -0,0 +1,196 @@
import 'dart:convert';
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:gametime/application/application.dart';
import 'package:gametime/domain/domain.dart';
import 'package:gametime/infrastructure/local/local.dart' as local;
void main() {
late local.AppDatabase database;
late local.DriftActiveSessionRepository activeRepository;
late local.DriftWorkoutTemplateRepository templateRepository;
late local.DriftWorkoutHistoryRepository historyRepository;
setUp(() {
database = local.AppDatabase(NativeDatabase.memory());
activeRepository = local.DriftActiveSessionRepository(database);
templateRepository = local.DriftWorkoutTemplateRepository(database);
historyRepository = local.DriftWorkoutHistoryRepository(database);
});
tearDown(() async {
await database.close();
});
test(
'running session elapsed time survives repository reconstruction',
() async {
final persistedAt = DateTime.utc(2026, 7, 17, 12);
final session = ActiveWorkoutSession(
metadata: _metadata('session-1', persistedAt),
sourceWorkoutTemplateId: null,
status: ActiveWorkoutStatus.running,
startedAt: persistedAt.subtract(const Duration(seconds: 30)),
lastPersistedAt: persistedAt,
elapsedActiveMs: 30000,
currentProgramIndex: 0,
currentExerciseIndex: 0,
currentSetIndex: 0,
resolvedTemplateSnapshotJson: _resolvedSnapshot(),
);
await activeRepository.save(session);
final afterAppKillRepository = local.DriftActiveSessionRepository(
database,
);
final afterAppKillUseCases = ActiveWorkoutSessionUseCases(
sessionRepository: afterAppKillRepository,
templateRepository: templateRepository,
clock: _FakeClock(persistedAt.add(const Duration(seconds: 20))),
ids: _FakeIds(),
originDeviceId: 'device-1',
);
final restored = await afterAppKillRepository.findOpen();
expect(restored, isNotNull);
expect(afterAppKillUseCases.elapsedActiveMilliseconds(restored!), 50000);
},
);
test(
'closing a session stores autonomous history rows with set snapshots',
() async {
final now = DateTime.utc(2026, 7, 17, 12);
final template = WorkoutTemplate(
metadata: _metadata('template-1', now),
name: 'Séance jambes',
);
await templateRepository.save(template);
final session = ActiveWorkoutSession(
metadata: _metadata('session-1', now),
sourceWorkoutTemplateId: template.metadata.id,
status: ActiveWorkoutStatus.running,
startedAt: now.subtract(const Duration(minutes: 10)),
lastPersistedAt: now,
elapsedActiveMs: 600000,
currentProgramIndex: 0,
currentExerciseIndex: 0,
currentSetIndex: 0,
resolvedTemplateSnapshotJson: _resolvedSnapshot(),
);
await activeRepository.save(session);
await activeRepository.saveSetResult(
ActiveSetResult(
metadata: _metadata('active-result-1', now),
activeWorkoutSessionId: session.metadata.id,
programSnapshotId: 'program-snapshot-1',
exerciseSnapshotId: 'exercise-snapshot-1',
programIndex: 0,
exerciseIndex: 0,
setIndex: 0,
actualTimeMs: 45000,
actualReps: 10,
actualScore: 80,
scoreLabelSnapshot: 'Charge',
scoreUnitSnapshot: 'kg',
completedAt: now,
),
);
final closeUseCase = CloseWorkoutSessionUseCase(
sessionRepository: activeRepository,
historyRepository: historyRepository,
clock: _FakeClock(now.add(const Duration(seconds: 5))),
ids: _FakeIds(),
originDeviceId: 'device-1',
);
final history = await closeUseCase.close(
sessionId: session.metadata.id,
nameSnapshot: 'Séance jambes',
completed: true,
);
await templateRepository.save(
WorkoutTemplate(
metadata: template.metadata.markDeleted(now),
name: template.name,
),
);
final restored = await historyRepository.findById(history.metadata.id);
expect(restored, isNotNull);
expect(restored!.nameSnapshot, 'Séance jambes');
expect(restored.results, hasLength(1));
expect(restored.results.single.programNameSnapshot, 'Programme jambes');
expect(restored.results.single.exerciseNameSnapshot, 'Squat');
expect(restored.results.single.actualTimeMs, 45000);
expect(restored.results.single.actualReps, 10);
expect(restored.results.single.actualScore, 80);
expect(restored.results.single.scoreUnitSnapshot, 'kg');
},
);
}
String _resolvedSnapshot() {
return jsonEncode({
'name': 'Séance jambes',
'programs': [
{
'id': 'program-snapshot-1',
'programNameSnapshot': 'Programme jambes',
'programSnapshotJson': jsonEncode({
'exercises': [
{
'id': 'exercise-snapshot-1',
'exerciseNameSnapshot': 'Squat',
'setsCount': 1,
'timeEnabled': true,
'repsEnabled': true,
'scoreEnabled': true,
'targetTimeSeconds': 45,
'targetReps': 10,
'targetScore': 80,
'scoreLabelSnapshot': 'Charge',
'scoreUnitSnapshot': 'kg',
'restSecondsOverride': 0,
},
],
}),
},
],
'overrides': const [],
});
}
EntityMetadata _metadata(String id, DateTime now) {
return EntityMetadata(
id: id,
createdAt: now,
updatedAt: now,
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';
}
}