feat(watch): fréquence cardiaque live montre/téléphone (ticket #155)

Met en conformité l'implémentation avec le cadrage produit acté
(#155, #144, #142) : diffusion de la FC live depuis la montre vers le
téléphone via le bridge, affichage sur l'écran de séance montre
(watch_session_screen) en plus de l'écran Stats existant.

Validé : bridge fonctionnel bout-en-bout, tests watch_session_screen OK.
Artefact canonique d'installation montre confirmé :
watch_app/build/app/outputs/flutter-apk/app-release.apk
(package com.gametime.app) ; ne pas utiliser l'artefact résiduel
watch_app/build/watch_app/app/outputs/flutter-apk/app-release.apk.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 13:03:40 +02:00
parent 8bb74df763
commit 592796915f
6 changed files with 246 additions and 89 deletions

View File

@ -212,62 +212,70 @@ void main() {
viewModel.dispose();
});
testWidgets('shows telemetry only on stats page when available', (
tester,
) async {
final client = _FakeNativeWatchBridgeClient();
final viewModel = WatchSessionViewModel(nativeClient: client);
testWidgets(
'shows live heart rate on main page and full telemetry on stats',
(tester) async {
final client = _FakeNativeWatchBridgeClient();
final viewModel = WatchSessionViewModel(nativeClient: client);
tester.view.devicePixelRatio = 1;
tester.view.physicalSize = const Size(192, 192);
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
tester.view.devicePixelRatio = 1;
tester.view.physicalSize = const Size(192, 192);
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await tester.pumpWidget(
MaterialApp(
theme: watchTheme(),
home: WatchSessionScreen(viewModel: viewModel),
),
);
await tester.pumpWidget(
MaterialApp(
theme: watchTheme(),
home: WatchSessionScreen(viewModel: viewModel),
),
);
client.emitProjection(_runningProjection());
await tester.pump();
expect(find.byTooltip('Stats'), findsNothing);
client.emitProjection(_runningProjection());
await tester.pump();
expect(find.byTooltip('Stats'), findsNothing);
client.emitSensorSample(
WatchSensorSample(
sessionId: 'session-1',
capturedAtEpochMs: DateTime.utc(2026, 7, 27, 10).millisecondsSinceEpoch,
heartRateBpm: 142,
distanceMeters: 840,
caloriesKcal: 186,
),
);
await tester.pump();
client.emitSensorSample(
WatchSensorSample(
sessionId: 'session-1',
capturedAtEpochMs: DateTime.utc(
2026,
7,
27,
10,
).millisecondsSinceEpoch,
heartRateBpm: 142,
distanceMeters: 840,
caloriesKcal: 186,
),
);
await tester.pump();
expect(find.text('142 bpm'), findsNothing);
expect(find.byTooltip('Stats'), findsOneWidget);
expect(find.text('FC 142 bpm'), findsOneWidget);
expect(find.text('840 m'), findsNothing);
expect(find.text('186 kcal'), findsNothing);
expect(find.byTooltip('Stats'), findsOneWidget);
await tester.drag(
find.byKey(const ValueKey('watch-session-page')),
const Offset(-220, 0),
);
await tester.pumpAndSettle();
await tester.drag(
find.byKey(const ValueKey('watch-actions-page')),
const Offset(-220, 0),
);
await tester.pumpAndSettle();
await tester.drag(
find.byKey(const ValueKey('watch-session-page')),
const Offset(-220, 0),
);
await tester.pumpAndSettle();
await tester.drag(
find.byKey(const ValueKey('watch-actions-page')),
const Offset(-220, 0),
);
await tester.pumpAndSettle();
expect(find.text('Stats'), findsOneWidget);
expect(find.text('FC'), findsOneWidget);
expect(find.text('142 bpm'), findsOneWidget);
expect(find.text('840 m'), findsOneWidget);
expect(find.text('186 kcal'), findsOneWidget);
expect(find.text('Stats'), findsOneWidget);
expect(find.text('FC'), findsOneWidget);
expect(find.text('142 bpm'), findsOneWidget);
expect(find.text('840 m'), findsOneWidget);
expect(find.text('186 kcal'), findsOneWidget);
await tester.pumpWidget(const SizedBox.shrink());
viewModel.dispose();
});
await tester.pumpWidget(const SizedBox.shrink());
viewModel.dispose();
},
);
testWidgets(
'sends pause command from the icon button when a timer dominates',