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:
@ -52,6 +52,94 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('WatchSensorSummary', () {
|
||||
test('round-trips heart rate summary through JSON', () {
|
||||
const summary = WatchSensorSummary(
|
||||
sessionId: 'session-1',
|
||||
sampleCount: 12,
|
||||
minHeartRateBpm: 91,
|
||||
averageHeartRateBpm: 128.5,
|
||||
maxHeartRateBpm: 174,
|
||||
totalDistanceMeters: 842.4,
|
||||
totalCaloriesKcal: 186.2,
|
||||
);
|
||||
|
||||
final decoded = WatchSensorSummary.fromJson(
|
||||
jsonDecode(jsonEncode(summary.toJson())) as Map<String, Object?>,
|
||||
);
|
||||
|
||||
expect(decoded, summary);
|
||||
});
|
||||
|
||||
test('falls back safely for absent fields', () {
|
||||
final summary = WatchSensorSummary.fromJson({});
|
||||
|
||||
expect(summary.schemaVersion, watchBridgeSchemaVersion);
|
||||
expect(summary.sessionId, '');
|
||||
expect(summary.sampleCount, 0);
|
||||
expect(summary.minHeartRateBpm, isNull);
|
||||
expect(summary.averageHeartRateBpm, isNull);
|
||||
expect(summary.maxHeartRateBpm, isNull);
|
||||
expect(summary.totalDistanceMeters, isNull);
|
||||
expect(summary.totalCaloriesKcal, isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('WatchSensorSample', () {
|
||||
test('round-trips live telemetry sample through JSON', () {
|
||||
const sample = WatchSensorSample(
|
||||
sampleId: 'sample-1',
|
||||
sessionId: 'session-1',
|
||||
capturedAtEpochMs: 1710000000300,
|
||||
programIndex: 0,
|
||||
exerciseIndex: 1,
|
||||
setIndex: 2,
|
||||
passageIndex: 3,
|
||||
stepIndex: 4,
|
||||
programSnapshotId: 'program-snapshot-1',
|
||||
exerciseSnapshotId: 'exercise-snapshot-1',
|
||||
stepSnapshotId: 'step-snapshot-1',
|
||||
heartRateBpm: 142,
|
||||
distanceMeters: 840.5,
|
||||
caloriesKcal: 184.2,
|
||||
);
|
||||
|
||||
final decoded = WatchSensorSample.fromJson(
|
||||
jsonDecode(jsonEncode(sample.toJson())) as Map<String, Object?>,
|
||||
);
|
||||
|
||||
expect(decoded, sample);
|
||||
});
|
||||
|
||||
test('falls back safely for absent fields', () {
|
||||
final sample = WatchSensorSample.fromJson({});
|
||||
|
||||
expect(sample.schemaVersion, watchBridgeSchemaVersion);
|
||||
expect(sample.sampleId, isNull);
|
||||
expect(sample.sessionId, '');
|
||||
expect(sample.recordedAtEpochMs, 0);
|
||||
expect(sample.capturedAtEpochMs, 0);
|
||||
expect(sample.programIndex, isNull);
|
||||
expect(sample.exerciseIndex, isNull);
|
||||
expect(sample.setIndex, isNull);
|
||||
expect(sample.passageIndex, isNull);
|
||||
expect(sample.stepIndex, isNull);
|
||||
expect(sample.heartRateBpm, isNull);
|
||||
expect(sample.distanceMeters, isNull);
|
||||
expect(sample.caloriesKcal, isNull);
|
||||
});
|
||||
|
||||
test('accepts legacy recordedAtEpochMs as captured timestamp', () {
|
||||
final sample = WatchSensorSample.fromJson({
|
||||
'sessionId': 'session-1',
|
||||
'recordedAtEpochMs': 1710000000300,
|
||||
});
|
||||
|
||||
expect(sample.capturedAtEpochMs, 1710000000300);
|
||||
expect(sample.recordedAtEpochMs, 1710000000300);
|
||||
});
|
||||
});
|
||||
|
||||
group('WatchSessionProjection', () {
|
||||
test('round-trips every phase, primary action, and secondary action', () {
|
||||
for (final phase in WatchSessionPhase.values) {
|
||||
@ -65,6 +153,9 @@ void main() {
|
||||
seriesIndex: 2,
|
||||
seriesTotal: 5,
|
||||
exerciseName: 'Pompes tempo',
|
||||
programIndex: 0,
|
||||
exerciseIndex: 1,
|
||||
setIndex: 1,
|
||||
passageIndex: 1,
|
||||
passageTotal: 3,
|
||||
stepIndex: 2,
|
||||
@ -76,6 +167,12 @@ void main() {
|
||||
secondaryActions: WatchSecondaryAction.values,
|
||||
nextExerciseName: 'Fentes sautees',
|
||||
statusLabel: 'Chrono etape',
|
||||
hasManualScore: true,
|
||||
currentManualScoreValue: 7.5,
|
||||
canDecrementScore: true,
|
||||
manualScoreTargetValue: 10,
|
||||
manualScoreTargetLabel: 'Cible',
|
||||
manualScoreScope: WatchManualScoreScope.step,
|
||||
);
|
||||
|
||||
final decoded = WatchSessionProjection.fromJson(
|
||||
@ -114,6 +211,12 @@ void main() {
|
||||
]);
|
||||
expect(projection.nextExerciseName, isNull);
|
||||
expect(projection.statusLabel, isNull);
|
||||
expect(projection.hasManualScore, isFalse);
|
||||
expect(projection.currentManualScoreValue, isNull);
|
||||
expect(projection.canDecrementScore, isFalse);
|
||||
expect(projection.manualScoreTargetValue, isNull);
|
||||
expect(projection.manualScoreTargetLabel, isNull);
|
||||
expect(projection.manualScoreScope, isNull);
|
||||
});
|
||||
|
||||
test('falls back to neutral values for absent required fields', () {
|
||||
@ -130,6 +233,12 @@ void main() {
|
||||
expect(projection.exerciseName, '');
|
||||
expect(projection.primaryAction, WatchPrimaryAction.none);
|
||||
expect(projection.secondaryActions, isEmpty);
|
||||
expect(projection.hasManualScore, isFalse);
|
||||
expect(projection.currentManualScoreValue, isNull);
|
||||
expect(projection.canDecrementScore, isFalse);
|
||||
expect(projection.manualScoreTargetValue, isNull);
|
||||
expect(projection.manualScoreTargetLabel, isNull);
|
||||
expect(projection.manualScoreScope, isNull);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user