Répare la régression de packaging introduite par le reset/remerge de develop : l'APK montre était packagée avec com.gametime.app au lieu de com.gametime.watch (namespace correct mais applicationId hérité du téléphone), ce qui cassait la détection du bridge. Aligne applicationId sur com.gametime.watch, nettoie la config de signing partagée sur les deux modules android/app et watch_app/android/app, et ajuste use_cases + watch_session_screen en conséquence. Validé : APK montre release package = com.gametime.watch, même SHA-256 de certificat sur les deux APKs (70cc69e8...45c7815), tests use_cases et watch_session_screen OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
691 lines
21 KiB
Dart
691 lines
21 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:gametime_watch/application/watch_session_view_model.dart';
|
|
import 'package:gametime_watch/infrastructure/watch_bridge/native_watch_bridge_client.dart';
|
|
import 'package:gametime_watch/presentation/watch_session_screen.dart';
|
|
import 'package:gametime_watch/presentation/watch_theme.dart';
|
|
import 'package:watch_bridge_contract/watch_bridge_contract.dart';
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
testWidgets(
|
|
'renders an active session on a compact round-sized viewport without overflow',
|
|
(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);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: watchTheme(),
|
|
home: WatchSessionScreen(viewModel: viewModel),
|
|
),
|
|
);
|
|
|
|
client.emitProjection(_runningProjection());
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 100));
|
|
|
|
expect(find.text('Squat jump'), findsOneWidget);
|
|
expect(find.text('02:14'), findsOneWidget);
|
|
expect(find.byTooltip('Pause'), findsOneWidget);
|
|
expect(tester.takeException(), isNull);
|
|
|
|
await tester.pumpWidget(const SizedBox.shrink());
|
|
viewModel.dispose();
|
|
},
|
|
);
|
|
|
|
testWidgets(
|
|
'transitions from no-session to manual score controls without rendering a black screen',
|
|
(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);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: watchTheme(),
|
|
home: WatchSessionScreen(viewModel: viewModel),
|
|
),
|
|
);
|
|
|
|
client.emitProjection(_noSessionStartProjection());
|
|
await tester.pump();
|
|
|
|
expect(find.text('Aucune séance en cours'), findsOneWidget);
|
|
|
|
client.emitProjection(_manualScoreProjection());
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 100));
|
|
|
|
expect(find.text('Lancers francs'), findsOneWidget);
|
|
expect(find.text('Routine de tir'), findsOneWidget);
|
|
expect(find.text('Cible : 8'), findsOneWidget);
|
|
expect(find.text('3'), findsOneWidget);
|
|
expect(find.byTooltip('Ajouter'), findsOneWidget);
|
|
expect(find.byTooltip('Retirer'), findsOneWidget);
|
|
expect(tester.takeException(), isNull);
|
|
|
|
await tester.pumpWidget(const SizedBox.shrink());
|
|
viewModel.dispose();
|
|
},
|
|
);
|
|
|
|
testWidgets('does not offer a start action from the no-session screen', (
|
|
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);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: watchTheme(),
|
|
home: WatchSessionScreen(viewModel: viewModel),
|
|
),
|
|
);
|
|
|
|
client.emitProjection(_noSessionStartProjection());
|
|
await tester.pump();
|
|
|
|
expect(find.text('Aucune séance en cours'), findsOneWidget);
|
|
expect(find.widgetWithText(FilledButton, 'Démarrer'), findsNothing);
|
|
expect(client.sentCommands, isEmpty);
|
|
|
|
await tester.pumpWidget(const SizedBox.shrink());
|
|
viewModel.dispose();
|
|
});
|
|
|
|
testWidgets('starts a stopped exercise timer from the timer button', (
|
|
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);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: watchTheme(),
|
|
home: WatchSessionScreen(viewModel: viewModel),
|
|
),
|
|
);
|
|
|
|
client.emitProjection(_readyProjection());
|
|
await tester.pump();
|
|
|
|
expect(find.text('Routine de tir'), findsOneWidget);
|
|
|
|
await tester.tap(find.byTooltip('Démarrer'));
|
|
await tester.pump();
|
|
|
|
expect(
|
|
client.sentCommands.single.type,
|
|
WatchCommandType.startCurrentExercise,
|
|
);
|
|
expect(
|
|
find.byKey(const ValueKey('timer-toggle-pending-dot')),
|
|
findsOneWidget,
|
|
);
|
|
|
|
await tester.pumpWidget(const SizedBox.shrink());
|
|
viewModel.dispose();
|
|
});
|
|
|
|
testWidgets('keeps the no-session start action disabled without phone', (
|
|
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);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: watchTheme(),
|
|
home: WatchSessionScreen(viewModel: viewModel),
|
|
),
|
|
);
|
|
|
|
client.emitConnection(const WatchBridgeConnectionEvent(isReachable: false));
|
|
client.emitProjection(_noSessionStartProjection(phoneReachable: false));
|
|
await tester.pump();
|
|
|
|
expect(find.text('Téléphone indisponible'), findsOneWidget);
|
|
expect(find.widgetWithText(FilledButton, 'Démarrer'), findsNothing);
|
|
|
|
await tester.pumpWidget(const SizedBox.shrink());
|
|
viewModel.dispose();
|
|
});
|
|
|
|
testWidgets('shows compact timer controls with a manual score step', (
|
|
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);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: watchTheme(),
|
|
home: WatchSessionScreen(viewModel: viewModel),
|
|
),
|
|
);
|
|
|
|
client.emitProjection(_manualScoreProjectionWithTimer());
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 100));
|
|
|
|
expect(find.text('3'), findsOneWidget);
|
|
expect(find.text('Routine de tir'), findsOneWidget);
|
|
expect(find.text('Chrono étape 02:14'), findsOneWidget);
|
|
expect(find.byTooltip('Pause'), findsOneWidget);
|
|
expect(tester.takeException(), isNull);
|
|
|
|
await tester.pumpWidget(const SizedBox.shrink());
|
|
viewModel.dispose();
|
|
});
|
|
|
|
testWidgets('shows telemetry only on stats page when available', (
|
|
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);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: watchTheme(),
|
|
home: WatchSessionScreen(viewModel: viewModel),
|
|
),
|
|
);
|
|
|
|
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();
|
|
|
|
expect(find.text('142 bpm'), 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();
|
|
|
|
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();
|
|
});
|
|
|
|
testWidgets(
|
|
'sends pause command from the icon button when a timer dominates',
|
|
(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);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: watchTheme(),
|
|
home: WatchSessionScreen(viewModel: viewModel),
|
|
),
|
|
);
|
|
|
|
client.emitProjection(_runningProjection());
|
|
await tester.pump();
|
|
|
|
await tester.tap(find.byTooltip('Pause'));
|
|
await tester.pump();
|
|
|
|
expect(client.sentCommands.single.type, WatchCommandType.pauseSession);
|
|
|
|
await tester.pumpWidget(const SizedBox.shrink());
|
|
viewModel.dispose();
|
|
},
|
|
);
|
|
|
|
testWidgets('shows pending feedback inside the pause button', (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);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: watchTheme(),
|
|
home: WatchSessionScreen(viewModel: viewModel),
|
|
),
|
|
);
|
|
|
|
client.emitProjection(_runningProjection());
|
|
await tester.pump();
|
|
|
|
await tester.tap(find.byTooltip('Pause'));
|
|
await tester.pump();
|
|
|
|
expect(
|
|
find.byKey(const ValueKey('timer-toggle-pending-dot')),
|
|
findsOneWidget,
|
|
);
|
|
expect(client.sentCommands.single.type, WatchCommandType.pauseSession);
|
|
|
|
await tester.pumpWidget(const SizedBox.shrink());
|
|
viewModel.dispose();
|
|
});
|
|
|
|
testWidgets(
|
|
'returns to session and shows a discreet notice when a set command is rejected',
|
|
(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);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: watchTheme(),
|
|
home: WatchSessionScreen(viewModel: viewModel),
|
|
),
|
|
);
|
|
|
|
client.emitProjection(_runningProjection());
|
|
await tester.pump();
|
|
|
|
await tester.tap(find.byTooltip('Actions'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('Terminer la série'));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 200));
|
|
|
|
expect(
|
|
client.sentCommands.single.type,
|
|
WatchCommandType.finishCurrentSet,
|
|
);
|
|
expect(find.text('Squat jump'), findsOneWidget);
|
|
|
|
client.emitAck(
|
|
WatchCommandAckEvent(
|
|
commandId: client.sentCommands.single.commandId,
|
|
status: WatchCommandAck.rejectedNotApplicable,
|
|
sessionId: 'session-1',
|
|
),
|
|
);
|
|
await tester.pump();
|
|
await tester.pump();
|
|
|
|
expect(find.text('Série non modifiée'), findsOneWidget);
|
|
|
|
await tester.pump(const Duration(milliseconds: 1900));
|
|
expect(find.text('Série non modifiée'), findsNothing);
|
|
|
|
await tester.pumpWidget(const SizedBox.shrink());
|
|
viewModel.dispose();
|
|
},
|
|
);
|
|
|
|
testWidgets(
|
|
'keeps rest skip secondary action open until the session projection updates',
|
|
(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);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: watchTheme(),
|
|
home: WatchSessionScreen(viewModel: viewModel),
|
|
),
|
|
);
|
|
|
|
client.emitProjection(_secondaryRestActionProjection());
|
|
await tester.pump();
|
|
|
|
await tester.tap(find.byTooltip('Actions'));
|
|
await tester.pumpAndSettle();
|
|
_expectActionsPageVisible(tester);
|
|
|
|
await tester.tap(find.text('Passer le repos'));
|
|
await tester.pump();
|
|
|
|
expect(client.sentCommands.single.type, WatchCommandType.skipCurrentRest);
|
|
_expectActionsPageVisible(tester);
|
|
|
|
client.emitProjection(_runningProjection());
|
|
await tester.pump();
|
|
await tester.pumpAndSettle();
|
|
|
|
_expectSessionPageVisible(tester);
|
|
|
|
await tester.pumpWidget(const SizedBox.shrink());
|
|
viewModel.dispose();
|
|
},
|
|
);
|
|
|
|
testWidgets(
|
|
'keeps rest skip secondary action open and shows feedback when rejected',
|
|
(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);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: watchTheme(),
|
|
home: WatchSessionScreen(viewModel: viewModel),
|
|
),
|
|
);
|
|
|
|
client.emitProjection(_secondaryRestActionProjection());
|
|
await tester.pump();
|
|
|
|
await tester.tap(find.byTooltip('Actions'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('Passer le repos'));
|
|
await tester.pump();
|
|
|
|
client.emitAck(
|
|
WatchCommandAckEvent(
|
|
commandId: client.sentCommands.single.commandId,
|
|
status: WatchCommandAck.rejectedNotApplicable,
|
|
sessionId: 'session-1',
|
|
),
|
|
);
|
|
await tester.pump();
|
|
await tester.pump();
|
|
|
|
_expectActionsPageVisible(tester);
|
|
expect(find.text('Commande non appliquée'), findsOneWidget);
|
|
|
|
await tester.pumpWidget(const SizedBox.shrink());
|
|
viewModel.dispose();
|
|
},
|
|
);
|
|
}
|
|
|
|
final class _FakeNativeWatchBridgeClient implements NativeWatchBridgeClient {
|
|
final _projectionController =
|
|
StreamController<WatchSessionProjection>.broadcast();
|
|
final _sensorSampleController =
|
|
StreamController<WatchSensorSample>.broadcast();
|
|
final _ackController = StreamController<WatchCommandAckEvent>.broadcast();
|
|
final _connectionController =
|
|
StreamController<WatchBridgeConnectionEvent>.broadcast();
|
|
|
|
var resyncRequests = 0;
|
|
var capabilityRefreshRequests = 0;
|
|
final sentCommands = <WatchCommandEnvelope>[];
|
|
|
|
@override
|
|
Stream<WatchSessionProjection> get projections =>
|
|
_projectionController.stream;
|
|
|
|
@override
|
|
Stream<WatchSensorSample> get sensorSamples => _sensorSampleController.stream;
|
|
|
|
@override
|
|
Stream<WatchCommandAckEvent> get acks => _ackController.stream;
|
|
|
|
@override
|
|
Stream<WatchBridgeConnectionEvent> get connectionEvents =>
|
|
_connectionController.stream;
|
|
|
|
void emitProjection(WatchSessionProjection projection) {
|
|
_projectionController.add(projection);
|
|
}
|
|
|
|
void emitSensorSample(WatchSensorSample sample) {
|
|
_sensorSampleController.add(sample);
|
|
}
|
|
|
|
void emitAck(WatchCommandAckEvent ack) {
|
|
_ackController.add(ack);
|
|
}
|
|
|
|
void emitConnection(WatchBridgeConnectionEvent event) {
|
|
_connectionController.add(event);
|
|
}
|
|
|
|
@override
|
|
Future<void> requestCapabilityRefresh() async {
|
|
capabilityRefreshRequests += 1;
|
|
}
|
|
|
|
@override
|
|
Future<void> requestResync() async {
|
|
resyncRequests += 1;
|
|
}
|
|
|
|
@override
|
|
Future<void> sendCommand(WatchCommandEnvelope command) async {
|
|
sentCommands.add(command);
|
|
}
|
|
}
|
|
|
|
WatchSessionProjection _runningProjection() {
|
|
return WatchSessionProjection(
|
|
deviceSessionId: 'session-1',
|
|
revision: 1,
|
|
projectedAtEpochMs: DateTime.utc(2026, 7, 27, 10).millisecondsSinceEpoch,
|
|
phase: WatchSessionPhase.running,
|
|
phoneReachable: true,
|
|
seriesIndex: 2,
|
|
seriesTotal: 4,
|
|
exerciseName: 'Squat jump',
|
|
statusLabel: 'Séance active',
|
|
primaryAction: WatchPrimaryAction.pauseSession,
|
|
dominantTimer: _runningStepTimer(),
|
|
secondaryTimers: const [
|
|
WatchTimerProjection(
|
|
kind: WatchTimerKind.setTimer,
|
|
label: 'Série',
|
|
displayMode: WatchTimerDisplayMode.elapsed,
|
|
runState: WatchTimerRunState.running,
|
|
referenceEpochMs: 0,
|
|
accumulatedMs: 45000,
|
|
),
|
|
],
|
|
secondaryActions: const [WatchSecondaryAction.finishCurrentSet],
|
|
);
|
|
}
|
|
|
|
WatchSessionProjection _noSessionStartProjection({bool phoneReachable = true}) {
|
|
return WatchSessionProjection(
|
|
deviceSessionId: '',
|
|
revision: 0,
|
|
projectedAtEpochMs: DateTime.utc(2026, 7, 27, 10).millisecondsSinceEpoch,
|
|
phase: WatchSessionPhase.noActiveSession,
|
|
phoneReachable: phoneReachable,
|
|
seriesIndex: 0,
|
|
seriesTotal: 0,
|
|
exerciseName: '',
|
|
statusLabel: 'Aucune séance',
|
|
primaryAction: WatchPrimaryAction.none,
|
|
);
|
|
}
|
|
|
|
WatchSessionProjection _readyProjection() {
|
|
return WatchSessionProjection(
|
|
deviceSessionId: 'session-1',
|
|
revision: 1,
|
|
projectedAtEpochMs: DateTime.utc(2026, 7, 27, 10).millisecondsSinceEpoch,
|
|
phase: WatchSessionPhase.ready,
|
|
phoneReachable: true,
|
|
seriesIndex: 1,
|
|
seriesTotal: 3,
|
|
exerciseName: 'Lancers francs',
|
|
stepName: 'Routine de tir',
|
|
statusLabel: 'Prêt à démarrer',
|
|
primaryAction: WatchPrimaryAction.startCurrentExercise,
|
|
dominantTimer: const WatchTimerProjection(
|
|
kind: WatchTimerKind.step,
|
|
label: 'Chrono étape',
|
|
displayMode: WatchTimerDisplayMode.countdown,
|
|
runState: WatchTimerRunState.stopped,
|
|
referenceEpochMs: 0,
|
|
accumulatedMs: 0,
|
|
targetMs: 30000,
|
|
),
|
|
);
|
|
}
|
|
|
|
WatchSessionProjection _secondaryRestActionProjection() {
|
|
return WatchSessionProjection(
|
|
deviceSessionId: 'session-1',
|
|
revision: 4,
|
|
projectedAtEpochMs: DateTime.utc(2026, 7, 27, 10).millisecondsSinceEpoch,
|
|
phase: WatchSessionPhase.running,
|
|
phoneReachable: true,
|
|
seriesIndex: 2,
|
|
seriesTotal: 4,
|
|
exerciseName: 'Squat jump',
|
|
statusLabel: 'Séance active',
|
|
primaryAction: WatchPrimaryAction.pauseSession,
|
|
secondaryActions: const [WatchSecondaryAction.skipCurrentRest],
|
|
);
|
|
}
|
|
|
|
void _expectActionsPageVisible(WidgetTester tester) {
|
|
expect(
|
|
tester.getTopLeft(find.byKey(const ValueKey('watch-actions-page'))).dx,
|
|
lessThan(96),
|
|
);
|
|
}
|
|
|
|
void _expectSessionPageVisible(WidgetTester tester) {
|
|
expect(
|
|
tester.getTopLeft(find.byKey(const ValueKey('watch-session-page'))).dx,
|
|
lessThan(96),
|
|
);
|
|
}
|
|
|
|
WatchSessionProjection _manualScoreProjection() {
|
|
return WatchSessionProjection(
|
|
deviceSessionId: 'session-1',
|
|
revision: 2,
|
|
projectedAtEpochMs: DateTime.utc(2026, 7, 27, 10).millisecondsSinceEpoch,
|
|
phase: WatchSessionPhase.running,
|
|
phoneReachable: true,
|
|
seriesIndex: 1,
|
|
seriesTotal: 3,
|
|
exerciseName: 'Lancers francs',
|
|
stepName: 'Routine de tir',
|
|
statusLabel: 'Score manuel',
|
|
primaryAction: WatchPrimaryAction.pauseSession,
|
|
hasManualScore: true,
|
|
currentManualScoreValue: 3,
|
|
canDecrementScore: true,
|
|
manualScoreTargetValue: 8,
|
|
manualScoreTargetLabel: 'Cible',
|
|
manualScoreScope: WatchManualScoreScope.series,
|
|
);
|
|
}
|
|
|
|
WatchSessionProjection _manualScoreProjectionWithTimer() {
|
|
return WatchSessionProjection(
|
|
deviceSessionId: 'session-1',
|
|
revision: 3,
|
|
projectedAtEpochMs: DateTime.utc(2026, 7, 27, 10).millisecondsSinceEpoch,
|
|
phase: WatchSessionPhase.running,
|
|
phoneReachable: true,
|
|
seriesIndex: 1,
|
|
seriesTotal: 3,
|
|
exerciseName: 'Lancers francs',
|
|
stepName: 'Routine de tir',
|
|
statusLabel: 'Score manuel',
|
|
primaryAction: WatchPrimaryAction.pauseSession,
|
|
dominantTimer: _runningStepTimer(),
|
|
hasManualScore: true,
|
|
currentManualScoreValue: 3,
|
|
canDecrementScore: true,
|
|
manualScoreTargetValue: 8,
|
|
manualScoreTargetLabel: 'Cible',
|
|
manualScoreScope: WatchManualScoreScope.step,
|
|
);
|
|
}
|
|
|
|
WatchTimerProjection _runningStepTimer() {
|
|
return WatchTimerProjection(
|
|
kind: WatchTimerKind.step,
|
|
label: 'Chrono étape',
|
|
displayMode: WatchTimerDisplayMode.elapsed,
|
|
runState: WatchTimerRunState.running,
|
|
referenceEpochMs: DateTime.now()
|
|
.toUtc()
|
|
.add(const Duration(minutes: 1))
|
|
.millisecondsSinceEpoch,
|
|
accumulatedMs: 134000,
|
|
startedAtEpochMs: DateTime.now().toUtc().millisecondsSinceEpoch,
|
|
targetMs: 180000,
|
|
);
|
|
}
|