MotorSignal-App/test/performance_launch_screen_t...

650 lines
28 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:async';
import 'package:drift/native.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:motorcycle_dash/core/theme/theme.dart';
import 'package:motorcycle_dash/database/database.dart' hide TelemetrySample;
import 'package:motorcycle_dash/features/performance/models/performance_launch_models.dart';
import 'package:motorcycle_dash/features/performance/performance_launch_screen.dart';
import 'package:motorcycle_dash/features/performance/performance_providers.dart';
import 'package:motorcycle_dash/features/performance/services/monotonic_clock.dart';
import 'package:motorcycle_dash/features/performance/services/performance_launch_state_machine.dart';
import 'package:motorcycle_dash/features/records/records_service.dart';
import 'package:motorcycle_dash/models/telemetry_sample.dart';
import 'package:motorcycle_dash/services/validation/telemetry_validator.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late AppDatabase db;
late StreamController<TelemetrySample> telemetryController;
late FakeMonotonicClock clock;
late RecordsService recordsService;
late SharedPreferences prefs;
late PerformanceLaunchStateMachine stateMachine;
late ProviderContainer container;
setUp(() async {
SharedPreferences.setMockInitialValues({});
prefs = await SharedPreferences.getInstance();
recordsService = RecordsService(prefs);
db = AppDatabase.forTesting(NativeDatabase.memory());
telemetryController = StreamController<TelemetrySample>.broadcast();
clock = FakeMonotonicClock(
initialElapsedMs: 10000,
initialDateTime: DateTime.utc(2026, 9, 5, 12, 0, 0),
);
stateMachine = PerformanceLaunchStateMachine(
db: db,
telemetryStream: telemetryController.stream,
recordsService: recordsService,
clock: clock,
countdownDurationMs: 3000,
);
container = ProviderContainer(
overrides: [
databaseProvider.overrideWithValue(db),
monotonicClockProvider.overrideWithValue(clock),
performanceLaunchProvider.overrideWith((ref) => stateMachine),
],
);
});
tearDown(() async {
container.dispose();
await telemetryController.close();
await db.close();
});
TelemetrySample makeSample({
required double speed,
int? monotonicMs,
DateTime? timestamp,
int? seq,
ValidationStatus status = ValidationStatus.valid,
double confidence = 1.0,
}) {
final ms = monotonicMs ?? clock.elapsedMilliseconds;
final dt = timestamp ?? clock.now;
return TelemetrySample(
timestamp: dt,
monotonicTimestampMs: ms,
sequenceNumber: seq,
speed: speed,
quality: TelemetryQuality(
status: status,
overallConfidence: confidence,
speedConfidence: confidence,
gpsConfidence: confidence,
),
);
}
Widget createTestWidget({
ThemeData? theme,
Size size = const Size(393, 852), // Pixel 8a default
double textScaleFactor = 1.0,
}) {
return UncontrolledProviderScope(
container: container,
child: MediaQuery(
data: MediaQueryData(
size: size,
textScaler: TextScaler.linear(textScaleFactor),
),
child: MaterialApp(
theme: theme ?? AppTheme.darkTheme,
home: const PerformanceLaunchScreen(),
),
),
);
}
group('PLM-2: Setup Experience Tests', () {
testWidgets('1. Auto/Countdown selector renders and 2. 60/100 selector renders', (tester) async {
await tester.pumpWidget(createTestWidget());
await tester.pumpAndSettle();
expect(find.byKey(const ValueKey('launch_mode_auto')), findsOneWidget);
expect(find.byKey(const ValueKey('launch_mode_countdown')), findsOneWidget);
expect(find.byKey(const ValueKey('launch_target_60')), findsOneWidget);
expect(find.byKey(const ValueKey('launch_target_100')), findsOneWidget);
expect(find.byKey(const ValueKey('btn_arm_launch')), findsOneWidget);
expect(find.text('Auto Launch'), findsOneWidget);
expect(find.text('Countdown'), findsOneWidget);
expect(find.text('0–60 MPH'), findsOneWidget);
expect(find.text('0–100 MPH'), findsOneWidget);
});
testWidgets('3. ARM command uses selected configuration and 4. no local fake READY state', (tester) async {
await tester.pumpWidget(createTestWidget());
await tester.pumpAndSettle();
// Tap Countdown mode and 0-100 target
await tester.tap(find.byKey(const ValueKey('launch_mode_countdown')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const ValueKey('launch_target_100')));
await tester.pumpAndSettle();
// Tap ARM
await tester.tap(find.byKey(const ValueKey('btn_arm_launch')));
await tester.pumpAndSettle();
// Since vehicle has no telemetry yet, state should NOT be READY
expect(stateMachine.state.mode, PerformanceLaunchMode.countdown);
expect(stateMachine.state.target, PerformanceLaunchTarget.hundred);
expect(stateMachine.state.state, PerformanceLaunchState.idle);
// Verify truthful waiting state renders (no fake ready)
expect(find.byKey(const ValueKey('ready_status_badge')), findsNothing);
expect(find.byKey(const ValueKey('waiting_status_text')), findsOneWidget);
});
});
group('PLM-2: READY Experience Tests', () {
testWidgets('5. waiting state renders truthfully with moving speed & transitions to READY when stopped', (tester) async {
await tester.pumpWidget(createTestWidget());
await tester.pumpAndSettle();
// Emit sample moving at 15 MPH
telemetryController.add(makeSample(speed: 15.0));
await tester.pump(const Duration(milliseconds: 20));
// Arm while moving
await tester.tap(find.byKey(const ValueKey('btn_arm_launch')));
await tester.pumpAndSettle();
// Should display COME TO A COMPLETE STOP
expect(find.text('COME TO A COMPLETE STOP'), findsOneWidget);
// Now bring motorcycle to a stop (< 1.0 MPH) with valid telemetry
telemetryController.add(makeSample(speed: 0.0, confidence: 1.0));
await tester.pump(const Duration(milliseconds: 20));
await tester.pumpAndSettle();
// Transitions to READY!
expect(find.byKey(const ValueKey('ready_status_badge')), findsOneWidget);
expect(find.text('READY'), findsOneWidget);
});
testWidgets('6. Auto READY says accelerate when ready and no countdown button', (tester) async {
await tester.pumpWidget(createTestWidget());
await tester.pumpAndSettle();
stateMachine.arm(mode: PerformanceLaunchMode.auto, target: PerformanceLaunchTarget.sixty);
telemetryController.add(makeSample(speed: 0.0, confidence: 1.0));
await tester.pump(const Duration(milliseconds: 20));
await tester.pumpAndSettle();
expect(find.byKey(const ValueKey('ready_status_badge')), findsOneWidget);
expect(find.text('Accelerate when ready'), findsOneWidget);
expect(find.byKey(const ValueKey('btn_start_countdown')), findsNothing);
});
testWidgets('7. Countdown READY exposes legitimate countdown start button', (tester) async {
await tester.pumpWidget(createTestWidget());
await tester.pumpAndSettle();
stateMachine.arm(mode: PerformanceLaunchMode.countdown, target: PerformanceLaunchTarget.sixty);
telemetryController.add(makeSample(speed: 0.0, confidence: 1.0));
await tester.pump(const Duration(milliseconds: 20));
await tester.pumpAndSettle();
expect(find.byKey(const ValueKey('ready_status_badge')), findsOneWidget);
expect(find.byKey(const ValueKey('btn_start_countdown')), findsOneWidget);
expect(find.text('Tap below to initiate countdown'), findsOneWidget);
});
testWidgets('8. READY survives widget rebuild', (tester) async {
await tester.pumpWidget(createTestWidget());
await tester.pumpAndSettle();
stateMachine.arm(mode: PerformanceLaunchMode.auto, target: PerformanceLaunchTarget.sixty);
telemetryController.add(makeSample(speed: 0.0, confidence: 1.0));
await tester.pump(const Duration(milliseconds: 20));
await tester.pumpAndSettle();
expect(find.byKey(const ValueKey('ready_status_badge')), findsOneWidget);
// Trigger rebuild
await tester.pumpWidget(createTestWidget(size: const Size(800, 360))); // landscape resize
await tester.pumpAndSettle();
expect(find.byKey(const ValueKey('ready_status_badge')), findsOneWidget);
expect(stateMachine.state.state, PerformanceLaunchState.ready);
});
});
group('PLM-2: Countdown Tests', () {
testWidgets('9. 3/2/1/GO derives from authoritative monotonic state & 10. widget animation is not timing authority', (tester) async {
clock.setElapsedMs(1000);
await tester.pumpWidget(createTestWidget());
await tester.pumpAndSettle();
stateMachine.arm(mode: PerformanceLaunchMode.countdown, target: PerformanceLaunchTarget.sixty);
telemetryController.add(makeSample(speed: 0.0, monotonicMs: 1000));
await tester.pump(const Duration(milliseconds: 20));
await tester.pumpAndSettle();
// Start countdown: scheduledGo = 1000 + 3000 = 4000
await tester.tap(find.byKey(const ValueKey('btn_start_countdown')));
await tester.pump(const Duration(milliseconds: 20));
// At t=1000ms: diff is 3000ms -> shows '3'
expect(find.text('3'), findsOneWidget);
// Advance monotonic clock to t=2500ms: diff is 1500ms -> shows '2'
clock.setElapsedMs(2500);
await tester.pump(const Duration(milliseconds: 40));
expect(find.text('2'), findsOneWidget);
// Advance monotonic clock to t=3500ms: diff is 500ms -> shows '1'
clock.setElapsedMs(3500);
await tester.pump(const Duration(milliseconds: 40));
expect(find.text('1'), findsOneWidget);
// Advance monotonic clock to t=4100ms (past scheduled GO): shows 'GO!'
clock.setElapsedMs(4100);
await tester.pump(const Duration(milliseconds: 40));
expect(find.text('GO!'), findsOneWidget);
});
testWidgets('11. rebuild does not restart countdown and 12. orientation/viewport resize preserves schedule', (tester) async {
clock.setElapsedMs(1000);
await tester.pumpWidget(createTestWidget());
await tester.pumpAndSettle();
stateMachine.arm(mode: PerformanceLaunchMode.countdown, target: PerformanceLaunchTarget.sixty);
telemetryController.add(makeSample(speed: 0.0, monotonicMs: 1000));
await tester.pump(const Duration(milliseconds: 20));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const ValueKey('btn_start_countdown')));
await tester.pump(const Duration(milliseconds: 20));
final scheduledGo = stateMachine.state.scheduledGoAtMonotonicMs;
expect(scheduledGo, 4000);
// Advance time to t=2500 (showing 2)
clock.setElapsedMs(2500);
await tester.pump(const Duration(milliseconds: 40));
expect(find.text('2'), findsOneWidget);
// Rotate / resize to landscape
await tester.pumpWidget(createTestWidget(size: const Size(800, 360)));
await tester.pump(const Duration(milliseconds: 40));
// Still at t=2500, still shows 2, scheduledGo unchanged!
expect(find.text('2'), findsOneWidget);
expect(stateMachine.state.scheduledGoAtMonotonicMs, scheduledGo);
});
});
group('PLM-2: Launched Timing Experience Tests', () {
testWidgets('13. active timing view requires no interaction, 14. speed renders, 15. timer renders', (tester) async {
await tester.pumpWidget(createTestWidget());
await tester.pumpAndSettle();
stateMachine.arm(mode: PerformanceLaunchMode.auto, target: PerformanceLaunchTarget.sixty);
telemetryController.add(makeSample(speed: 0.0, monotonicMs: 1000));
await tester.pump(const Duration(milliseconds: 20));
await tester.pumpAndSettle();
// Launch!
telemetryController.add(makeSample(speed: 12.0, monotonicMs: 1050));
await tester.pump(const Duration(milliseconds: 20));
await tester.pumpAndSettle();
// Launched state verified
expect(stateMachine.state.state, PerformanceLaunchState.launched);
expect(find.byKey(const ValueKey('speed_readout')), findsOneWidget);
expect(find.byKey(const ValueKey('elapsed_timer_readout')), findsOneWidget);
// Update speed and time at 20 Hz
telemetryController.add(makeSample(speed: 45.0, monotonicMs: 2500));
await tester.pump(const Duration(milliseconds: 20));
expect(find.text('45'), findsOneWidget);
expect(find.text('1.45 s'), findsOneWidget);
});
testWidgets('16. 100 target displays authoritative 60 split when available', (tester) async {
await tester.pumpWidget(createTestWidget());
await tester.pumpAndSettle();
stateMachine.arm(mode: PerformanceLaunchMode.auto, target: PerformanceLaunchTarget.hundred);
telemetryController.add(makeSample(speed: 0.0, monotonicMs: 1000));
await tester.pump(const Duration(milliseconds: 20));
await tester.pumpAndSettle();
// Launch
telemetryController.add(makeSample(speed: 10.0, monotonicMs: 1050));
await tester.pump(const Duration(milliseconds: 20));
// Cross 60 MPH at t=3550ms
telemetryController.add(makeSample(speed: 62.0, monotonicMs: 3550));
await tester.pump(const Duration(milliseconds: 20));
await tester.pumpAndSettle();
expect(find.byKey(const ValueKey('split_60_readout')), findsOneWidget);
expect(find.text('60 MPH SPLIT: 2.50 s'), findsOneWidget);
});
testWidgets('17. 60 target does not invent intermediate split', (tester) async {
await tester.pumpWidget(createTestWidget());
await tester.pumpAndSettle();
stateMachine.arm(mode: PerformanceLaunchMode.auto, target: PerformanceLaunchTarget.sixty);
telemetryController.add(makeSample(speed: 0.0, monotonicMs: 1000));
await tester.pump(const Duration(milliseconds: 20));
await tester.pumpAndSettle();
telemetryController.add(makeSample(speed: 40.0, monotonicMs: 1050));
await tester.pump(const Duration(milliseconds: 20));
expect(find.byKey(const ValueKey('split_60_readout')), findsNothing);
});
});
group('PLM-2: Result Presentation Tests', () {
testWidgets('18. Auto result omits Launch Reaction', (tester) async {
await tester.pumpWidget(createTestWidget());
await tester.pumpAndSettle();
stateMachine.arm(mode: PerformanceLaunchMode.auto, target: PerformanceLaunchTarget.sixty);
telemetryController.add(makeSample(speed: 0.0, monotonicMs: 1000));
await tester.pump(const Duration(milliseconds: 20));
telemetryController.add(makeSample(speed: 5.0, monotonicMs: 1050));
await tester.pump(const Duration(milliseconds: 20));
telemetryController.add(makeSample(speed: 61.0, monotonicMs: 4250));
await tester.pump(const Duration(milliseconds: 20));
await tester.pumpAndSettle();
expect(stateMachine.state.state, PerformanceLaunchState.completed);
expect(find.text('ACCELERATION'), findsOneWidget);
expect(find.text('3.20 s'), findsOneWidget);
// Launch reaction is NOT shown for Auto
expect(find.byKey(const ValueKey('launch_reaction_readout')), findsNothing);
expect(find.text('Launch Reaction'), findsNothing);
});
testWidgets('19. Countdown result displays Launch Reaction and 20. Total From GO', (tester) async {
clock.setElapsedMs(1000);
await tester.pumpWidget(createTestWidget());
await tester.pumpAndSettle();
stateMachine.arm(mode: PerformanceLaunchMode.countdown, target: PerformanceLaunchTarget.sixty);
telemetryController.add(makeSample(speed: 0.0, monotonicMs: 1000));
await tester.pump(const Duration(milliseconds: 20));
await tester.pumpAndSettle();
// Start countdown (scheduledGo = 4000)
await tester.tap(find.byKey(const ValueKey('btn_start_countdown')));
await tester.pump(const Duration(milliseconds: 20));
// Rider moves at t=4250ms (+250ms reaction)
telemetryController.add(makeSample(speed: 5.0, monotonicMs: 4250));
await tester.pump(const Duration(milliseconds: 20));
// Cross 60 at t=7450ms (3.20s acceleration, 3.45s from GO)
telemetryController.add(makeSample(speed: 61.0, monotonicMs: 7450));
await tester.pump(const Duration(milliseconds: 20));
await tester.pumpAndSettle();
expect(stateMachine.state.state, PerformanceLaunchState.completed);
expect(find.byKey(const ValueKey('launch_reaction_readout')), findsOneWidget);
expect(find.text('+0.25 s'), findsOneWidget);
expect(find.byKey(const ValueKey('total_from_go_readout')), findsOneWidget);
expect(find.text('3.45 s'), findsOneWidget);
expect(find.text('Total From GO'), findsOneWidget);
});
testWidgets('21. false start displays negative Launch Reaction, 22. labeled, 23. not PR', (tester) async {
clock.setElapsedMs(1000);
await tester.pumpWidget(createTestWidget());
await tester.pumpAndSettle();
stateMachine.arm(mode: PerformanceLaunchMode.countdown, target: PerformanceLaunchTarget.sixty);
telemetryController.add(makeSample(speed: 0.0, monotonicMs: 1000));
await tester.pump(const Duration(milliseconds: 20));
await tester.pumpAndSettle();
// Start countdown (scheduledGo = 4000)
await tester.tap(find.byKey(const ValueKey('btn_start_countdown')));
await tester.pump(const Duration(milliseconds: 20));
// Rider jumps before GO at t=3750ms (-250ms false start)
telemetryController.add(makeSample(speed: 5.0, monotonicMs: 3750));
await tester.pump(const Duration(milliseconds: 20));
// Timing continues actively despite false start
expect(stateMachine.state.state, PerformanceLaunchState.launched);
expect(stateMachine.state.isFalseStart, isTrue);
expect(find.byKey(const ValueKey('speed_readout')), findsOneWidget);
expect(find.byKey(const ValueKey('elapsed_timer_readout')), findsOneWidget);
// Interim sample while timing
telemetryController.add(makeSample(speed: 35.0, monotonicMs: 5000));
await tester.pump(const Duration(milliseconds: 20));
expect(stateMachine.state.state, PerformanceLaunchState.launched);
// Reaches 60 at t=6950ms
telemetryController.add(makeSample(speed: 62.0, monotonicMs: 6950));
await tester.pump(const Duration(milliseconds: 20));
await tester.pumpAndSettle();
expect(stateMachine.state.state, PerformanceLaunchState.completed);
expect(stateMachine.state.lastResult!.isFalseStart, isTrue);
expect(stateMachine.state.lastResult!.accelerationTimeSec, closeTo(3.20, 0.05));
// Verify false start badge and negative reaction
expect(find.text('FALSE START'), findsOneWidget);
expect(find.text('Movement detected before GO.'), findsOneWidget);
expect(find.byKey(const ValueKey('launch_reaction_readout')), findsOneWidget);
expect(find.text('-0.25 s'), findsOneWidget);
// Verify records service was not updated
expect(recordsService.state.fastest0To60, double.infinity);
});
testWidgets('24. Try Again invokes authoritative reset', (tester) async {
await tester.pumpWidget(createTestWidget());
await tester.pumpAndSettle();
stateMachine.arm(mode: PerformanceLaunchMode.auto, target: PerformanceLaunchTarget.sixty);
telemetryController.add(makeSample(speed: 0.0, monotonicMs: 1000));
await tester.pump(const Duration(milliseconds: 20));
telemetryController.add(makeSample(speed: 5.0, monotonicMs: 1050));
await tester.pump(const Duration(milliseconds: 20));
telemetryController.add(makeSample(speed: 62.0, monotonicMs: 4000));
await tester.pump(const Duration(milliseconds: 20));
await tester.pumpAndSettle();
expect(find.byKey(const ValueKey('btn_try_again')), findsOneWidget);
// Tap Try Again
await tester.tap(find.byKey(const ValueKey('btn_try_again')));
await tester.pumpAndSettle();
// State machine reset back to idle/setup
expect(stateMachine.state.state, PerformanceLaunchState.idle);
expect(find.byKey(const ValueKey('btn_arm_launch')), findsOneWidget);
});
});
group('PLM-2: Telemetry Unavailable Tests', () {
testWidgets('26. unavailable telemetry does not display fake READY & 27. rejected telemetry represented truthfully', (tester) async {
await tester.pumpWidget(createTestWidget());
await tester.pumpAndSettle();
// Arm auto launch
await tester.tap(find.byKey(const ValueKey('btn_arm_launch')));
await tester.pumpAndSettle();
// Emit low confidence sample
telemetryController.add(makeSample(speed: 0.0, confidence: 0.2));
await tester.pump(const Duration(milliseconds: 20));
await tester.pumpAndSettle();
// Does NOT show READY
expect(find.byKey(const ValueKey('ready_status_badge')), findsNothing);
expect(find.text('WAITING FOR VALID TELEMETRY'), findsOneWidget);
});
});
group('PLM-2: Responsive & Theme Tests', () {
testWidgets('28. no RenderFlex overflow at 320dp width', (tester) async {
await tester.pumpWidget(createTestWidget(size: const Size(320, 600)));
await tester.pumpAndSettle();
expect(find.byKey(const ValueKey('btn_arm_launch')), findsOneWidget);
expect(tester.takeException(), isNull);
});
testWidgets('29. Pixel-like portrait layout renders clean', (tester) async {
await tester.pumpWidget(createTestWidget(size: const Size(412, 915)));
await tester.pumpAndSettle();
expect(find.byKey(const ValueKey('btn_arm_launch')), findsOneWidget);
expect(tester.takeException(), isNull);
});
testWidgets('30. short landscape phone layout renders clean', (tester) async {
await tester.pumpWidget(createTestWidget(size: const Size(800, 360)));
await tester.pumpAndSettle();
expect(find.byKey(const ValueKey('btn_arm_launch')), findsOneWidget);
expect(tester.takeException(), isNull);
});
testWidgets('31. standard landscape layout renders clean', (tester) async {
await tester.pumpWidget(createTestWidget(size: const Size(915, 412)));
await tester.pumpAndSettle();
expect(find.byKey(const ValueKey('btn_arm_launch')), findsOneWidget);
expect(tester.takeException(), isNull);
});
testWidgets('32. expanded tablet layout renders clean', (tester) async {
await tester.pumpWidget(createTestWidget(size: const Size(1200, 800)));
await tester.pumpAndSettle();
expect(find.byKey(const ValueKey('btn_arm_launch')), findsOneWidget);
expect(tester.takeException(), isNull);
});
testWidgets('34. text scaling smoke test (1.5x scale)', (tester) async {
await tester.pumpWidget(createTestWidget(textScaleFactor: 1.5));
await tester.pumpAndSettle();
expect(find.byKey(const ValueKey('btn_arm_launch')), findsOneWidget);
expect(tester.takeException(), isNull);
});
testWidgets('35. System/Light/Dark theme smoke test', (tester) async {
await tester.pumpWidget(createTestWidget(theme: AppTheme.lightTheme));
await tester.pumpAndSettle();
expect(find.byKey(const ValueKey('btn_arm_launch')), findsOneWidget);
await tester.pumpWidget(createTestWidget(theme: AppTheme.darkTheme));
await tester.pumpAndSettle();
expect(find.byKey(const ValueKey('btn_arm_launch')), findsOneWidget);
expect(tester.takeException(), isNull);
});
});
group('PLM-2: Lifecycle & Orientation State Preservation Tests', () {
testWidgets('36. resize does not recreate authoritative attempt & 37. rebuild does not duplicate persistence', (tester) async {
await tester.pumpWidget(createTestWidget(size: const Size(412, 915)));
await tester.pumpAndSettle();
stateMachine.arm(mode: PerformanceLaunchMode.auto, target: PerformanceLaunchTarget.sixty);
telemetryController.add(makeSample(speed: 0.0, monotonicMs: 1000));
await tester.pump(const Duration(milliseconds: 20));
telemetryController.add(makeSample(speed: 5.0, monotonicMs: 1050));
await tester.pump(const Duration(milliseconds: 20));
telemetryController.add(makeSample(speed: 61.0, monotonicMs: 4000));
await tester.pump(const Duration(milliseconds: 20));
await tester.pumpAndSettle();
expect(stateMachine.state.state, PerformanceLaunchState.completed);
// Resize/rebuild multiple times
await tester.pumpWidget(createTestWidget(size: const Size(800, 360)));
await tester.pumpAndSettle();
await tester.pumpWidget(createTestWidget(size: const Size(320, 600)));
await tester.pumpAndSettle();
// Only ONE row in performanceRuns table
await tester.runAsync(() async {
final runs = await db.select(db.performanceRuns).get();
expect(runs, hasLength(1));
});
});
testWidgets('25. Done resets state machine safely', (tester) async {
await tester.pumpWidget(createTestWidget());
await tester.pumpAndSettle();
stateMachine.arm(mode: PerformanceLaunchMode.auto, target: PerformanceLaunchTarget.sixty);
telemetryController.add(makeSample(speed: 0.0, monotonicMs: 1000));
await tester.pump(const Duration(milliseconds: 20));
telemetryController.add(makeSample(speed: 5.0, monotonicMs: 1050));
await tester.pump(const Duration(milliseconds: 20));
telemetryController.add(makeSample(speed: 61.0, monotonicMs: 4000));
await tester.pump(const Duration(milliseconds: 20));
await tester.pumpAndSettle();
expect(find.byKey(const ValueKey('btn_done')), findsOneWidget);
await tester.tap(find.byKey(const ValueKey('btn_done')));
await tester.pumpAndSettle();
expect(stateMachine.state.state, PerformanceLaunchState.idle);
});
testWidgets('33. IME insets do not break setup screen', (tester) async {
await tester.pumpWidget(
UncontrolledProviderScope(
container: container,
child: MediaQuery(
data: const MediaQueryData(
size: Size(393, 852),
viewInsets: EdgeInsets.only(bottom: 350), // IME open
),
child: MaterialApp(
theme: AppTheme.darkTheme,
home: const PerformanceLaunchScreen(),
),
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(const ValueKey('btn_arm_launch')), findsOneWidget);
expect(tester.takeException(), isNull);
});
testWidgets('38. concurrent arm calls during active launch do not corrupt attempt', (tester) async {
await tester.pumpWidget(createTestWidget());
await tester.pumpAndSettle();
stateMachine.arm(mode: PerformanceLaunchMode.auto, target: PerformanceLaunchTarget.sixty);
telemetryController.add(makeSample(speed: 0.0, monotonicMs: 1000));
await tester.pump(const Duration(milliseconds: 20));
telemetryController.add(makeSample(speed: 10.0, monotonicMs: 1050));
await tester.pump(const Duration(milliseconds: 20));
expect(stateMachine.state.state, PerformanceLaunchState.launched);
final initialArmedMs = stateMachine.state.armedAtMonotonicMs;
// Attempt concurrent re-arm during launched state
stateMachine.arm(mode: PerformanceLaunchMode.countdown, target: PerformanceLaunchTarget.hundred);
expect(stateMachine.state.state, PerformanceLaunchState.launched);
expect(stateMachine.state.mode, PerformanceLaunchMode.auto);
expect(stateMachine.state.target, PerformanceLaunchTarget.sixty);
expect(stateMachine.state.armedAtMonotonicMs, initialArmedMs);
});
});
}