fix(watch): finalise correctif sync workoutHistory/exercise et distance live montre (#157)

This commit is contained in:
2026-07-29 11:22:17 +02:00
parent 6f913e4e8d
commit 30c6259748
28 changed files with 1658 additions and 248 deletions

View File

@ -2611,6 +2611,68 @@ void main() {
expect(inboxRepository.items.single.status, ShareInboxStatus.accepted);
});
test(
'ShareUseCases acceptShare imports all returned resources locally',
() async {
final remoteShareApi = _FakeRemoteShareApi()
..acceptResults = [
_remoteSharedProgramItem(),
_remoteSharedTemplateItem(),
];
final localChanges = _FakeLocalSyncChangeRepository();
final inboxRepository = _FakeShareInboxRepository()
..items.add(_shareInboxItem(status: ShareInboxStatus.pending));
await _shareUseCase(
remoteShareApi: remoteShareApi,
localChanges: localChanges,
inboxRepository: inboxRepository,
).acceptShare('share-1');
expect(localChanges.appliedItems.map((item) => item.resourceType), [
SyncResourceType.program,
SyncResourceType.workoutTemplate,
]);
expect(inboxRepository.items.single.status, ShareInboxStatus.accepted);
},
);
test(
'ShareUseCases acceptShare imports cached pack payload after remote ack',
() async {
final remoteShareApi = _FakeRemoteShareApi();
final templateRepository = _FakeWorkoutTemplateRepository();
final inboxRepository = _FakeShareInboxRepository()
..items.add(
_shareInboxItem(
status: ShareInboxStatus.pending,
resourceType: ShareResourceType.pack,
payloadJson: jsonEncode({
'name': 'Pack reprise',
'workouts': [
_sharedTemplatePayload(id: 'template-a', name: 'Séance A'),
_sharedTemplatePayload(id: 'template-b', name: 'Séance B'),
],
}),
),
);
await _shareUseCase(
remoteShareApi: remoteShareApi,
inboxRepository: inboxRepository,
templateRepository: templateRepository,
).acceptShare('share-1');
expect(remoteShareApi.acceptCalls, 1);
expect(templateRepository.templates, hasLength(2));
expect(
templateRepository.templates.map((template) => template.name),
containsAll(['Séance A', 'Séance B']),
);
expect(inboxRepository.items.single.status, ShareInboxStatus.accepted);
},
);
test(
'ShareUseCases acceptShare sans token importe une copie locale indépendante',
() async {
@ -4559,6 +4621,7 @@ ShareUseCases _shareUseCase({
final class _FakeRemoteShareApi implements RemoteShareApi {
Exception? exception;
RemoteSyncedItem? acceptResult;
List<RemoteSyncedItem>? acceptResults;
List<ShareInboxItem> inboxItems = const [];
var sendCalls = 0;
var acceptCalls = 0;
@ -4568,13 +4631,16 @@ final class _FakeRemoteShareApi implements RemoteShareApi {
Map<String, Object?>? lastPayload;
@override
Future<RemoteSyncedItem> acceptShare(String shareId, String token) async {
Future<List<RemoteSyncedItem>> acceptShare(
String shareId,
String token,
) async {
acceptCalls += 1;
final error = exception;
if (error != null) {
throw error;
}
return acceptResult ?? _remoteSharedProgramItem();
return acceptResults ?? [acceptResult ?? _remoteSharedProgramItem()];
}
@override
@ -4804,6 +4870,19 @@ RemoteSyncedItem _remoteSharedProgramItem() {
);
}
RemoteSyncedItem _remoteSharedTemplateItem() {
return RemoteSyncedItem(
resourceType: SyncResourceType.workoutTemplate,
clientId: 'shared-template-1',
serverId: 'server-template-1',
schemaVersion: 1,
clientUpdatedAt: DateTime.utc(2026, 7, 17, 12, 2),
serverUpdatedAt: DateTime.utc(2026, 7, 17, 12, 3),
deletedAt: null,
payload: const {'id': 'shared-template-1', 'name': 'Séance partagée'},
);
}
ExerciseUseCases _exerciseUseCase(
_FakeExerciseRepository repository, {
_FakeProgramRepository? programRepository,

View File

@ -377,6 +377,43 @@ void main() {
expect(env.repository.stepResults.last.actualScore, 0);
});
test('projects reps target for independent step manual score', () async {
final session = _session(
steps: [
_step(
type: ExerciseStepType.reps,
defaultTargetValue: 10,
hasScore: true,
defaultTargetScore: 8,
),
],
);
final repository = _FakeActiveSessionRepository()..session = session;
repository.stepProgressStates['step-state'] = _stepState(
sessionId: session.metadata.id,
);
final projectionUseCases = WatchCompanionProjectionUseCases(
sessionRepository: repository,
clock: _FakeClock(_now),
ids: _FakeIds(),
originDeviceId: 'device-1',
);
final projection = await projectionUseCases.emitCurrentProjection();
expect(projection.hasManualScore, isTrue);
expect(projection.manualScoreScope, WatchManualScoreScope.step);
expect(projection.manualScoreRepsTargetValue, 10);
expect(projection.manualScoreTargetValue, 8);
expect(projection.manualScoreTargetLabel, 'Cible');
expect(
projection.expiresAtEpochMs - projection.projectedAtEpochMs,
const Duration(seconds: 12).inMilliseconds,
);
await projectionUseCases.dispose();
});
test('decrementScore at zero is accepted no-op', () async {
final env = _env(
session: _session(scoreEnabled: true),
@ -740,6 +777,7 @@ final class _FakeProjectionSource implements WatchProjectionSource {
deviceSessionId: projection.deviceSessionId,
revision: projection.revision + 1,
projectedAtEpochMs: projection.projectedAtEpochMs,
expiresAtEpochMs: projection.expiresAtEpochMs,
phase: projection.phase,
phoneReachable: projection.phoneReachable,
seriesIndex: projection.seriesIndex,
@ -752,6 +790,9 @@ final class _FakeProjectionSource implements WatchProjectionSource {
hasManualScore: projection.hasManualScore,
currentManualScoreValue: projection.currentManualScoreValue,
canDecrementScore: projection.canDecrementScore,
manualScoreTargetValue: projection.manualScoreTargetValue,
manualScoreTargetLabel: projection.manualScoreTargetLabel,
manualScoreRepsTargetValue: projection.manualScoreRepsTargetValue,
manualScoreScope: projection.manualScoreScope,
);
return projection;