MotorSignal-App/test/lean_angle_tap_calibrate_te...

253 lines
8.0 KiB
Dart

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<RideSession> 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<void> calibrateUpright({int? motorcycleId}) async {
onCalibrate();
}
}