212 lines
7.1 KiB
Dart
212 lines
7.1 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_map/flutter_map.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:motorcycle_dash/features/routes/route_planner_screen.dart';
|
|
import 'package:motorcycle_dash/services/auth/auth_providers.dart';
|
|
import 'package:motorcycle_dash/services/auth/auth_user.dart';
|
|
import 'package:motorcycle_dash/services/map/map_provider_interface.dart';
|
|
import 'package:motorcycle_dash/services/map/map_providers.dart';
|
|
import 'package:motorcycle_dash/services/map/open_street_map_provider.dart';
|
|
import 'package:motorcycle_dash/services/map/unified_map_view.dart';
|
|
import 'package:motorcycle_dash/services/routes/route_providers.dart';
|
|
|
|
class FakeMapProvider extends Fake implements OpenStreetMapProvider {
|
|
@override
|
|
Widget buildMapView({
|
|
required BuildContext context,
|
|
required AppLatLng center,
|
|
required double zoom,
|
|
double bearing = 0.0,
|
|
required List<AppMapMarker> markers,
|
|
required List<AppPolyline> polylines,
|
|
required bool isFollowMode,
|
|
required MapConfig config,
|
|
required ValueChanged<AppLatLng>? onPositionChanged,
|
|
required VoidCallback? onUserInteraction,
|
|
}) => const SizedBox.expand();
|
|
}
|
|
|
|
void main() {
|
|
const riderPosition = AppLatLng(33.749, -84.388);
|
|
const overview = AppLatLng(39.5, -98.35);
|
|
|
|
Future<ProviderContainer> openPlanner(
|
|
WidgetTester tester,
|
|
Future<AppLatLng?> location, {
|
|
bool realMap = false,
|
|
bool initiallyFollowing = true,
|
|
}) async {
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [
|
|
currentUserProvider.overrideWith(
|
|
(ref) => const AuthUser(uid: 'rider'),
|
|
),
|
|
routePlannerLocationProvider.overrideWith((ref) => location),
|
|
mapFollowStateProvider.overrideWith(
|
|
(ref) => MapFollowNotifier()..setFollow(initiallyFollowing),
|
|
),
|
|
if (!realMap)
|
|
openStreetMapProviderInstance.overrideWithValue(FakeMapProvider()),
|
|
],
|
|
child: const MaterialApp(home: RoutePlannerScreen()),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
return ProviderScope.containerOf(
|
|
tester.element(find.byType(RoutePlannerScreen)),
|
|
);
|
|
}
|
|
|
|
UnifiedMapView map(WidgetTester tester) =>
|
|
tester.widget(find.byType(UnifiedMapView));
|
|
|
|
for (final size in [const Size(360, 640), const Size(800, 360)]) {
|
|
testWidgets(
|
|
'known rider position centers the planner at $size without adding a stop',
|
|
(tester) async {
|
|
await tester.binding.setSurfaceSize(size);
|
|
addTearDown(() => tester.binding.setSurfaceSize(null));
|
|
final container = await openPlanner(
|
|
tester,
|
|
Future.value(riderPosition),
|
|
);
|
|
|
|
expect(map(tester).center, riderPosition);
|
|
expect(map(tester).zoom, 12);
|
|
expect(container.read(routePlannerNotifierProvider).stops, isEmpty);
|
|
},
|
|
);
|
|
}
|
|
|
|
testWidgets('pending or absent location shows an overview without blocking', (
|
|
tester,
|
|
) async {
|
|
final location = Completer<AppLatLng?>();
|
|
await openPlanner(tester, location.future);
|
|
|
|
expect(find.text('ROUTE PLANNER'), findsOneWidget);
|
|
expect(map(tester).center, overview);
|
|
expect(map(tester).zoom, 3);
|
|
|
|
location.complete(null);
|
|
await tester.pumpAndSettle();
|
|
expect(map(tester).center, overview);
|
|
expect(map(tester).zoom, 3);
|
|
});
|
|
|
|
testWidgets('cached position arriving later updates the overview', (
|
|
tester,
|
|
) async {
|
|
final location = Completer<AppLatLng?>();
|
|
await openPlanner(tester, location.future);
|
|
expect(map(tester).zoom, 3);
|
|
|
|
location.complete(riderPosition);
|
|
await tester.pumpAndSettle();
|
|
expect(map(tester).center, riderPosition);
|
|
expect(map(tester).zoom, 12);
|
|
});
|
|
|
|
testWidgets(
|
|
'previously panned map does not prevent initial cached centering',
|
|
(tester) async {
|
|
final location = Completer<AppLatLng?>();
|
|
await openPlanner(
|
|
tester,
|
|
location.future,
|
|
realMap: true,
|
|
initiallyFollowing: false,
|
|
);
|
|
final controller = tester
|
|
.widget<FlutterMap>(find.byType(FlutterMap))
|
|
.mapController!;
|
|
expect(controller.camera.center.latitude, overview.latitude);
|
|
|
|
location.complete(riderPosition);
|
|
await tester.pumpAndSettle();
|
|
expect(
|
|
controller.camera.center.latitude,
|
|
closeTo(riderPosition.latitude, 0.00001),
|
|
);
|
|
expect(
|
|
controller.camera.center.longitude,
|
|
closeTo(riderPosition.longitude, 0.00001),
|
|
);
|
|
expect(controller.camera.zoom, 12);
|
|
},
|
|
);
|
|
|
|
testWidgets('route stops take priority over a late cached rider position', (
|
|
tester,
|
|
) async {
|
|
final location = Completer<AppLatLng?>();
|
|
final container = await openPlanner(tester, location.future);
|
|
container
|
|
.read(routePlannerNotifierProvider.notifier)
|
|
.addStop(displayName: 'Route start', latitude: 35.6, longitude: -83.5);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(map(tester).center, const AppLatLng(35.6, -83.5));
|
|
expect(map(tester).zoom, 12);
|
|
location.complete(riderPosition);
|
|
await tester.pumpAndSettle();
|
|
expect(map(tester).center, const AppLatLng(35.6, -83.5));
|
|
expect(map(tester).zoom, 12);
|
|
});
|
|
|
|
test(
|
|
'location source only reads cache and tolerates unavailable GPS',
|
|
() async {
|
|
const channel = MethodChannel('flutter.baseflow.com/geolocator');
|
|
final calls = <String>[];
|
|
final messenger =
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
|
|
messenger.setMockMethodCallHandler(channel, (call) async {
|
|
calls.add(call.method);
|
|
throw PlatformException(code: 'PERMISSION_DENIED');
|
|
});
|
|
addTearDown(() => messenger.setMockMethodCallHandler(channel, null));
|
|
final container = ProviderContainer();
|
|
addTearDown(container.dispose);
|
|
|
|
expect(await container.read(routePlannerLocationProvider.future), isNull);
|
|
expect(calls, ['getLastKnownPosition']);
|
|
},
|
|
);
|
|
|
|
for (final position in [riderPosition, null, const AppLatLng(0, 0)]) {
|
|
test(
|
|
'cached location source validates $position without starting GPS',
|
|
() async {
|
|
const channel = MethodChannel('flutter.baseflow.com/geolocator');
|
|
final calls = <String>[];
|
|
final messenger =
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
|
|
messenger.setMockMethodCallHandler(channel, (call) async {
|
|
calls.add(call.method);
|
|
return position == null
|
|
? null
|
|
: {
|
|
'latitude': position.latitude,
|
|
'longitude': position.longitude,
|
|
};
|
|
});
|
|
addTearDown(() => messenger.setMockMethodCallHandler(channel, null));
|
|
final container = ProviderContainer();
|
|
addTearDown(container.dispose);
|
|
|
|
expect(
|
|
await container.read(routePlannerLocationProvider.future),
|
|
position == riderPosition ? riderPosition : null,
|
|
);
|
|
expect(calls, ['getLastKnownPosition']);
|
|
},
|
|
);
|
|
}
|
|
}
|