import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:geolocator/geolocator.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:motorcycle_dash/core/theme/theme_extensions.dart'; import 'package:motorcycle_dash/features/map/map_screen.dart'; import 'package:motorcycle_dash/features/dashboard/widgets/dashboard_alert_region.dart'; import 'package:motorcycle_dash/services/alerts/road_alert_models.dart'; import 'package:motorcycle_dash/services/alerts/road_alert_service.dart'; import 'package:motorcycle_dash/services/location_service.dart'; import 'package:motorcycle_dash/services/map/map_provider_interface.dart'; import 'package:motorcycle_dash/services/routes/route_models.dart'; import 'package:motorcycle_dash/services/routes/route_provider.dart'; import 'package:motorcycle_dash/services/routes/route_providers.dart'; class _FakeRouteProvider implements RouteProvider { final RoutingPreference preference; final bool avoidHighways; _FakeRouteProvider({ this.preference = RoutingPreference.fastest, this.avoidHighways = false, }); @override String get name => 'Fake Provider'; @override Future> searchPlaces( String query, { AppLatLng? nearLocation, }) async { return const [ PlaceSearchResult( providerId: 'blood_mtn', primaryText: 'Blood Mountain Gap', secondaryText: 'US-19, Blairsville, GA', latitude: 34.7330, longitude: -83.9180, ), ]; } @override Future resolvePlace(String placeId) async { return const ResolvedPlace( providerId: 'blood_mtn', displayName: 'Blood Mountain Gap', formattedAddress: 'Blood Mountain Gap, GA', latitude: 34.7330, longitude: -83.9180, ); } @override Future calculateRoute( List stops, { bool avoidHighways = false, bool avoidTolls = false, bool avoidFerries = false, RoutingOptions? options, }) async { return RouteCalculationResult( straightLineDistanceMeters: 10000.0, estimatedDistanceMeters: 12000.0, estimatedDurationSeconds: 900, geometry: [ AppLatLng(stops.first.latitude, stops.first.longitude), AppLatLng(stops.last.latitude, stops.last.longitude), ], isStraightLine: false, providerName: name, options: options ?? RoutingOptions( preference: preference, avoidHighways: avoidHighways, ), curvatureMetric: 85.0, // Twisty curves ); } } void main() { TestWidgetsFlutterBinding.ensureInitialized(); setUp(() { SharedPreferences.setMockInitialValues({}); }); Widget buildSubject({ ThemeMode themeMode = ThemeMode.light, RoutingPreference preference = RoutingPreference.fastest, bool avoidHighways = false, }) { return ProviderScope( overrides: [ activeRouteCalculationProvider.overrideWithValue( _FakeRouteProvider( preference: preference, avoidHighways: avoidHighways, ), ), locationStreamProvider.overrideWith( (ref) => Stream.value( Position( latitude: 34.7200, longitude: -83.9100, timestamp: DateTime.now(), accuracy: 5.0, altitude: 300.0, altitudeAccuracy: 1.0, heading: 45.0, headingAccuracy: 1.0, speed: 15.0, speedAccuracy: 0.5, ), ), ), ], child: MaterialApp( themeMode: themeMode, theme: ThemeData.light().copyWith( extensions: const [MotorSignalThemeColors.light], ), darkTheme: ThemeData.dark().copyWith( extensions: const [MotorSignalThemeColors.dark], ), home: const MapScreen(), ), ); } group('Destination Autocomplete & Map Screen Remediations', () { testWidgets('Search results overlay uses high contrast theme tokens in light mode', (tester) async { await tester.binding.setSurfaceSize(const Size(412, 915)); await tester.pumpWidget(buildSubject(themeMode: ThemeMode.light)); await tester.pumpAndSettle(); final searchField = find.byType(TextField); await tester.enterText(searchField, 'Blood Mountain'); await tester.pump(const Duration(milliseconds: 400)); await tester.pumpAndSettle(); final overlayCard = tester.widget(find.byKey(const ValueKey('search_results_overlay'))); expect(overlayCard.color, isNotNull); // Verify title text is rendered and readable final titleText = tester.widget(find.text('Blood Mountain Gap')); expect(titleText.style?.color, isNotNull); // In light mode, text color should not be hardcoded white expect(titleText.style?.color != Colors.white, isTrue); final subtitleText = tester.widget(find.text('US-19, Blairsville, GA')); expect(subtitleText.style?.color != Colors.white70, isTrue); // Report button is hidden while search results are showing expect(find.byKey(const ValueKey('quick_swarm_report_button')), findsNothing); }); testWidgets('Route preview displays confirmed mode badge, labeled curves, and repositioned report FAB', (tester) async { await tester.binding.setSurfaceSize(const Size(412, 915)); await tester.pumpWidget(buildSubject( themeMode: ThemeMode.dark, preference: RoutingPreference.fastest, avoidHighways: true, )); await tester.pumpAndSettle(); // Enter search and select destination final searchField = find.byType(TextField); await tester.enterText(searchField, 'Blood Mountain'); await tester.pump(const Duration(milliseconds: 400)); await tester.pumpAndSettle(); await tester.tap(find.text('Blood Mountain Gap')); await tester.pumpAndSettle(); // Verify preview card rendered expect(find.byKey(const ValueKey('route_preview_card')), findsOneWidget); // Confirmed mode badge displays FASTEST expect(find.text('FASTEST'), findsOneWidget); // Twistiness is clearly labeled as CURVES: Twisty Curves expect(find.text('CURVES: Twisty Curves'), findsOneWidget); // Scroll preference chips row horizontally to reveal Avoid Highways chip final chipFinder = find.byKey(const ValueKey('preference_avoid_highways')); await tester.scrollUntilVisible( chipFinder, 150.0, scrollable: find.byType(Scrollable).last, ); await tester.pumpAndSettle(); await tester.tap(chipFinder); await tester.pumpAndSettle(); // Avoid highways badge is displayed when enabled expect(find.text('NO HIGHWAYS'), findsOneWidget); // Report FAB is visible and repositioned safely above route preview card (bottom: 295) final reportFabPositioned = tester.widget( find.ancestor( of: find.byKey(const ValueKey('quick_swarm_report_button')), matching: find.byType(Positioned), ).first, ); expect(reportFabPositioned.bottom, 295.0); }); testWidgets('Dashboard alert distance formatting avoids 1 ft Ahead and uses Nearby for close alerts', (tester) async { final closeAlert = RoadAlert( id: 'alert_close', providerId: 'swarm_alerts', category: RoadAlertCategory.hazard, latitude: 34.7200, longitude: -83.9100, description: 'Road gravel', reportedAt: DateTime.now(), lastUpdated: DateTime.now(), source: 'swarm', distanceFromUser: 5.0, // 5 meters ); await tester.pumpWidget( MaterialApp( theme: ThemeData.light().copyWith( extensions: const [MotorSignalThemeColors.light], ), home: Scaffold( body: ProviderScope( overrides: [ locationPermissionGrantedProvider.overrideWith((ref) => true), activeAlertsStreamProvider.overrideWith((ref) => Stream.value([closeAlert])), ], child: const DashboardAlertRegion(), ), ), ), ); await tester.pumpAndSettle(); // Close alert formats as "Nearby" expect(find.textContaining('Nearby'), findsOneWidget); expect(find.textContaining('1 ft Ahead'), findsNothing); }); testWidgets('Dashboard alert distance formatting rounds mid-distance to clean 10ft units', (tester) async { final midAlert = RoadAlert( id: 'alert_mid', providerId: 'swarm_alerts', category: RoadAlertCategory.police, latitude: 34.7200, longitude: -83.9100, description: 'Speed trap', reportedAt: DateTime.now(), lastUpdated: DateTime.now(), source: 'swarm', distanceFromUser: 45.0, // 45 meters (~150 ft) ); await tester.pumpWidget( MaterialApp( theme: ThemeData.light().copyWith( extensions: const [MotorSignalThemeColors.light], ), home: Scaffold( body: ProviderScope( overrides: [ locationPermissionGrantedProvider.overrideWith((ref) => true), activeAlertsStreamProvider.overrideWith((ref) => Stream.value([midAlert])), ], child: const DashboardAlertRegion(), ), ), ), ); await tester.pumpAndSettle(); // Mid alert formats in clean rounded feet increments (e.g. 150 ft Ahead) expect(find.textContaining('150 ft Ahead'), findsOneWidget); expect(find.textContaining('1 ft Ahead'), findsNothing); }); }); }