325 lines
11 KiB
Dart
325 lines
11 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°) requires explicit upright confirmation and blocks unconfirmed calibration', (tester) async {
|
|
final sample = TelemetrySample(
|
|
timestamp: DateTime.now(),
|
|
leanAngle: 15.0, // Resting on kickstand
|
|
speed: 0.0,
|
|
);
|
|
|
|
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();
|
|
|
|
await tester.tap(find.byKey(const ValueKey('lean_angle_indicator_tap')));
|
|
await tester.pumpAndSettle();
|
|
|
|
// Verify recovery dialog appears with tilted warning container
|
|
expect(find.byKey(const ValueKey('lean_calibrate_dialog')), findsOneWidget);
|
|
expect(find.byKey(const ValueKey('lean_calibrate_tilted_warning_container')), findsOneWidget);
|
|
|
|
// Attempt calibration without checking the upright confirmation box
|
|
await tester.tap(find.byKey(const ValueKey('confirm_lean_calibrate_button')));
|
|
await tester.pump();
|
|
|
|
// Calibration must NOT occur and unconfirmed warning snackbar must be shown
|
|
expect(calibrated, isFalse);
|
|
expect(find.byKey(const ValueKey('lean_calibrate_tilted_warning')), findsOneWidget);
|
|
expect(find.textContaining('Motorcycle is tilted (15°). Hold the bike upright'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('Right-pinned lean gauge defect (42.0° right while upright) recovers via stationary upright confirmation', (tester) async {
|
|
final sample = TelemetrySample(
|
|
timestamp: DateTime.now(),
|
|
leanAngle: 42.0, // Right-pinned defect on Pixel 8a while Yamaha R6 is upright
|
|
speed: 0.0,
|
|
);
|
|
|
|
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();
|
|
|
|
// Verify gauge is showing tilted / right-pinned value
|
|
expect(find.text('42°'), findsOneWidget);
|
|
|
|
// Tap indicator to initiate recovery
|
|
await tester.tap(find.byKey(const ValueKey('lean_angle_indicator_tap')));
|
|
await tester.pumpAndSettle();
|
|
|
|
// Verify recovery dialog opened
|
|
expect(find.byKey(const ValueKey('lean_calibrate_dialog')), findsOneWidget);
|
|
expect(find.text('Calibrate Lean Angle (Upright Recovery)'), findsOneWidget);
|
|
expect(find.byKey(const ValueKey('confirm_physically_upright_checkbox')), findsOneWidget);
|
|
|
|
// Check the confirmation box confirming bike is physically upright
|
|
await tester.tap(find.byKey(const ValueKey('confirm_physically_upright_checkbox')));
|
|
await tester.pumpAndSettle();
|
|
|
|
// Tap recalibrate button
|
|
await tester.tap(find.byKey(const ValueKey('confirm_lean_calibrate_button')));
|
|
await tester.pumpAndSettle();
|
|
|
|
// Verify calibration succeeded
|
|
expect(calibrated, isTrue, reason: 'Right-pinned gauge must successfully recalibrate upright');
|
|
expect(find.byKey(const ValueKey('lean_calibrate_success_snackbar')), findsOneWidget);
|
|
});
|
|
|
|
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();
|
|
}
|
|
}
|