import 'dart:convert'; import 'package:test/test.dart'; import 'package:watch_bridge_contract/watch_bridge_contract.dart'; void main() { group('WatchCommandEnvelope', () { test('round-trips every command type through JSON', () { for (final type in WatchCommandType.values) { final command = WatchCommandEnvelope( commandId: 'command-${type.name}', type: type, sessionId: 'session-1', expectedRevision: 12, sentAtEpochMs: 1710000000000, ); final decoded = WatchCommandEnvelope.fromJson( jsonDecode(jsonEncode(command.toJson())) as Map, ); expect(decoded, command); } }); test('ignores unknown fields and falls back for missing fields', () { final command = WatchCommandEnvelope.fromJson({ 'type': 'pauseSession', 'unknown': 'ignored', }); expect(command.schemaVersion, watchBridgeSchemaVersion); expect(command.commandId, ''); expect(command.type, WatchCommandType.pauseSession); expect(command.sessionId, ''); expect(command.expectedRevision, 0); expect(command.sentAtEpochMs, 0); }); }); group('WatchCommandAck', () { test('round-trips every ack enum value by stable JSON name', () { for (final ack in WatchCommandAck.values) { final encoded = jsonEncode(ack.name); final decodedName = jsonDecode(encoded) as String; final decoded = WatchCommandAck.values.singleWhere( (value) => value.name == decodedName, ); expect(decoded, ack); } }); }); group('WatchSessionProjection', () { test('round-trips every phase, primary action, and secondary action', () { for (final phase in WatchSessionPhase.values) { for (final primaryAction in WatchPrimaryAction.values) { final projection = WatchSessionProjection( deviceSessionId: 'session-${phase.name}-${primaryAction.name}', revision: 4, projectedAtEpochMs: 1710000000100, phase: phase, phoneReachable: true, seriesIndex: 2, seriesTotal: 5, exerciseName: 'Pompes tempo', passageIndex: 1, passageTotal: 3, stepIndex: 2, stepTotal: 4, stepName: 'Descente', dominantTimer: _stepTimer(), secondaryTimers: [_scoreStopwatchTimer(), _setTimer()], primaryAction: primaryAction, secondaryActions: WatchSecondaryAction.values, nextExerciseName: 'Fentes sautees', statusLabel: 'Chrono etape', ); final decoded = WatchSessionProjection.fromJson( jsonDecode(jsonEncode(projection.toJson())) as Map, ); expect(decoded, projection); } } }); test('ignores unknown fields and accepts absent optional fields', () { final projection = WatchSessionProjection.fromJson({ 'schemaVersion': 1, 'deviceSessionId': 'session-1', 'revision': 9, 'projectedAtEpochMs': 1710000000200, 'phase': 'restPaused', 'phoneReachable': true, 'seriesIndex': 3, 'seriesTotal': 5, 'exerciseName': 'Burpees', 'primaryAction': 'resumeSession', 'secondaryActions': ['skipCurrentRest', 'futureAction'], 'extra': {'ignored': true}, }); expect(projection.phase, WatchSessionPhase.restPaused); expect(projection.passageIndex, isNull); expect(projection.stepIndex, isNull); expect(projection.dominantTimer, isNull); expect(projection.secondaryTimers, isEmpty); expect(projection.primaryAction, WatchPrimaryAction.resumeSession); expect(projection.secondaryActions, [ WatchSecondaryAction.skipCurrentRest, ]); expect(projection.nextExerciseName, isNull); expect(projection.statusLabel, isNull); }); test('falls back to neutral values for absent required fields', () { final projection = WatchSessionProjection.fromJson({}); expect(projection.schemaVersion, watchBridgeSchemaVersion); expect(projection.deviceSessionId, ''); expect(projection.revision, 0); expect(projection.projectedAtEpochMs, 0); expect(projection.phase, WatchSessionPhase.noActiveSession); expect(projection.phoneReachable, false); expect(projection.seriesIndex, 0); expect(projection.seriesTotal, 0); expect(projection.exerciseName, ''); expect(projection.primaryAction, WatchPrimaryAction.none); expect(projection.secondaryActions, isEmpty); }); }); group('WatchTimerProjection', () { test('round-trips every timer enum combination through JSON', () { for (final kind in WatchTimerKind.values) { for (final displayMode in WatchTimerDisplayMode.values) { for (final runState in WatchTimerRunState.values) { final timer = WatchTimerProjection( kind: kind, label: 'timer-${kind.name}', displayMode: displayMode, runState: runState, referenceEpochMs: 1710000000300, accumulatedMs: 42000, startedAtEpochMs: 1710000000000, targetMs: 90000, ); final decoded = WatchTimerProjection.fromJson( jsonDecode(jsonEncode(timer.toJson())) as Map, ); expect(decoded, timer); } } } }); test('ignores unknown fields and falls back for missing fields', () { final timer = WatchTimerProjection.fromJson({ 'kind': 'rest', 'displayMode': 'countdown', 'runState': 'paused', 'unknown': 'ignored', }); expect(timer.kind, WatchTimerKind.rest); expect(timer.label, ''); expect(timer.displayMode, WatchTimerDisplayMode.countdown); expect(timer.runState, WatchTimerRunState.paused); expect(timer.referenceEpochMs, 0); expect(timer.accumulatedMs, 0); expect(timer.startedAtEpochMs, isNull); expect(timer.targetMs, isNull); }); }); } WatchTimerProjection _stepTimer() { return const WatchTimerProjection( kind: WatchTimerKind.step, label: 'Chrono etape', displayMode: WatchTimerDisplayMode.countdown, runState: WatchTimerRunState.running, referenceEpochMs: 1710000000000, accumulatedMs: 18000, startedAtEpochMs: 1710000000000, targetMs: 20000, ); } WatchTimerProjection _scoreStopwatchTimer() { return const WatchTimerProjection( kind: WatchTimerKind.scoreStopwatch, label: 'Score chrono', displayMode: WatchTimerDisplayMode.elapsed, runState: WatchTimerRunState.running, referenceEpochMs: 1710000000000, accumulatedMs: 51000, startedAtEpochMs: 1710000000000, ); } WatchTimerProjection _setTimer() { return const WatchTimerProjection( kind: WatchTimerKind.setTimer, label: 'Temps de serie', displayMode: WatchTimerDisplayMode.elapsed, runState: WatchTimerRunState.running, referenceEpochMs: 1710000000000, accumulatedMs: 102000, startedAtEpochMs: 1710000000000, ); }