Réorganise l'écran d'exécution autour d'une carte "Exercice actif" sous le compteur SÉRIE X/Y (nom, médias, temps de série, action "Démarrer l'exercice"). Le timer de série devient un état persistant dédié (plus un DateTime UI volatile), pause-aware, et "Démarrer l'exercice" lance en une action tous les chronos qui doivent démarrer en début de série (série, première étape temps, score chrono si stopwatch). Implémentation restée non commitée depuis sa réalisation ; QA a validé les vérifications statiques exécutables en sandbox (dart analyze, git diff --check). flutter analyze / flutter test restent à relancer dans un environnement avec cache Flutter SDK accessible. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
801 lines
22 KiB
Dart
801 lines
22 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(
|
||
'personnaliser l’enchaînement automatique transmet l’override séance',
|
||
(tester) async {
|
||
final program = WorkoutTemplateProgramDraft(
|
||
clientKey: 'program-1',
|
||
programNameSnapshot: 'Circuit',
|
||
defaultRestSecondsSnapshot: 60,
|
||
programSnapshotJson: '{}',
|
||
exercises: [
|
||
WorkoutTemplateExerciseDraft(
|
||
snapshotExerciseId: 'exercise-1',
|
||
exerciseNameSnapshot: 'Navettes',
|
||
setsCount: 3,
|
||
timeEnabled: true,
|
||
repsEnabled: false,
|
||
scoreEnabled: false,
|
||
exerciseStepsSnapshot: [
|
||
_step(
|
||
id: 'step-1',
|
||
position: 0,
|
||
name: 'Course',
|
||
type: ExerciseStepType.time,
|
||
),
|
||
_step(
|
||
id: 'step-2',
|
||
position: 1,
|
||
name: 'Retour',
|
||
type: ExerciseStepType.time,
|
||
),
|
||
],
|
||
autoStartNextTimedStepSnapshot: true,
|
||
targetTimeSeconds: 30,
|
||
),
|
||
],
|
||
);
|
||
|
||
await tester.binding.setSurfaceSize(const Size(400, 1400));
|
||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(home: WorkoutTemplateProgramDetailScreen(program: program)),
|
||
);
|
||
|
||
await tester.tap(
|
||
find.widgetWithText(
|
||
SwitchListTile,
|
||
'Enchaîner automatiquement les chronos consécutifs',
|
||
),
|
||
);
|
||
await tester.pump();
|
||
|
||
expect(
|
||
find.textContaining('Personnalisé pour cette séance'),
|
||
findsOneWidget,
|
||
);
|
||
final override = program.toOverrides().single;
|
||
expect(override.autoStartNextTimedStepOverride, isFalse);
|
||
},
|
||
);
|
||
|
||
testWidgets(
|
||
'revenir au réglage du programme remet l’override séance à null',
|
||
(tester) async {
|
||
final program = WorkoutTemplateProgramDraft(
|
||
clientKey: 'program-1',
|
||
programNameSnapshot: 'Circuit',
|
||
defaultRestSecondsSnapshot: 60,
|
||
programSnapshotJson: '{}',
|
||
exercises: [
|
||
WorkoutTemplateExerciseDraft(
|
||
snapshotExerciseId: 'exercise-1',
|
||
exerciseNameSnapshot: 'Navettes',
|
||
setsCount: 3,
|
||
timeEnabled: true,
|
||
repsEnabled: false,
|
||
scoreEnabled: false,
|
||
exerciseStepsSnapshot: [
|
||
_step(
|
||
id: 'step-1',
|
||
position: 0,
|
||
name: 'Course',
|
||
type: ExerciseStepType.time,
|
||
),
|
||
],
|
||
autoStartNextTimedStepSnapshot: false,
|
||
autoStartNextTimedStepOverride: true,
|
||
targetTimeSeconds: 30,
|
||
),
|
||
],
|
||
);
|
||
|
||
await tester.binding.setSurfaceSize(const Size(400, 1400));
|
||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(home: WorkoutTemplateProgramDetailScreen(program: program)),
|
||
);
|
||
|
||
expect(
|
||
find.textContaining('Personnalisé pour cette séance'),
|
||
findsOneWidget,
|
||
);
|
||
|
||
await tester.tap(find.text('Revenir au réglage du programme'));
|
||
await tester.pump();
|
||
|
||
expect(find.textContaining('Réglage du programme'), findsOneWidget);
|
||
expect(program.toOverrides(), isEmpty);
|
||
},
|
||
);
|
||
|
||
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(),
|
||
templateRepository: templateRepository,
|
||
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);
|
||
},
|
||
);
|
||
|
||
testWidgets(
|
||
'supprimer une séance demande confirmation puis la retire de la liste',
|
||
(tester) async {
|
||
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||
final templateRepository = _FakeWorkoutTemplateRepository([
|
||
_workoutTemplate(),
|
||
]);
|
||
final activeRepository = _FakeActiveSessionRepository();
|
||
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(),
|
||
templateRepository: templateRepository,
|
||
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.byTooltip('Supprimer la séance'));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.text('Supprimer Séance jambes ?'), findsOneWidget);
|
||
expect(find.text('Cette action est irréversible.'), findsOneWidget);
|
||
|
||
await tester.tap(find.text('Annuler'));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.text('Séance jambes'), findsOneWidget);
|
||
expect(templateRepository.saved, isEmpty);
|
||
|
||
await tester.tap(find.byTooltip('Supprimer la séance'));
|
||
await tester.pumpAndSettle();
|
||
await tester.tap(find.widgetWithText(FilledButton, 'Supprimer'));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.text('Séance jambes'), findsNothing);
|
||
expect(templateRepository.saved.single.metadata.deletedAt, isNotNull);
|
||
},
|
||
);
|
||
|
||
testWidgets('partager une séance ouvre le formulaire de partage', (
|
||
tester,
|
||
) async {
|
||
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||
final templateRepository = _FakeWorkoutTemplateRepository([
|
||
_workoutTemplate(),
|
||
]);
|
||
final programRepository = _FakeProgramRepository();
|
||
final remoteShareApi = _FakeRemoteShareApi();
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: WorkoutTemplateFormScreen(
|
||
workoutTemplateUseCases: WorkoutTemplateUseCases(
|
||
templateRepository: templateRepository,
|
||
programRepository: programRepository,
|
||
clock: clock,
|
||
ids: _FakeIds(),
|
||
originDeviceId: 'device-1',
|
||
),
|
||
programUseCases: ProgramUseCases(
|
||
programRepository: programRepository,
|
||
exerciseRepository: _FakeExerciseRepository(),
|
||
templateRepository: templateRepository,
|
||
clock: clock,
|
||
ids: _FakeIds(),
|
||
originDeviceId: 'device-1',
|
||
),
|
||
shareUseCases: _shareUseCases(
|
||
templateRepository: templateRepository,
|
||
programRepository: programRepository,
|
||
remoteShareApi: remoteShareApi,
|
||
),
|
||
template: templateRepository.templates.single,
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.tap(find.byTooltip('Partager'));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.text('Partager la séance'), findsOneWidget);
|
||
|
||
await tester.enterText(
|
||
find.widgetWithText(TextFormField, 'Emails destinataires'),
|
||
'coach@example.com',
|
||
);
|
||
await tester.tap(find.text('Envoyer le partage'));
|
||
await tester.pump();
|
||
await tester.pump();
|
||
|
||
expect(remoteShareApi.lastResourceType, ShareResourceType.workoutTemplate);
|
||
expect(remoteShareApi.lastRecipientEmails, ['coach@example.com']);
|
||
});
|
||
}
|
||
|
||
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',
|
||
);
|
||
}
|
||
|
||
ExerciseStep _step({
|
||
required String id,
|
||
required int position,
|
||
required String name,
|
||
required ExerciseStepType type,
|
||
}) {
|
||
return ExerciseStep(
|
||
id: id,
|
||
position: position,
|
||
name: name,
|
||
type: type,
|
||
defaultTargetValue: type == ExerciseStepType.time ? 30 : 5,
|
||
);
|
||
}
|
||
|
||
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;
|
||
final saved = <WorkoutTemplate>[];
|
||
|
||
@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 {
|
||
return templates
|
||
.where((template) => template.metadata.deletedAt == null)
|
||
.toList();
|
||
}
|
||
|
||
@override
|
||
Future<void> replaceComposition(
|
||
WorkoutTemplate template,
|
||
DateTime deletedAt,
|
||
) async {
|
||
templates.removeWhere((item) => item.metadata.id == template.metadata.id);
|
||
templates.add(template);
|
||
}
|
||
|
||
@override
|
||
Future<void> save(WorkoutTemplate template) async {
|
||
saved.add(template);
|
||
templates.removeWhere((item) => item.metadata.id == template.metadata.id);
|
||
templates.add(template);
|
||
}
|
||
|
||
@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<ActiveSetTimerState?> findSetTimerState({
|
||
required String sessionId,
|
||
required int programIndex,
|
||
required int exerciseIndex,
|
||
required int setIndex,
|
||
}) async {
|
||
return null;
|
||
}
|
||
|
||
@override
|
||
Future<ActiveExerciseStepProgressState?> findExerciseStepProgressState({
|
||
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<ActiveSetTimerState>> listSetTimerStates(String sessionId) async {
|
||
return const [];
|
||
}
|
||
|
||
@override
|
||
Future<List<ActiveSetResult>> listSetResults(String sessionId) async {
|
||
return const [];
|
||
}
|
||
|
||
@override
|
||
Future<List<ActiveExerciseStepProgressState>> listExerciseStepProgressStates(
|
||
String sessionId,
|
||
) async {
|
||
return const [];
|
||
}
|
||
|
||
@override
|
||
Future<List<ActiveExerciseStepResult>> listExerciseStepResults(
|
||
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> saveSetTimerState(ActiveSetTimerState state) async {}
|
||
|
||
@override
|
||
Future<void> saveExerciseStepProgressState(
|
||
ActiveExerciseStepProgressState state,
|
||
) async {}
|
||
|
||
@override
|
||
Future<void> saveExerciseStepResult(ActiveExerciseStepResult result) 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 {}
|
||
|
||
@override
|
||
Future<void> saveStepResult(WorkoutHistoryStepResult result) async {}
|
||
}
|
||
|
||
ShareUseCases _shareUseCases({
|
||
required _FakeWorkoutTemplateRepository templateRepository,
|
||
required _FakeProgramRepository programRepository,
|
||
_FakeRemoteShareApi? remoteShareApi,
|
||
}) {
|
||
return ShareUseCases(
|
||
tokenStore: _FakeAuthTokenStore('token-1'),
|
||
remoteShareApi: remoteShareApi ?? _FakeRemoteShareApi(),
|
||
inboxRepository: _FakeShareInboxRepository(),
|
||
pendingActionRepository: _FakePendingShareActionRepository(),
|
||
localChanges: _FakeLocalSyncChangeRepository(),
|
||
programRepository: programRepository,
|
||
templateRepository: templateRepository,
|
||
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
|
||
ids: _FakeIds(),
|
||
originDeviceId: 'device-1',
|
||
);
|
||
}
|
||
|
||
final class _FakeAuthTokenStore implements AuthTokenStore {
|
||
_FakeAuthTokenStore([this.token]);
|
||
|
||
String? token;
|
||
|
||
@override
|
||
Future<void> clearToken() async {
|
||
token = null;
|
||
}
|
||
|
||
@override
|
||
Future<String?> readToken() async => token;
|
||
|
||
@override
|
||
Future<void> saveToken(String token, DateTime expiresAt) async {
|
||
this.token = token;
|
||
}
|
||
}
|
||
|
||
final class _FakeRemoteShareApi implements RemoteShareApi {
|
||
ShareResourceType? lastResourceType;
|
||
List<String> lastRecipientEmails = const [];
|
||
|
||
@override
|
||
Future<RemoteSyncedItem> acceptShare(String shareId, String token) async {
|
||
return RemoteSyncedItem(
|
||
resourceType: SyncResourceType.workoutTemplate,
|
||
clientId: 'template-remote',
|
||
serverId: 'template-server',
|
||
schemaVersion: 1,
|
||
clientUpdatedAt: DateTime.utc(2026, 7, 17),
|
||
serverUpdatedAt: DateTime.utc(2026, 7, 17),
|
||
deletedAt: null,
|
||
payload: const {'name': 'Séance partagée'},
|
||
);
|
||
}
|
||
|
||
@override
|
||
Future<void> declineShare(String shareId, String token) async {}
|
||
|
||
@override
|
||
Future<List<ShareInboxItem>> fetchInbox(String token) async => const [];
|
||
|
||
@override
|
||
Future<void> revokeShare(String shareId, String token) async {}
|
||
|
||
@override
|
||
Future<RemoteShareSendResult> sendShare({
|
||
required ShareResourceType resourceType,
|
||
required Map<String, Object?> payload,
|
||
required List<String> recipientEmails,
|
||
required String token,
|
||
}) async {
|
||
lastResourceType = resourceType;
|
||
lastRecipientEmails = recipientEmails;
|
||
return const RemoteShareSendResult(
|
||
shareId: 'share-1',
|
||
recipientUserIds: ['recipient-1'],
|
||
unresolvedEmails: [],
|
||
);
|
||
}
|
||
}
|
||
|
||
final class _FakeShareInboxRepository implements ShareInboxRepository {
|
||
@override
|
||
Future<ShareInboxItem?> findByShareId(String shareId) async => null;
|
||
|
||
@override
|
||
Future<List<ShareInboxItem>> listAll() async => const [];
|
||
|
||
@override
|
||
Future<void> markStatus(
|
||
String shareId,
|
||
ShareInboxStatus status,
|
||
DateTime respondedAt,
|
||
) async {}
|
||
|
||
@override
|
||
Future<void> upsert(ShareInboxItem item) async {}
|
||
|
||
@override
|
||
Future<void> upsertAll(List<ShareInboxItem> items) async {}
|
||
}
|
||
|
||
final class _FakePendingShareActionRepository
|
||
implements PendingShareActionRepository {
|
||
@override
|
||
Future<void> add(PendingShareAction action) async {}
|
||
|
||
@override
|
||
Future<List<PendingShareAction>> listPending() async => const [];
|
||
|
||
@override
|
||
Future<void> markFailed(String id, DateTime attemptedAt) async {}
|
||
|
||
@override
|
||
Future<void> markSucceeded(String id, DateTime attemptedAt) async {}
|
||
}
|
||
|
||
final class _FakeLocalSyncChangeRepository
|
||
implements LocalSyncChangeRepository {
|
||
@override
|
||
Future<bool> applyRemoteItem(RemoteSyncedItem item) async => true;
|
||
|
||
@override
|
||
Future<List<PendingSyncChange>> listPendingChanges() async => const [];
|
||
|
||
@override
|
||
Future<void> markChangesSynced(
|
||
List<String> changeLogIds,
|
||
DateTime syncedAt,
|
||
) async {}
|
||
}
|