import 'package:flutter_test/flutter_test.dart'; import 'package:motorcycle_dash/services/map/map_provider_interface.dart'; import 'package:motorcycle_dash/services/navigation/navigation_session_service.dart'; import 'package:motorcycle_dash/services/routes/route_models.dart'; import 'package:motorcycle_dash/services/routes/route_provider.dart'; class _TestRouteProvider implements RouteProvider { int calculateCount = 0; List? lastStops; RoutingOptions? lastOptions; @override String get name => 'Test Provider'; @override Future calculateRoute( List stops, { bool avoidHighways = false, bool avoidTolls = false, bool avoidFerries = false, RoutingOptions? options, }) async { calculateCount++; lastStops = stops; lastOptions = options; final maneuvers = [ RouteManeuver( index: 0, instruction: 'Head toward New Route St', type: ManeuverType.depart, distanceMeters: 400.0, durationSeconds: 45, location: stops.first.latLng, ), RouteManeuver( index: 1, instruction: 'Arrive at destination', type: ManeuverType.arrive, distanceMeters: 0.0, durationSeconds: 0, location: stops.last.latLng, ), ]; return RouteCalculationResult( straightLineDistanceMeters: 1200.0, estimatedDistanceMeters: 1200.0, estimatedDurationSeconds: 150, geometry: [stops.first.latLng, stops.last.latLng], isStraightLine: false, providerName: name, maneuvers: maneuvers, options: options, ); } @override Future> searchPlaces( String query, { AppLatLng? nearLocation, }) async => []; @override Future resolvePlace(String providerId) async => throw UnimplementedError(); } void main() { group('NavigationSessionNotifier Tests', () { test('initial state is idle and empty', () { final notifier = NavigationSessionNotifier(); expect(notifier.state.status, equals(NavigationStatus.idle)); expect(notifier.state.isNavigating, isFalse); expect(notifier.state.hasActiveRoute, isFalse); expect(notifier.state.maneuvers, isEmpty); }); test('startNavigation initiates turn-by-turn guidance and records last announced instruction', () async { final notifier = NavigationSessionNotifier(); final maneuvers = [ const RouteManeuver( index: 0, instruction: 'Head north on Peachtree St', type: ManeuverType.depart, distanceMeters: 500.0, durationSeconds: 60, streetName: 'Peachtree St', location: AppLatLng(33.750, -84.388), ), const RouteManeuver( index: 1, instruction: 'Turn right on 10th St', type: ManeuverType.turnRight, distanceMeters: 1000.0, durationSeconds: 120, streetName: '10th St', location: AppLatLng(33.780, -84.385), ), const RouteManeuver( index: 2, instruction: 'Arrive at Piedmont Park', type: ManeuverType.arrive, distanceMeters: 0.0, durationSeconds: 0, streetName: 'Piedmont Park', location: AppLatLng(33.790, -84.370), ), ]; final calcResult = RouteCalculationResult( straightLineDistanceMeters: 1500.0, estimatedDistanceMeters: 1500.0, estimatedDurationSeconds: 180, geometry: const [ AppLatLng(33.750, -84.388), AppLatLng(33.780, -84.385), AppLatLng(33.790, -84.370), ], isStraightLine: false, providerName: 'AWS Location', maneuvers: maneuvers, ); await notifier.startNavigation( calculationResult: calcResult, destination: const ResolvedPlace( providerId: 'dest_1', displayName: 'Piedmont Park', latitude: 33.790, longitude: -84.370, ), ); expect(notifier.state.isNavigating, isTrue); expect(notifier.state.status, equals(NavigationStatus.navigating)); expect(notifier.state.maneuvers.length, equals(3)); expect(notifier.state.currentManeuverIndex, equals(0)); expect(notifier.state.currentManeuver?.instruction, equals('Head north on Peachtree St')); expect(notifier.state.nextManeuver?.instruction, equals('Turn right on 10th St')); expect(notifier.state.remainingDistanceMeters, equals(1500.0)); expect(notifier.state.remainingDurationSeconds, equals(180)); }); test('updateLocation advances maneuvers when rider approaches waypoint', () async { final notifier = NavigationSessionNotifier(); final maneuvers = [ const RouteManeuver( index: 0, instruction: 'Head north on Main', type: ManeuverType.depart, distanceMeters: 200.0, durationSeconds: 30, location: AppLatLng(33.7500, -84.3880), ), const RouteManeuver( index: 1, instruction: 'Turn left on Elm', type: ManeuverType.turnLeft, distanceMeters: 800.0, durationSeconds: 90, location: AppLatLng(33.7518, -84.3880), ), const RouteManeuver( index: 2, instruction: 'Arrive at destination', type: ManeuverType.arrive, distanceMeters: 0.0, durationSeconds: 0, location: AppLatLng(33.7518, -84.3960), ), ]; final calcResult = RouteCalculationResult( straightLineDistanceMeters: 1000.0, estimatedDistanceMeters: 1000.0, estimatedDurationSeconds: 120, geometry: const [ AppLatLng(33.7500, -84.3880), AppLatLng(33.7518, -84.3880), AppLatLng(33.7518, -84.3960), ], isStraightLine: false, providerName: 'AWS Location', maneuvers: maneuvers, ); await notifier.startNavigation(calculationResult: calcResult); // Rider reaches within 10 meters of maneuver 0 point along the route notifier.updateLocation(const AppLatLng(33.7500, -84.3880), speedMph: 25.0); expect(notifier.state.currentManeuverIndex, equals(1)); expect(notifier.state.currentManeuver?.instruction, equals('Turn left on Elm')); // Pause and resume notifier.pauseNavigation(); expect(notifier.state.status, equals(NavigationStatus.paused)); expect(notifier.state.isNavigating, isFalse); notifier.resumeNavigation(); expect(notifier.state.status, equals(NavigationStatus.navigating)); // Stop navigation await notifier.stopNavigation(); expect(notifier.state.status, equals(NavigationStatus.idle)); expect(notifier.state.hasActiveRoute, isFalse); }); test('off-route detection triggers genuine route recalculation with newly calculated maneuvers', () async { final testRouteProvider = _TestRouteProvider(); final notifier = NavigationSessionNotifier(routeProvider: testRouteProvider); final initialManeuvers = [ const RouteManeuver( index: 0, instruction: 'Head north on North Ave', type: ManeuverType.depart, distanceMeters: 500.0, durationSeconds: 60, location: AppLatLng(33.7500, -84.3880), ), const RouteManeuver( index: 1, instruction: 'Arrive at destination', type: ManeuverType.arrive, distanceMeters: 0.0, durationSeconds: 0, location: AppLatLng(33.7600, -84.3880), ), ]; final initialResult = RouteCalculationResult( straightLineDistanceMeters: 1100.0, estimatedDistanceMeters: 1100.0, estimatedDurationSeconds: 90, geometry: const [ AppLatLng(33.7500, -84.3880), AppLatLng(33.7600, -84.3880), ], isStraightLine: false, providerName: 'Test Provider', maneuvers: initialManeuvers, ); await notifier.startNavigation( calculationResult: initialResult, destination: const ResolvedPlace( providerId: 'dest_park', displayName: 'Park', latitude: 33.7600, longitude: -84.3880, ), ); expect(notifier.state.isNavigating, isTrue); expect(testRouteProvider.calculateCount, equals(0)); // Rider deviates far off-route (> 500 meters east) const offRoutePos = AppLatLng(33.7550, -84.3750); notifier.updateLocation(offRoutePos, speedMph: 35.0); await Future.delayed(Duration.zero); // Verify recalculation was actually invoked on the RouteProvider expect(testRouteProvider.calculateCount, equals(1)); expect(testRouteProvider.lastStops?.first.latitude, equals(offRoutePos.latitude)); expect(testRouteProvider.lastStops?.first.longitude, equals(offRoutePos.longitude)); // Verify session updated with the new recalculation result and maneuvers expect(notifier.state.isNavigating, isTrue); expect(notifier.state.currentManeuver?.instruction, equals('Head toward New Route St')); expect(notifier.state.maneuvers.length, equals(2)); }); test('toggleTts toggles voice guidance', () { final notifier = NavigationSessionNotifier(); expect(notifier.state.ttsEnabled, isTrue); notifier.toggleTts(); expect(notifier.state.ttsEnabled, isFalse); notifier.toggleTts(); expect(notifier.state.ttsEnabled, isTrue); }); }); }