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 markers, required List polylines, required bool isFollowMode, required MapConfig config, required ValueChanged? onPositionChanged, required VoidCallback? onUserInteraction, }) => const SizedBox.expand(); } void main() { const riderPosition = AppLatLng(33.749, -84.388); const overview = AppLatLng(39.5, -98.35); Future openPlanner( WidgetTester tester, Future 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(); 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(); 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(); await openPlanner( tester, location.future, realMap: true, initiallyFollowing: false, ); final controller = tester .widget(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(); 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 = []; 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 = []; 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']); }, ); } }