diff --git a/.ideai/tickets/72/carnet.md b/.ideai/tickets/72/carnet.md index 005235c..feba76b 100644 --- a/.ideai/tickets/72/carnet.md +++ b/.ideai/tickets/72/carnet.md @@ -1,6 +1,6 @@ --- issueRef: "#72" -version: 3 -updatedBy: {"kind":"user"} -updatedAt: 1784493074891 +version: 4 +updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} +updatedAt: 1784527867008 --- diff --git a/.ideai/tickets/72/issue.md b/.ideai/tickets/72/issue.md index 32ccb53..bb924b8 100644 --- a/.ideai/tickets/72/issue.md +++ b/.ideai/tickets/72/issue.md @@ -2,15 +2,15 @@ id: "b388e21b-5262-4b3e-b075-5654d2a478f2" number: 72 title: "[Bug] les étapes des exercices ne sont pas affichés pendant la séance" -status: "open" +status: "qa" priority: "critical" sprint: null links: [] agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}] createdBy: {"kind":"user"} -updatedBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} createdAt: 1784492460868 -updatedAt: 1784493074891 -version: 3 +updatedAt: 1784527867008 +version: 4 --- Pendant la séance, il n'y a aucune étape d'exercice affiché. J'ia un exercice divisé en 3 étapes avec des temps par étapes ou des répétitions, et quand je suis a cet exercices pendant ma séance, je n'ai pas les étapes d'affichées à l'écran. Je ne sais donc pas ce que je dois faire, ni combien de temps ou combien de fois. Je pense qu'il faudrait au moins le titre de chaque étape avec le nombre de répétitions ou le chrono qui défile et un bouton pour start/stop les chronos des étapes si les étapes possèdent un temps. \ No newline at end of file diff --git a/.ideai/tickets/index.json b/.ideai/tickets/index.json index 3497714..5e09cf3 100644 --- a/.ideai/tickets/index.json +++ b/.ideai/tickets/index.json @@ -811,13 +811,13 @@ "issueRef": "#72", "path": "72", "title": "[Bug] les étapes des exercices ne sont pas affichés pendant la séance", - "status": "open", + "status": "qa", "priority": "critical", "sprint": null, "assignedAgentIds": [ "57695b92-24d0-4876-837c-76116e70a6ae" ], - "updatedAt": 1784493074891 + "updatedAt": 1784527867008 }, { "issueRef": "#73", diff --git a/lib/application/app_bootstrap.dart b/lib/application/app_bootstrap.dart index ea2417f..490b872 100644 --- a/lib/application/app_bootstrap.dart +++ b/lib/application/app_bootstrap.dart @@ -171,6 +171,7 @@ final class AppBootstrap implements AppDependencies { templateRepository: templateRepository, clock: clock, ids: ids, + originDeviceId: originDeviceId, ), syncGateway: const NoOpSyncGateway(), ); diff --git a/lib/application/use_cases.dart b/lib/application/use_cases.dart index 4c7d116..f703c33 100644 --- a/lib/application/use_cases.dart +++ b/lib/application/use_cases.dart @@ -250,6 +250,7 @@ final class ShareUseCases { required this.templateRepository, required this.clock, required this.ids, + required this.originDeviceId, }); final AuthTokenStore tokenStore; @@ -261,6 +262,7 @@ final class ShareUseCases { final WorkoutTemplateRepository templateRepository; final Clock clock; final IdGenerator ids; + final String originDeviceId; Future sendShare({ required ShareResourceType resourceType, @@ -322,8 +324,10 @@ final class ShareUseCases { } Future acceptShare(String shareId) async { + final cachedItem = await inboxRepository.findByShareId(shareId); final token = await tokenStore.readToken(); if (token == null) { + await _acceptCachedShareAndQueueAck(shareId, cachedItem); return; } try { @@ -335,12 +339,7 @@ final class ShareUseCases { clock.now(), ); } catch (_) { - await pendingActionRepository.add( - _pendingShareAction( - actionType: PendingShareActionType.accept, - shareId: shareId, - ), - ); + await _acceptCachedShareAndQueueAck(shareId, cachedItem); } } @@ -488,6 +487,257 @@ final class ShareUseCases { return; } } + + Future _acceptCachedShareAndQueueAck( + String shareId, + ShareInboxItem? item, + ) async { + if (item != null && item.status == ShareInboxStatus.pending) { + await _importSharedPayload(item); + await inboxRepository.markStatus( + shareId, + ShareInboxStatus.accepted, + clock.now(), + ); + } + await pendingActionRepository.add( + _pendingShareAction( + actionType: PendingShareActionType.accept, + shareId: shareId, + ), + ); + } + + Future _importSharedPayload(ShareInboxItem item) async { + final payload = _jsonObject(item.payloadJson); + final now = clock.now(); + switch (item.resourceType) { + case ShareResourceType.program: + await programRepository.save( + _programCopyFromSharePayload(payload, now), + ); + return; + case ShareResourceType.workoutTemplate: + await templateRepository.save( + _workoutTemplateCopyFromSharePayload(payload, now), + ); + return; + } + } + + Program _programCopyFromSharePayload( + Map payload, + DateTime now, + ) { + final programId = ids.newId(); + final programMetadata = EntityMetadata( + id: programId, + createdAt: now, + updatedAt: now, + originDeviceId: originDeviceId, + ); + return Program( + metadata: programMetadata, + name: _stringFromSharePayload(payload, 'name', 'Programme partagé'), + defaultRestSeconds: payload['defaultRestSeconds'] as int? ?? 0, + exercises: _programExerciseCopiesFromSharePayload( + payload['exercises'], + programId, + now, + ), + ); + } + + List _programExerciseCopiesFromSharePayload( + Object? value, + String programId, + DateTime now, + ) { + if (value is! List) { + return const []; + } + final exercises = []; + for (var index = 0; index < value.length; index++) { + final raw = value[index]; + if (raw is! Map) { + continue; + } + final map = Map.from(raw); + exercises.add( + ProgramExercise( + metadata: EntityMetadata( + id: ids.newId(), + createdAt: now, + updatedAt: now, + originDeviceId: originDeviceId, + ), + programId: programId, + sourceExerciseId: null, + position: map['position'] as int? ?? index, + exerciseNameSnapshot: _stringFromSharePayload( + map, + 'exerciseNameSnapshot', + 'Exercice partagé', + ), + exerciseDescriptionSnapshot: + map['exerciseDescriptionSnapshot'] as String?, + exerciseImageMediaIdSnapshot: + map['exerciseImageMediaIdSnapshot'] as String?, + exerciseImageMediaIdsSnapshot: _stringListFromSharePayload( + map['exerciseImageMediaIdsSnapshot'] ?? + map['imageMediaIdsSnapshot'], + ), + exerciseVideoMediaIdSnapshot: + map['exerciseVideoMediaIdSnapshot'] as String?, + exerciseStepsSnapshot: _exerciseStepsFromSnapshot( + map['exerciseStepsSnapshot'], + ), + exerciseArchivedSnapshot: map['exerciseArchivedSnapshot'] == true, + availableTimeSnapshot: map['availableTimeSnapshot'] == true, + availableRepsSnapshot: map['availableRepsSnapshot'] == true, + availableScoreSnapshot: map['availableScoreSnapshot'] == true, + scoreInputModeSnapshot: _scoreInputModeFromSnapshot( + map['scoreInputModeSnapshot'], + ), + scoreLabelSnapshot: map['scoreLabelSnapshot'] as String?, + scoreUnitSnapshot: map['scoreUnitSnapshot'] as String?, + setsCount: map['setsCount'] as int? ?? 1, + timeEnabled: map['timeEnabled'] == true, + repsEnabled: map['repsEnabled'] == true, + scoreEnabled: map['scoreEnabled'] == true, + targetTimeSeconds: map['targetTimeSeconds'] as int?, + targetReps: map['targetReps'] as int?, + targetScore: (map['targetScore'] as num?)?.toDouble(), + targetScoreTimeMs: map['targetScoreTimeMs'] as int?, + restSecondsOverride: map['restSecondsOverride'] as int?, + ), + ); + } + return exercises; + } + + WorkoutTemplate _workoutTemplateCopyFromSharePayload( + Map payload, + DateTime now, + ) { + final templateId = ids.newId(); + final programIdBySharedId = {}; + final programs = _workoutTemplateProgramCopiesFromSharePayload( + payload['programs'], + templateId, + programIdBySharedId, + now, + ); + return WorkoutTemplate( + metadata: EntityMetadata( + id: templateId, + createdAt: now, + updatedAt: now, + originDeviceId: originDeviceId, + ), + name: _stringFromSharePayload(payload, 'name', 'Séance partagée'), + lastStartedAt: null, + programs: programs, + overrides: _workoutTemplateOverrideCopiesFromSharePayload( + payload['overrides'], + programIdBySharedId, + now, + ), + ); + } + + List _workoutTemplateProgramCopiesFromSharePayload( + Object? value, + String templateId, + Map programIdBySharedId, + DateTime now, + ) { + if (value is! List) { + return const []; + } + final programs = []; + for (var index = 0; index < value.length; index++) { + final raw = value[index]; + if (raw is! Map) { + continue; + } + final map = Map.from(raw); + final copiedId = ids.newId(); + final sharedId = map['id'] as String?; + if (sharedId != null) { + programIdBySharedId[sharedId] = copiedId; + } + programs.add( + WorkoutTemplateProgram( + metadata: EntityMetadata( + id: copiedId, + createdAt: now, + updatedAt: now, + originDeviceId: originDeviceId, + ), + workoutTemplateId: templateId, + sourceProgramId: null, + position: map['position'] as int? ?? index, + programNameSnapshot: _stringFromSharePayload( + map, + 'programNameSnapshot', + 'Programme partagé', + ), + defaultRestSecondsSnapshot: + map['defaultRestSecondsSnapshot'] as int? ?? 0, + programSnapshotJson: + map['programSnapshotJson'] as String? ?? '{"exercises":[]}', + ), + ); + } + return programs; + } + + List + _workoutTemplateOverrideCopiesFromSharePayload( + Object? value, + Map programIdBySharedId, + DateTime now, + ) { + if (value is! List) { + return const []; + } + final overrides = []; + for (final raw in value) { + if (raw is! Map) { + continue; + } + final map = Map.from(raw); + final sharedProgramId = map['workoutTemplateProgramId'] as String?; + final copiedProgramId = programIdBySharedId[sharedProgramId]; + if (copiedProgramId == null) { + continue; + } + final snapshotProgramExerciseId = map['snapshotProgramExerciseId']; + if (snapshotProgramExerciseId is! String || + snapshotProgramExerciseId.trim().isEmpty) { + continue; + } + overrides.add( + WorkoutTemplateExerciseOverride( + metadata: EntityMetadata( + id: ids.newId(), + createdAt: now, + updatedAt: now, + originDeviceId: originDeviceId, + ), + workoutTemplateProgramId: copiedProgramId, + snapshotProgramExerciseId: snapshotProgramExerciseId, + setsCountOverride: map['setsCountOverride'] as int?, + targetTimeSecondsOverride: map['targetTimeSecondsOverride'] as int?, + targetRepsOverride: map['targetRepsOverride'] as int?, + targetScoreOverride: (map['targetScoreOverride'] as num?)?.toDouble(), + targetScoreTimeMsOverride: map['targetScoreTimeMsOverride'] as int?, + ), + ); + } + return overrides; + } } final class ExerciseUseCases { @@ -2965,6 +3215,22 @@ Map _jsonObject(String? json) { return const {}; } +String _stringFromSharePayload( + Map payload, + String key, + String fallback, +) { + final value = payload[key]; + return value is String && value.trim().isNotEmpty ? value : fallback; +} + +List _stringListFromSharePayload(Object? value) { + if (value is! List) { + return const []; + } + return value.whereType().toList(growable: false); +} + List _jsonStringList(String? json) { if (json == null) { return const []; diff --git a/lib/presentation/profile_screen.dart b/lib/presentation/profile_screen.dart index 49d71b0..9c3ddc3 100644 --- a/lib/presentation/profile_screen.dart +++ b/lib/presentation/profile_screen.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import '../application/application.dart'; @@ -64,6 +66,7 @@ final class _ProfileScreenState extends State { builder: (context) => LoginScreen( authUseCases: widget.authUseCases, syncUseCases: widget.syncUseCases, + shareUseCases: widget.shareUseCases, ), ), ); @@ -83,6 +86,7 @@ final class _ProfileScreenState extends State { builder: (context) => RegisterScreen( authUseCases: widget.authUseCases, syncUseCases: widget.syncUseCases, + shareUseCases: widget.shareUseCases, ), ), ); @@ -420,11 +424,13 @@ final class LoginScreen extends StatefulWidget { const LoginScreen({ required this.authUseCases, required this.syncUseCases, + this.shareUseCases, super.key, }); final AuthUseCases authUseCases; final SyncUseCases syncUseCases; + final ShareUseCases? shareUseCases; @override State createState() => _LoginScreenState(); @@ -486,6 +492,7 @@ final class _LoginScreenState extends State { builder: (context) => RegisterScreen( authUseCases: widget.authUseCases, syncUseCases: widget.syncUseCases, + shareUseCases: widget.shareUseCases, ), ), ); @@ -515,6 +522,11 @@ final class _LoginScreenState extends State { email: _emailController.text, password: _passwordController.text, ); + unawaited(widget.syncUseCases.synchronize(manual: false)); + final shareUseCases = widget.shareUseCases; + if (shareUseCases != null) { + unawaited(shareUseCases.processPendingShareActions()); + } if (!mounted) return; Navigator.of(context).pop(true); } on RemoteAuthException catch (error) { @@ -537,11 +549,13 @@ final class RegisterScreen extends StatefulWidget { const RegisterScreen({ required this.authUseCases, required this.syncUseCases, + this.shareUseCases, super.key, }); final AuthUseCases authUseCases; final SyncUseCases syncUseCases; + final ShareUseCases? shareUseCases; @override State createState() => _RegisterScreenState(); @@ -628,6 +642,7 @@ final class _RegisterScreenState extends State { builder: (context) => LoginScreen( authUseCases: widget.authUseCases, syncUseCases: widget.syncUseCases, + shareUseCases: widget.shareUseCases, ), ), ); @@ -660,6 +675,11 @@ final class _RegisterScreenState extends State { password: _passwordController.text, displayName: displayName.isEmpty ? null : displayName, ); + unawaited(widget.syncUseCases.synchronize(manual: false)); + final shareUseCases = widget.shareUseCases; + if (shareUseCases != null) { + unawaited(shareUseCases.processPendingShareActions()); + } if (!mounted) return; Navigator.of(context).pop(true); } on RemoteAuthException catch (error) { diff --git a/lib/presentation/share_screen.dart b/lib/presentation/share_screen.dart index 6b1d7ea..b3a3169 100644 --- a/lib/presentation/share_screen.dart +++ b/lib/presentation/share_screen.dart @@ -118,6 +118,7 @@ final class _ShareFormScreenState extends State { MaterialPageRoute( builder: (context) => ShareAccountRequiredScreen( resourceType: widget.resourceType, + shareUseCases: widget.shareUseCases, authUseCases: widget.authUseCases, syncUseCases: widget.syncUseCases, ), @@ -137,12 +138,14 @@ final class _ShareFormScreenState extends State { final class ShareAccountRequiredScreen extends StatelessWidget { const ShareAccountRequiredScreen({ required this.resourceType, + this.shareUseCases, this.authUseCases, this.syncUseCases, super.key, }); final ShareResourceType resourceType; + final ShareUseCases? shareUseCases; final AuthUseCases? authUseCases; final SyncUseCases? syncUseCases; @@ -179,6 +182,7 @@ final class ShareAccountRequiredScreen extends StatelessWidget { builder: (context) => LoginScreen( authUseCases: authUseCases!, syncUseCases: syncUseCases!, + shareUseCases: shareUseCases, ), ), ) @@ -193,6 +197,7 @@ final class ShareAccountRequiredScreen extends StatelessWidget { builder: (context) => RegisterScreen( authUseCases: authUseCases!, syncUseCases: syncUseCases!, + shareUseCases: shareUseCases, ), ), ) diff --git a/test/application/use_cases_test.dart b/test/application/use_cases_test.dart index aaf241b..7c25970 100644 --- a/test/application/use_cases_test.dart +++ b/test/application/use_cases_test.dart @@ -1434,6 +1434,50 @@ void main() { expect(inboxRepository.items.single.status, ShareInboxStatus.accepted); }); + test( + 'ShareUseCases acceptShare sans token importe une copie locale indépendante', + () async { + final remoteShareApi = _FakeRemoteShareApi(); + final pendingRepository = _FakePendingShareActionRepository(); + final programRepository = _FakeProgramRepository(); + final inboxRepository = _FakeShareInboxRepository() + ..items.add( + _shareInboxItem( + status: ShareInboxStatus.pending, + payloadJson: jsonEncode(_sharedProgramPayload()), + ), + ); + + await _shareUseCase( + tokenStore: _FakeAuthTokenStore(), + remoteShareApi: remoteShareApi, + inboxRepository: inboxRepository, + pendingActionRepository: pendingRepository, + programRepository: programRepository, + ).acceptShare('share-1'); + + expect(remoteShareApi.acceptCalls, 0); + expect( + pendingRepository.actions.single.actionType, + PendingShareActionType.accept, + ); + expect(inboxRepository.items.single.status, ShareInboxStatus.accepted); + final imported = programRepository.programs.single; + expect(imported.metadata.id, isNot('program-from-sender')); + expect(imported.metadata.originDeviceId, 'device-1'); + expect(imported.name, 'Programme partagé'); + expect( + imported.exercises.single.metadata.id, + isNot('exercise-from-sender'), + ); + expect(imported.exercises.single.sourceExerciseId, isNull); + expect( + imported.exercises.single.exerciseStepsSnapshot.single.name, + 'Sprint', + ); + }, + ); + test('ShareUseCases refreshInbox updates the local cache', () async { final remoteShareApi = _FakeRemoteShareApi() ..inboxItems = [_shareInboxItem(status: ShareInboxStatus.pending)]; @@ -1866,6 +1910,7 @@ ShareUseCases _shareUseCase({ templateRepository: templateRepository ?? _FakeWorkoutTemplateRepository(), clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)), ids: _FakeIds(), + originDeviceId: 'device-1', ); } @@ -2027,17 +2072,58 @@ Program _shareableProgram() { ShareInboxItem _shareInboxItem({ String shareId = 'share-1', required ShareInboxStatus status, + String payloadJson = '{"name":"Programme partagé"}', }) { return ShareInboxItem( shareId: shareId, senderUserId: 'sender-1', resourceType: ShareResourceType.program, - payloadJson: '{"name":"Programme partagé"}', + payloadJson: payloadJson, status: status, createdAt: DateTime.utc(2026, 7, 17, 12), ); } +Map _sharedProgramPayload() { + return { + 'id': 'program-from-sender', + 'name': 'Programme partagé', + 'defaultRestSeconds': 45, + 'exercises': [ + { + 'id': 'exercise-from-sender', + 'sourceExerciseId': 'source-exercise-from-sender', + 'position': 0, + 'exerciseNameSnapshot': 'Sprint', + 'exerciseStepsSnapshot': [ + { + 'id': 'step-from-sender', + 'position': 0, + 'name': 'Sprint', + 'type': 'time', + 'defaultTargetValue': 20, + 'hasScore': false, + 'scoreInputMode': 'manual', + 'scoreLabel': null, + 'scoreUnit': null, + 'defaultTargetScore': null, + 'defaultTargetScoreTimeMs': null, + }, + ], + 'availableTimeSnapshot': true, + 'availableRepsSnapshot': false, + 'availableScoreSnapshot': false, + 'scoreInputModeSnapshot': 'manual', + 'setsCount': 2, + 'timeEnabled': true, + 'repsEnabled': false, + 'scoreEnabled': false, + 'targetTimeSeconds': 20, + }, + ], + }; +} + RemoteSyncedItem _remoteSharedProgramItem() { return RemoteSyncedItem( resourceType: SyncResourceType.program, diff --git a/test/presentation/home_screen_test.dart b/test/presentation/home_screen_test.dart index 1da6a49..24b9ec0 100644 --- a/test/presentation/home_screen_test.dart +++ b/test/presentation/home_screen_test.dart @@ -156,6 +156,7 @@ final class _FakeBootstrap implements AppDependencies { templateRepository: _FakeWorkoutTemplateRepository(), clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)), ids: _FakeIds(), + originDeviceId: 'device-1', ), activeWorkoutSessionUseCases = ActiveWorkoutSessionUseCases( sessionRepository: activeRepository, diff --git a/test/presentation/profile_screen_test.dart b/test/presentation/profile_screen_test.dart index e781c86..a439324 100644 --- a/test/presentation/profile_screen_test.dart +++ b/test/presentation/profile_screen_test.dart @@ -93,6 +93,7 @@ void main() { expect(find.text('alex@example.com'), findsOneWidget); expect(find.text('Synchronisation'), findsOneWidget); expect(find.text('Partages reçus'), findsOneWidget); + expect(harness.syncRemote.pullCalls, 1); }); testWidgets('la déconnexion repasse au profil déconnecté', (tester) async { @@ -273,6 +274,7 @@ final class _AuthHarness { templateRepository: templateRepository, clock: const _FakeClock(), ids: _FakeIds(), + originDeviceId: 'device-1', ); } diff --git a/test/presentation/program_screen_test.dart b/test/presentation/program_screen_test.dart index 04f54a9..717c852 100644 --- a/test/presentation/program_screen_test.dart +++ b/test/presentation/program_screen_test.dart @@ -604,6 +604,7 @@ ShareUseCases _shareUseCases({ templateRepository: templateRepository ?? _FakeWorkoutTemplateRepository(), clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)), ids: _FakeIds(), + originDeviceId: 'device-1', ); } diff --git a/test/presentation/share_inbox_screen_test.dart b/test/presentation/share_inbox_screen_test.dart index 6d68f95..bfbb3cc 100644 --- a/test/presentation/share_inbox_screen_test.dart +++ b/test/presentation/share_inbox_screen_test.dart @@ -70,22 +70,78 @@ void main() { ShareInboxStatus.declined, ); }); + + testWidgets( + 'accepter un partage en cache hors connexion importe une copie locale', + (tester) async { + final remoteShareApi = _FakeRemoteShareApi(); + final inboxRepository = _FakeShareInboxRepository() + ..items.add( + _shareItem( + shareId: 'share-program', + resourceType: ShareResourceType.program, + name: 'Programme tirs', + status: ShareInboxStatus.pending, + ), + ); + final programRepository = _FakeProgramRepository(); + final pendingRepository = _FakePendingShareActionRepository(); + + await tester.pumpWidget( + MaterialApp( + home: ShareInboxScreen( + shareUseCases: _shareUseCases( + tokenStore: _FakeAuthTokenStore(null), + remoteShareApi: remoteShareApi, + inboxRepository: inboxRepository, + pendingActionRepository: pendingRepository, + programRepository: programRepository, + ), + ), + ), + ); + await tester.pump(); + await tester.pump(); + + await tester.tap(find.widgetWithText(FilledButton, 'Accepter')); + await tester.pump(); + await tester.pump(); + + expect(remoteShareApi.acceptCalls, 0); + expect(programRepository.saved.single.name, 'Programme tirs'); + expect( + programRepository.saved.single.metadata.id, + isNot('share-program'), + ); + expect(inboxRepository.items.single.status, ShareInboxStatus.accepted); + expect( + pendingRepository.actions.single.actionType, + PendingShareActionType.accept, + ); + expect(find.text('Programme ajouté.'), findsOneWidget); + }, + ); } ShareUseCases _shareUseCases({ + _FakeAuthTokenStore? tokenStore, required _FakeRemoteShareApi remoteShareApi, required _FakeShareInboxRepository inboxRepository, + _FakePendingShareActionRepository? pendingActionRepository, + _FakeProgramRepository? programRepository, }) { return ShareUseCases( - tokenStore: _FakeAuthTokenStore('token-1'), + tokenStore: tokenStore ?? _FakeAuthTokenStore('token-1'), remoteShareApi: remoteShareApi, inboxRepository: inboxRepository, - pendingActionRepository: _FakePendingShareActionRepository(), + pendingActionRepository: + pendingActionRepository ?? _FakePendingShareActionRepository(), localChanges: _FakeLocalSyncChangeRepository(), - programRepository: _FakeProgramRepository(), + programRepository: programRepository ?? _FakeProgramRepository(), templateRepository: _FakeWorkoutTemplateRepository(), clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)), ids: _FakeIds(), + originDeviceId: 'device-1', ); } @@ -99,7 +155,17 @@ ShareInboxItem _shareItem({ ShareResourceType.program => { 'name': name, 'exercises': const [ - {'setsCount': 3}, + { + 'exerciseNameSnapshot': 'Sprint', + 'setsCount': 3, + 'availableTimeSnapshot': false, + 'availableRepsSnapshot': true, + 'availableScoreSnapshot': false, + 'timeEnabled': false, + 'repsEnabled': true, + 'scoreEnabled': false, + 'targetReps': 10, + }, ], }, ShareResourceType.workoutTemplate => { @@ -267,8 +333,12 @@ final class _FakeShareInboxRepository implements ShareInboxRepository { final class _FakePendingShareActionRepository implements PendingShareActionRepository { + final actions = []; + @override - Future add(PendingShareAction action) async {} + Future add(PendingShareAction action) async { + actions.add(action); + } @override Future> listPending() async => const []; @@ -296,6 +366,8 @@ final class _FakeLocalSyncChangeRepository } final class _FakeProgramRepository implements ProgramRepository { + final saved = []; + @override Future findById(String id) async => null; @@ -306,7 +378,9 @@ final class _FakeProgramRepository implements ProgramRepository { Future replaceExercises(Program program, DateTime deletedAt) async {} @override - Future save(Program program) async {} + Future save(Program program) async { + saved.add(program); + } @override Future saveExercise(ProgramExercise exercise) async {} diff --git a/test/presentation/workout_template_screen_test.dart b/test/presentation/workout_template_screen_test.dart index 3d9027c..8c00744 100644 --- a/test/presentation/workout_template_screen_test.dart +++ b/test/presentation/workout_template_screen_test.dart @@ -537,6 +537,7 @@ ShareUseCases _shareUseCases({ templateRepository: templateRepository, clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)), ids: _FakeIds(), + originDeviceId: 'device-1', ); }