feat(watch): clôture lot #91 - fréquence cardiaque live, notifications de séance et finitions montre
Consolide le lot applicatif watch companion validé : - télémétrie fréquence cardiaque live remontée montre -> téléphone (collecteur watch, adapter Wear Data Layer, persistance Drift, propagation aux écrans historique/programme/profil/exécution) - notifications de séance en arrière-plan côté téléphone (service foreground de statut + passerelle applicative) - finitions montre : chrono d'étape, score d'étape, retrait du bouton "lancer une séance", thème, icônes et polices watch_app Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -62,6 +62,140 @@ void main() {
|
||||
]);
|
||||
expect(repository.items.single.clientId, 'exercise-1');
|
||||
});
|
||||
|
||||
test('pull returns synced items and filters them with since query', () async {
|
||||
final repository = _FakeSyncedResourceRepository()
|
||||
..items.addAll([
|
||||
SyncedResource(
|
||||
serverId: 'resource-1',
|
||||
ownerUserId: 'user-1',
|
||||
resourceType: SyncedResourceType.exercise,
|
||||
clientId: 'exercise-1',
|
||||
payloadJson: {'name': 'Squat'},
|
||||
schemaVersion: 1,
|
||||
clientUpdatedAt: DateTime.utc(2026, 7, 19, 10),
|
||||
serverUpdatedAt: DateTime.utc(2026, 7, 19, 11),
|
||||
),
|
||||
SyncedResource(
|
||||
serverId: 'resource-2',
|
||||
ownerUserId: 'user-1',
|
||||
resourceType: SyncedResourceType.program,
|
||||
clientId: 'program-1',
|
||||
payloadJson: {'name': 'Programme A'},
|
||||
schemaVersion: 2,
|
||||
clientUpdatedAt: DateTime.utc(2026, 7, 19, 11),
|
||||
serverUpdatedAt: DateTime.utc(2026, 7, 19, 12, 30),
|
||||
),
|
||||
]);
|
||||
final handler = buildApiHandler(syncApi: _syncApi(resources: repository));
|
||||
|
||||
final response = await handler(
|
||||
Request(
|
||||
'GET',
|
||||
Uri.parse(
|
||||
'http://localhost/sync/pull?since=2026-07-19T12:00:00Z',
|
||||
),
|
||||
headers: {'authorization': 'Bearer valid-token'},
|
||||
),
|
||||
);
|
||||
|
||||
final body = jsonDecode(await response.readAsString()) as Map;
|
||||
final items = body['items'] as List;
|
||||
|
||||
expect(response.statusCode, 200);
|
||||
expect(body['serverCursor'], '2026-07-19T12:30:00.000Z');
|
||||
expect(items, hasLength(1));
|
||||
expect((items.single as Map)['clientId'], 'program-1');
|
||||
expect((items.single as Map)['resourceType'], 'program');
|
||||
});
|
||||
|
||||
test('pull returns 400 for invalid since query parameter', () async {
|
||||
final handler = buildApiHandler(syncApi: _syncApi());
|
||||
|
||||
final response = await handler(
|
||||
Request(
|
||||
'GET',
|
||||
Uri.parse('http://localhost/sync/pull?since=not-a-date'),
|
||||
headers: {'authorization': 'Bearer valid-token'},
|
||||
),
|
||||
);
|
||||
|
||||
expect(response.statusCode, 400);
|
||||
expect(
|
||||
jsonDecode(await response.readAsString()),
|
||||
{'error': 'Invalid date format'},
|
||||
);
|
||||
});
|
||||
|
||||
test('exchange returns push results followed by pulled items', () async {
|
||||
final repository = _FakeSyncedResourceRepository()
|
||||
..items.add(
|
||||
SyncedResource(
|
||||
serverId: 'existing-1',
|
||||
ownerUserId: 'user-1',
|
||||
resourceType: SyncedResourceType.workoutHistory,
|
||||
clientId: 'history-1',
|
||||
payloadJson: {'score': 12},
|
||||
schemaVersion: 1,
|
||||
clientUpdatedAt: DateTime.utc(2026, 7, 19, 8),
|
||||
serverUpdatedAt: DateTime.utc(2026, 7, 19, 9),
|
||||
),
|
||||
);
|
||||
final handler = buildApiHandler(syncApi: _syncApi(resources: repository));
|
||||
|
||||
final response = await handler(
|
||||
Request(
|
||||
'POST',
|
||||
Uri.parse('http://localhost/sync/exchange'),
|
||||
headers: {'authorization': 'Bearer valid-token'},
|
||||
body: jsonEncode({
|
||||
'deviceId': 'device-1',
|
||||
'since': '2026-07-19T08:30:00Z',
|
||||
'items': [
|
||||
{
|
||||
'resourceType': 'exercise',
|
||||
'clientId': 'exercise-2',
|
||||
'schemaVersion': 1,
|
||||
'clientUpdatedAt': '2026-07-19T10:00:00Z',
|
||||
'payload': {'name': 'Lunge'},
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
final body = jsonDecode(await response.readAsString()) as Map;
|
||||
final pushResults = body['pushResults'] as List;
|
||||
final items = body['items'] as List;
|
||||
|
||||
expect(response.statusCode, 200);
|
||||
expect(pushResults, hasLength(1));
|
||||
expect((pushResults.single as Map)['status'], 'accepted');
|
||||
expect(items, hasLength(2));
|
||||
expect(
|
||||
items.map((item) => (item as Map)['clientId']),
|
||||
containsAll(['history-1', 'exercise-2']),
|
||||
);
|
||||
});
|
||||
|
||||
test('exchange returns 400 when items is not a json array', () async {
|
||||
final handler = buildApiHandler(syncApi: _syncApi());
|
||||
|
||||
final response = await handler(
|
||||
Request(
|
||||
'POST',
|
||||
Uri.parse('http://localhost/sync/exchange'),
|
||||
headers: {'authorization': 'Bearer valid-token'},
|
||||
body: jsonEncode({'deviceId': 'device-1', 'items': 'not-a-list'}),
|
||||
),
|
||||
);
|
||||
|
||||
expect(response.statusCode, 400);
|
||||
expect(
|
||||
jsonDecode(await response.readAsString()),
|
||||
{'error': 'items must be a JSON array.'},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
SyncApi _syncApi({_FakeSyncedResourceRepository? resources}) {
|
||||
|
||||
Reference in New Issue
Block a user