373 lines
13 KiB
Dart
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));
|
|
});
|
|
});
|
|
}
|