MotorSignal-App/test/navigation_session_service_...

373 lines
13 KiB
Dart

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<RouteStop>? lastStops;
RoutingOptions? lastOptions;
@override
String get name => 'Test Provider';
@override
Future<RouteCalculationResult> calculateRoute(
List<RouteStop> 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<List<PlaceSearchResult>> searchPlaces(
String query, {
AppLatLng? nearLocation,
}) async =>
[];
@override
Future<ResolvedPlace> 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);
// Stationary rider at departure does NOT prematurely advance to step 1
notifier.updateLocation(const AppLatLng(33.7500, -84.3880), speedMph: 0.0);
expect(notifier.state.currentManeuverIndex, equals(0));
expect(notifier.state.currentManeuver?.instruction, equals('Head north on Main'));
// Rider departs and is moving (> 3.0 mph), advancing to step 1
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<void>.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);
});
test('remaining distance strictly counts down and maneuvers advance as rider travels', () async {
final notifier = NavigationSessionNotifier();
// Polyline with 5 points, ~1000m total
const p0 = AppLatLng(33.7500, -84.3880);
const p1 = AppLatLng(33.7525, -84.3880); // ~278m north
const p2 = AppLatLng(33.7550, -84.3880); // ~278m north (turn right)
const p3 = AppLatLng(33.7550, -84.3850); // ~278m east
const p4 = AppLatLng(33.7550, -84.3820); // ~278m east (destination)
final maneuvers = <RouteManeuver>[
const RouteManeuver(
index: 0,
instruction: 'Depart north on Main St',
type: ManeuverType.depart,
distanceMeters: 556.0,
durationSeconds: 60,
location: p0,
geometryOffset: 0,
),
const RouteManeuver(
index: 1,
instruction: 'Turn right on Oak St',
type: ManeuverType.turnRight,
distanceMeters: 556.0,
durationSeconds: 60,
location: p2,
geometryOffset: 2,
),
const RouteManeuver(
index: 2,
instruction: 'Arrive at destination',
type: ManeuverType.arrive,
distanceMeters: 0.0,
durationSeconds: 0,
location: p4,
geometryOffset: 4,
),
];
final calcResult = RouteCalculationResult(
straightLineDistanceMeters: 1000.0,
estimatedDistanceMeters: 1112.0,
estimatedDurationSeconds: 120,
geometry: const [p0, p1, p2, p3, p4],
isStraightLine: false,
providerName: 'AWS Location',
maneuvers: maneuvers,
);
await notifier.startNavigation(calculationResult: calcResult);
final initialRemaining = notifier.state.remainingDistanceMeters;
expect(initialRemaining, greaterThan(1000.0));
// Rider moves to p1 (halfway to turn)
notifier.updateLocation(p1, speedMph: 30.0);
final remainingAtP1 = notifier.state.remainingDistanceMeters;
// Remaining distance MUST decrease (never count up!)
expect(remainingAtP1, lessThan(initialRemaining));
expect(notifier.state.currentManeuverIndex, equals(1));
expect(notifier.state.currentManeuver?.instruction, equals('Turn right on Oak St'));
final distToTurnP1 = notifier.state.distanceToNextManeuverMeters;
expect(distToTurnP1, greaterThan(200.0));
expect(distToTurnP1, lessThan(350.0));
// Rider approaches turn at p2 (within 10m of p2)
notifier.updateLocation(p2, speedMph: 20.0);
final remainingAtP2 = notifier.state.remainingDistanceMeters;
expect(remainingAtP2, lessThan(remainingAtP1));
// Maneuver advances to arrival!
expect(notifier.state.currentManeuverIndex, equals(2));
expect(notifier.state.currentManeuver?.instruction, equals('Arrive at destination'));
// Rider continues to p3
notifier.updateLocation(p3, speedMph: 25.0);
final remainingAtP3 = notifier.state.remainingDistanceMeters;
expect(remainingAtP3, lessThan(remainingAtP2));
// Rider arrives at destination p4
notifier.updateLocation(p4, speedMph: 5.0);
expect(notifier.state.status, equals(NavigationStatus.arrived));
expect(notifier.state.remainingDistanceMeters, equals(0.0));
});
});
}