chore(wip): consolidation intermédiaire multi-tickets (sprints Statistiques, UI, Bug resolution, Serveur-client)
Regroupe l'état de travail en cours réalisé dans un même worktree sur plusieurs tickets/sprints (#85, #136, #145, #155-160, #162-164), mélangeant des tickets QA et inProgress. Ne constitue pas une feature terminée : commit de sauvegarde avant triage/split par ticket en branches feature/* dédiées. Exclut les dossiers d'environnement de build locaux et le heap dump parasite (.gitignore mis à jour). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.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';
|
||||
@ -35,7 +36,10 @@ void main() {
|
||||
|
||||
expect(find.text('Squat jump'), findsOneWidget);
|
||||
expect(find.text('02:14'), findsOneWidget);
|
||||
expect(find.text('Chrono étape'), findsOneWidget);
|
||||
expect(find.byTooltip('Pause'), findsOneWidget);
|
||||
expect(find.text('Séance active'), findsNothing);
|
||||
expect(find.text('Série 00:45'), findsNothing);
|
||||
expect(tester.takeException(), isNull);
|
||||
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
@ -83,6 +87,39 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('hides set timer even when it is projected as dominant', (
|
||||
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(_setTimerDominantProjection());
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
expect(find.text('Chrono étape'), findsOneWidget);
|
||||
expect(find.text('02:14'), findsOneWidget);
|
||||
expect(find.text('Série'), findsNothing);
|
||||
expect(find.text('00:45'), findsNothing);
|
||||
expect(find.byTooltip('Pause'), 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 {
|
||||
@ -204,7 +241,8 @@ void main() {
|
||||
|
||||
expect(find.text('3'), findsOneWidget);
|
||||
expect(find.text('Routine de tir'), findsOneWidget);
|
||||
expect(find.text('Chrono étape 02:14'), findsOneWidget);
|
||||
expect(find.text('Chrono étape'), findsOneWidget);
|
||||
expect(find.text('02:14'), findsOneWidget);
|
||||
expect(find.byTooltip('Pause'), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
|
||||
@ -250,7 +288,9 @@ void main() {
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('FC 142 bpm'), findsOneWidget);
|
||||
expect(find.text('FC 142 bpm'), findsNothing);
|
||||
expect(find.byIcon(Icons.favorite), findsOneWidget);
|
||||
expect(find.text('142 bpm'), findsOneWidget);
|
||||
expect(find.text('840 m'), findsNothing);
|
||||
expect(find.text('186 kcal'), findsNothing);
|
||||
expect(find.byTooltip('Stats'), findsOneWidget);
|
||||
@ -478,6 +518,122 @@ void main() {
|
||||
viewModel.dispose();
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'renders rest without overlapping series progress and next name',
|
||||
(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(_restProjection());
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
expect(find.text('REPOS'), findsOneWidget);
|
||||
expect(find.text('Après série 2/4'), findsOneWidget);
|
||||
expect(find.text('Ensuite'), findsOneWidget);
|
||||
expect(find.text('Gainage latéral'), findsOneWidget);
|
||||
expect(find.text('Série 2/4'), findsNothing);
|
||||
expect(find.textContaining('Ensuite :'), findsNothing);
|
||||
expect(find.text('00:30'), findsOneWidget);
|
||||
expect(find.byTooltip('Pause'), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
viewModel.dispose();
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('hides timer controls when no active timer is projected', (
|
||||
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(_manualScoreProjection());
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Chrono étape'), findsNothing);
|
||||
expect(find.text('02:14'), findsNothing);
|
||||
expect(find.byTooltip('Pause'), findsNothing);
|
||||
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
viewModel.dispose();
|
||||
});
|
||||
|
||||
testWidgets('vibrates once when a countdown timer reaches zero', (
|
||||
tester,
|
||||
) async {
|
||||
final hapticCalls = <MethodCall>[];
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(SystemChannels.platform, (call) async {
|
||||
if (call.method == 'HapticFeedback.vibrate') {
|
||||
hapticCalls.add(call);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
addTearDown(() {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(SystemChannels.platform, null);
|
||||
});
|
||||
|
||||
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(_countdownProjection(accumulatedMs: 29000));
|
||||
await tester.pump();
|
||||
expect(hapticCalls, isEmpty);
|
||||
|
||||
client.emitProjection(_countdownProjection(accumulatedMs: 30000));
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
expect(hapticCalls, hasLength(1));
|
||||
expect(hapticCalls.single.arguments, 'HapticFeedbackType.heavyImpact');
|
||||
|
||||
client.emitProjection(_countdownProjection(accumulatedMs: 30000));
|
||||
await tester.pump();
|
||||
expect(hapticCalls, hasLength(1));
|
||||
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
viewModel.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
final class _FakeNativeWatchBridgeClient implements NativeWatchBridgeClient {
|
||||
@ -622,6 +778,31 @@ WatchSessionProjection _secondaryRestActionProjection() {
|
||||
);
|
||||
}
|
||||
|
||||
WatchSessionProjection _setTimerDominantProjection() {
|
||||
return WatchSessionProjection(
|
||||
deviceSessionId: 'session-1',
|
||||
revision: 6,
|
||||
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: const WatchTimerProjection(
|
||||
kind: WatchTimerKind.setTimer,
|
||||
label: 'Série',
|
||||
displayMode: WatchTimerDisplayMode.elapsed,
|
||||
runState: WatchTimerRunState.running,
|
||||
referenceEpochMs: 0,
|
||||
accumulatedMs: 45000,
|
||||
),
|
||||
secondaryTimers: [_runningStepTimer()],
|
||||
secondaryActions: const [WatchSecondaryAction.finishCurrentSet],
|
||||
);
|
||||
}
|
||||
|
||||
void _expectActionsPageVisible(WidgetTester tester) {
|
||||
expect(
|
||||
tester.getTopLeft(find.byKey(const ValueKey('watch-actions-page'))).dx,
|
||||
@ -681,6 +862,60 @@ WatchSessionProjection _manualScoreProjectionWithTimer() {
|
||||
);
|
||||
}
|
||||
|
||||
WatchSessionProjection _restProjection() {
|
||||
return WatchSessionProjection(
|
||||
deviceSessionId: 'session-1',
|
||||
revision: 5,
|
||||
projectedAtEpochMs: DateTime.utc(2026, 7, 27, 10).millisecondsSinceEpoch,
|
||||
phase: WatchSessionPhase.restRunning,
|
||||
phoneReachable: true,
|
||||
seriesIndex: 2,
|
||||
seriesTotal: 4,
|
||||
exerciseName: 'Squat jump',
|
||||
nextExerciseName: 'Gainage latéral',
|
||||
statusLabel: 'Repos',
|
||||
primaryAction: WatchPrimaryAction.pauseSession,
|
||||
dominantTimer: WatchTimerProjection(
|
||||
kind: WatchTimerKind.rest,
|
||||
label: 'Repos',
|
||||
displayMode: WatchTimerDisplayMode.countdown,
|
||||
runState: WatchTimerRunState.running,
|
||||
referenceEpochMs: DateTime.now()
|
||||
.toUtc()
|
||||
.add(const Duration(minutes: 1))
|
||||
.millisecondsSinceEpoch,
|
||||
accumulatedMs: 30000,
|
||||
startedAtEpochMs: DateTime.now().toUtc().millisecondsSinceEpoch,
|
||||
targetMs: 60000,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
WatchSessionProjection _countdownProjection({required int accumulatedMs}) {
|
||||
return WatchSessionProjection(
|
||||
deviceSessionId: 'session-1',
|
||||
revision: accumulatedMs,
|
||||
projectedAtEpochMs: DateTime.utc(2026, 7, 27, 10).millisecondsSinceEpoch,
|
||||
phase: WatchSessionPhase.running,
|
||||
phoneReachable: true,
|
||||
seriesIndex: 1,
|
||||
seriesTotal: 3,
|
||||
exerciseName: 'Gainage',
|
||||
statusLabel: 'Chrono étape',
|
||||
primaryAction: WatchPrimaryAction.pauseSession,
|
||||
dominantTimer: WatchTimerProjection(
|
||||
kind: WatchTimerKind.step,
|
||||
label: 'Chrono étape',
|
||||
displayMode: WatchTimerDisplayMode.countdown,
|
||||
runState: WatchTimerRunState.running,
|
||||
referenceEpochMs: DateTime.utc(2026, 7, 27, 10).millisecondsSinceEpoch,
|
||||
accumulatedMs: accumulatedMs,
|
||||
startedAtEpochMs: null,
|
||||
targetMs: 30000,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
WatchTimerProjection _runningStepTimer() {
|
||||
return WatchTimerProjection(
|
||||
kind: WatchTimerKind.step,
|
||||
|
||||
Reference in New Issue
Block a user