Files
GameTime/packages/watch_bridge_contract/test/watch_bridge_contract_test.dart

218 lines
7.1 KiB
Dart

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<String, Object?>,
);
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<String, Object?>,
);
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<String, Object?>,
);
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,
);
}