feat(online): partage sortant et boîte de réception (ticket #69)
Ajoute l'écran de partage sortant (presentation/share_screen.dart) et la boîte de réception des partages (presentation/share_inbox_screen.dart), avec les points d'entrée sur profile_screen.dart, home_screen.dart, program_screen.dart et workout_template_screen.dart. flutter analyze propre (mêmes infos préexistantes), 132/132 tests verts, build APK debug validé. Dernier ticket de développement du chantier "Ajouter les features serveur au client" (#63) — il ne reste que la QA finale (#70). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -184,6 +184,61 @@ void main() {
|
||||
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() {
|
||||
@ -466,3 +521,135 @@ final class _FakeWorkoutHistoryRepository implements WorkoutHistoryRepository {
|
||||
@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(),
|
||||
);
|
||||
}
|
||||
|
||||
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 {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user