import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:motorcycle_dash/features/dashboard/widgets/lean_angle_indicator.dart'; import 'package:motorcycle_dash/features/rides/ride_state_machine.dart'; import 'package:motorcycle_dash/models/telemetry_sample.dart'; import 'package:motorcycle_dash/services/telemetry_service.dart'; import 'package:motorcycle_dash/services/sensor_fusion_service.dart'; import 'package:motorcycle_dash/services/calibration_service.dart'; void main() { testWidgets('LeanAngleIndicator tap while moving (> 3.0 MPH) blocks calibration with warning', (tester) async { final sample = TelemetrySample( timestamp: DateTime.now(), leanAngle: 12.0, speed: 25.0, // Moving at 25 MPH ); await tester.pumpWidget( ProviderScope( overrides: [ telemetryStreamProvider.overrideWith( (ref) => Stream.value(sample), ), rideStateMachineProvider.overrideWith( (ref) => MockRideStateMachine( RideSession(state: RideState.riding), ), ), ], child: const MaterialApp( home: Scaffold( body: Center( child: LeanAngleIndicator(), ), ), ), ), ); await tester.pumpAndSettle(); // Verify indicator rendered expect(find.byKey(const ValueKey('lean_angle_indicator_tap')), findsOneWidget); // Tap on the indicator await tester.tap(find.byKey(const ValueKey('lean_angle_indicator_tap'))); await tester.pump(); // Verify warning snackbar is shown expect(find.byKey(const ValueKey('lean_calibrate_moving_warning')), findsOneWidget); expect(find.textContaining('Cannot calibrate while moving (> 3.0 MPH)'), findsOneWidget); // Calibration dialog must NOT appear expect(find.byKey(const ValueKey('lean_calibrate_dialog')), findsNothing); }); testWidgets('LeanAngleIndicator tap while stationary (< 3.0 MPH) prompts confirmation and calibrates upright', (tester) async { final sample = TelemetrySample( timestamp: DateTime.now(), leanAngle: 0.0, speed: 0.0, // Stationary ); var calibrated = false; await tester.pumpWidget( ProviderScope( overrides: [ telemetryStreamProvider.overrideWith( (ref) => Stream.value(sample), ), rideStateMachineProvider.overrideWith( (ref) => MockRideStateMachine( RideSession(state: RideState.idle), ), ), sensorFusionServiceProvider.overrideWith((ref) => SensorFusionServiceMock(() { calibrated = true; })), ], child: const MaterialApp( home: Scaffold( body: Center( child: LeanAngleIndicator(), ), ), ), ), ); await tester.pumpAndSettle(); // Tap on indicator await tester.tap(find.byKey(const ValueKey('lean_angle_indicator_tap'))); await tester.pumpAndSettle(); // Verify dialog appears expect(find.byKey(const ValueKey('lean_calibrate_dialog')), findsOneWidget); expect(find.byKey(const ValueKey('confirm_lean_calibrate_button')), findsOneWidget); // Confirm calibration await tester.tap(find.byKey(const ValueKey('confirm_lean_calibrate_button'))); await tester.pumpAndSettle(); expect(calibrated, isTrue, reason: 'Must invoke calibrateUpright() on SensorFusionService'); expect(find.byKey(const ValueKey('lean_calibrate_success_snackbar')), findsOneWidget); }); testWidgets('LeanAngleIndicator tap while stationary (< 3.0 MPH) during active RideState.riding allows calibration', (tester) async { final sample = TelemetrySample( timestamp: DateTime.now(), leanAngle: 0.0, speed: 0.0, // Stationary at traffic stop during ride ); await tester.pumpWidget( ProviderScope( overrides: [ telemetryStreamProvider.overrideWith( (ref) => Stream.value(sample), ), rideStateMachineProvider.overrideWith( (ref) => MockRideStateMachine( RideSession(state: RideState.riding), // Actively recording ), ), ], child: const MaterialApp( home: Scaffold( body: Center( child: LeanAngleIndicator(), ), ), ), ), ); await tester.pumpAndSettle(); // Tap on indicator await tester.tap(find.byKey(const ValueKey('lean_angle_indicator_tap'))); await tester.pumpAndSettle(); // Verify dialog appears because motorcycle is stationary even though ride is recording expect(find.byKey(const ValueKey('lean_calibrate_dialog')), findsOneWidget); expect(find.byKey(const ValueKey('confirm_lean_calibrate_button')), findsOneWidget); }); testWidgets('LeanAngleIndicator tap while tilted on kickstand (> 12.0°) blocks calibration with warning', (tester) async { final sample = TelemetrySample( timestamp: DateTime.now(), leanAngle: 15.0, // Resting on kickstand speed: 0.0, ); await tester.pumpWidget( ProviderScope( overrides: [ telemetryStreamProvider.overrideWith( (ref) => Stream.value(sample), ), rideStateMachineProvider.overrideWith( (ref) => MockRideStateMachine( RideSession(state: RideState.idle), ), ), ], child: const MaterialApp( home: Scaffold( body: Center( child: LeanAngleIndicator(), ), ), ), ), ); await tester.pumpAndSettle(); await tester.tap(find.byKey(const ValueKey('lean_angle_indicator_tap'))); await tester.pump(); // Verify kickstand/tilted warning snackbar is shown expect(find.byKey(const ValueKey('lean_calibrate_tilted_warning')), findsOneWidget); expect(find.textContaining('Motorcycle is tilted (15°). Hold the bike upright'), findsOneWidget); // Calibration dialog must NOT appear expect(find.byKey(const ValueKey('lean_calibrate_dialog')), findsNothing); }); testWidgets('LeanAngleIndicator tap while acceleration is unstable (> 0.25G) blocks calibration with warning', (tester) async { final sample = TelemetrySample( timestamp: DateTime.now(), leanAngle: 0.0, speed: 0.0, longitudinalG: 0.35, // High vibration or dynamic shake ); await tester.pumpWidget( ProviderScope( overrides: [ telemetryStreamProvider.overrideWith( (ref) => Stream.value(sample), ), rideStateMachineProvider.overrideWith( (ref) => MockRideStateMachine( RideSession(state: RideState.idle), ), ), ], child: const MaterialApp( home: Scaffold( body: Center( child: LeanAngleIndicator(), ), ), ), ), ); await tester.pumpAndSettle(); await tester.tap(find.byKey(const ValueKey('lean_angle_indicator_tap'))); await tester.pump(); // Verify unstable warning snackbar is shown expect(find.byKey(const ValueKey('lean_calibrate_unstable_warning')), findsOneWidget); expect(find.textContaining('Sensors are unstable'), findsOneWidget); // Calibration dialog must NOT appear expect(find.byKey(const ValueKey('lean_calibrate_dialog')), findsNothing); }); } class MockRideStateMachine extends StateNotifier implements RideStateMachine { MockRideStateMachine(super.initialState); @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } class SensorFusionServiceMock extends SensorFusionService { final VoidCallback onCalibrate; SensorFusionServiceMock(this.onCalibrate) : super(CalibrationService()); @override Future calibrateUpright({int? motorcycleId}) async { onCalibrate(); } }