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/map/widgets/tactical_layers_bottom_sheet.dart'; import 'package:motorcycle_dash/services/map/map_layer_settings.dart'; import 'package:motorcycle_dash/services/map/map_provider_interface.dart'; import 'package:motorcycle_dash/services/location_service.dart'; import 'package:motorcycle_dash/services/alerts/road_alert_service.dart'; Widget _buildTestApp({ required Widget child, List overrides = const [], }) { return ProviderScope( overrides: overrides, child: MaterialApp( theme: ThemeData.dark().copyWith( extensions: const [MotorSignalThemeColors.dark], ), home: child, ), ); } Position _createPosition({ double latitude = 37.7749, double longitude = -122.4194, double speed = 15.0, double heading = 90.0, }) { return Position( latitude: latitude, longitude: longitude, timestamp: DateTime.now(), accuracy: 5.0, altitude: 50.0, altitudeAccuracy: 1.0, heading: heading, headingAccuracy: 1.0, speed: speed, speedAccuracy: 0.5, ); } void main() { TestWidgetsFlutterBinding.ensureInitialized(); setUp(() { SharedPreferences.setMockInitialValues({}); }); group('MapLayerSettings & Notifier Unit Tests', () { test('Default MapLayerSettings values match Stitch MAP-02 spec', () { const settings = MapLayerSettings(); expect(settings.style, equals(TacticalMapStyle.tacticalDark)); expect(settings.showPoliceTraps, isTrue); expect(settings.showPackBlips, isTrue); expect(settings.showTwistyHighlights, isTrue); expect(settings.offlineOnly, isFalse); }); test('MapLayerSettings equality and copyWith', () { const s1 = MapLayerSettings(); final s2 = s1.copyWith( style: TacticalMapStyle.satellite4K, offlineOnly: true, ); expect(s2.style, equals(TacticalMapStyle.satellite4K)); expect(s2.offlineOnly, isTrue); expect(s2.showPoliceTraps, isTrue); expect(s1 == s2, isFalse); final s3 = s2.copyWith( style: TacticalMapStyle.tacticalDark, offlineOnly: false, ); expect(s1 == s3, isTrue); expect(s1.hashCode == s3.hashCode, isTrue); }); test('MapConfig named constructors produce valid configurations', () { final darkConfig = MapConfig.tacticalDark(); expect(darkConfig.tileUrlTemplate, contains('openstreetmap.org')); expect(darkConfig.isDarkMode, isTrue); expect(darkConfig.offlineOnly, isFalse); final satConfig = MapConfig.satellite4K(); expect(satConfig.tileUrlTemplate, contains('arcgisonline.com')); expect(satConfig.attributionText, contains('Esri')); final topoConfig = MapConfig.elevation3D(); expect(topoConfig.tileUrlTemplate, contains('opentopomap.org')); expect(topoConfig.attributionText, contains('OpenTopoMap')); }); test('MapLayerSettingsNotifier toggles and resets properly', () async { final container = ProviderContainer(); addTearDown(container.dispose); final notifier = container.read(mapLayerSettingsProvider.notifier); expect(container.read(mapLayerSettingsProvider).style, TacticalMapStyle.tacticalDark); await notifier.setStyle(TacticalMapStyle.satellite4K); expect(container.read(mapLayerSettingsProvider).style, TacticalMapStyle.satellite4K); await notifier.togglePoliceTraps(false); expect(container.read(mapLayerSettingsProvider).showPoliceTraps, isFalse); await notifier.togglePackBlips(false); expect(container.read(mapLayerSettingsProvider).showPackBlips, isFalse); await notifier.toggleTwistyHighlights(false); expect(container.read(mapLayerSettingsProvider).showTwistyHighlights, isFalse); await notifier.toggleOfflineOnly(true); expect(container.read(mapLayerSettingsProvider).offlineOnly, isTrue); await notifier.resetToDefaults(); final resetState = container.read(mapLayerSettingsProvider); expect(resetState.style, TacticalMapStyle.tacticalDark); expect(resetState.showPoliceTraps, isTrue); expect(resetState.showPackBlips, isTrue); expect(resetState.showTwistyHighlights, isTrue); expect(resetState.offlineOnly, isFalse); }); }); group('TacticalLayersBottomSheet Widget Tests (MAP-02)', () { testWidgets('Renders bottom sheet header, MAP-02 badge, styles and switches', (tester) async { await tester.binding.setSurfaceSize(const Size(1080, 2400)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget(_buildTestApp( child: Builder( builder: (context) => Scaffold( body: Center( child: ElevatedButton( onPressed: () => TacticalLayersBottomSheet.show(context), child: const Text('Open Drawer'), ), ), ), ), )); await tester.tap(find.text('Open Drawer')); await tester.pumpAndSettle(); expect(find.text('TACTICAL MAP LAYERS'), findsOneWidget); expect(find.text('MAP-02'), findsOneWidget); expect(find.byKey(const ValueKey('style_card_tactical_dark')), findsOneWidget); expect(find.byKey(const ValueKey('style_card_satellite_4k')), findsOneWidget); expect(find.byKey(const ValueKey('style_card_elevation_3d')), findsOneWidget); expect(find.byKey(const ValueKey('switch_police_traps')), findsOneWidget); expect(find.byKey(const ValueKey('switch_pack_blips')), findsOneWidget); expect(find.byKey(const ValueKey('switch_twisty_highlights')), findsOneWidget); expect(find.byKey(const ValueKey('switch_offline_only')), findsOneWidget); expect(find.byKey(const ValueKey('reset_layers_button')), findsOneWidget); expect(find.byKey(const ValueKey('done_layers_button')), findsOneWidget); }); testWidgets('Tapping style cards switches style in provider', (tester) async { await tester.binding.setSurfaceSize(const Size(1080, 2400)); addTearDown(() => tester.binding.setSurfaceSize(null)); final container = ProviderContainer(); addTearDown(container.dispose); await tester.pumpWidget( UncontrolledProviderScope( container: container, child: MaterialApp( theme: ThemeData.dark().copyWith( extensions: const [MotorSignalThemeColors.dark], ), home: Builder( builder: (context) => Scaffold( body: ElevatedButton( onPressed: () => TacticalLayersBottomSheet.show(context), child: const Text('Open Drawer'), ), ), ), ), ), ); await tester.tap(find.text('Open Drawer')); await tester.pumpAndSettle(); expect(container.read(mapLayerSettingsProvider).style, TacticalMapStyle.tacticalDark); // Tap Satellite 4K await tester.tap(find.byKey(const ValueKey('style_card_satellite_4k'))); await tester.pumpAndSettle(); expect(container.read(mapLayerSettingsProvider).style, TacticalMapStyle.satellite4K); // Tap 3D Topo await tester.tap(find.byKey(const ValueKey('style_card_elevation_3d'))); await tester.pumpAndSettle(); expect(container.read(mapLayerSettingsProvider).style, TacticalMapStyle.elevation3D); }); testWidgets('Toggling overlay switches modifies settings', (tester) async { await tester.binding.setSurfaceSize(const Size(1080, 2400)); addTearDown(() => tester.binding.setSurfaceSize(null)); final container = ProviderContainer(); addTearDown(container.dispose); await tester.pumpWidget( UncontrolledProviderScope( container: container, child: MaterialApp( theme: ThemeData.dark().copyWith( extensions: const [MotorSignalThemeColors.dark], ), home: Builder( builder: (context) => Scaffold( body: ElevatedButton( onPressed: () => TacticalLayersBottomSheet.show(context), child: const Text('Open Drawer'), ), ), ), ), ), ); await tester.tap(find.text('Open Drawer')); await tester.pumpAndSettle(); // Toggle offline only switch final offlineSwitch = find.descendant( of: find.byKey(const ValueKey('switch_offline_only')), matching: find.byType(Switch), ); expect(offlineSwitch, findsOneWidget); await tester.ensureVisible(offlineSwitch); await tester.pumpAndSettle(); await tester.tap(offlineSwitch); await tester.pumpAndSettle(); expect(container.read(mapLayerSettingsProvider).offlineOnly, isTrue); // Tap Reset button await tester.ensureVisible(find.byKey(const ValueKey('reset_layers_button'))); await tester.pumpAndSettle(); await tester.tap(find.byKey(const ValueKey('reset_layers_button'))); await tester.pumpAndSettle(); expect(container.read(mapLayerSettingsProvider).offlineOnly, isFalse); // Tap Apply & Close await tester.ensureVisible(find.byKey(const ValueKey('done_layers_button'))); await tester.pumpAndSettle(); await tester.tap(find.byKey(const ValueKey('done_layers_button'))); await tester.pumpAndSettle(); expect(find.byKey(const ValueKey('tactical_layers_bottom_sheet')), findsNothing); }); }); group('MapScreen HUD & Tactical Controls Tests', () { testWidgets('Renders right HUD controls and top search card', (tester) async { await tester.binding.setSurfaceSize(const Size(1080, 2400)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget(_buildTestApp( child: const MapScreen(), overrides: [ locationStreamProvider.overrideWith((ref) => Stream.value(_createPosition())), activeAlertsStreamProvider.overrideWith((ref) => Stream.value([])), ], )); await tester.pumpAndSettle(); await tester.pump(const Duration(seconds: 4)); expect(find.byKey(const ValueKey('tactical_layers_button')), findsOneWidget); expect(find.byKey(const ValueKey('compass_orient_button')), findsOneWidget); expect(find.byKey(const ValueKey('zoom_in_button')), findsOneWidget); expect(find.byKey(const ValueKey('zoom_out_button')), findsOneWidget); expect(find.byKey(const ValueKey('quick_swarm_report_button')), findsOneWidget); // Floating search card expect(find.text('Search destinations, twisty passes...'), findsOneWidget); expect(find.byKey(const ValueKey('chip_gas_and_fuel')), findsOneWidget); expect(find.byKey(const ValueKey('chip_twisty_passes')), findsOneWidget); expect(find.byKey(const ValueKey('chip_scenic_rest')), findsOneWidget); expect(find.byKey(const ValueKey('chip_repair_moto')), findsOneWidget); }); testWidgets('Tapping search quick chips selects chip and updates query', (tester) async { await tester.binding.setSurfaceSize(const Size(1080, 2400)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget(_buildTestApp( child: const MapScreen(), overrides: [ locationStreamProvider.overrideWith((ref) => Stream.value(_createPosition())), activeAlertsStreamProvider.overrideWith((ref) => Stream.value([])), ], )); await tester.pumpAndSettle(); await tester.pump(const Duration(seconds: 4)); // Tap Twisty Passes chip await tester.tap(find.byKey(const ValueKey('chip_twisty_passes'))); await tester.pumpAndSettle(); expect(find.text('Twisty Passes'), findsWidgets); expect(find.byKey(const ValueKey('clear_search_button')), findsOneWidget); // Clear search await tester.tap(find.byKey(const ValueKey('clear_search_button'))); await tester.pumpAndSettle(); expect(find.byKey(const ValueKey('clear_search_button')), findsNothing); }); testWidgets('Tapping zoom and compass orient HUD buttons', (tester) async { await tester.binding.setSurfaceSize(const Size(1080, 2400)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget(_buildTestApp( child: const MapScreen(), overrides: [ locationStreamProvider.overrideWith((ref) => Stream.value(_createPosition())), activeAlertsStreamProvider.overrideWith((ref) => Stream.value([])), ], )); await tester.pumpAndSettle(); await tester.pump(const Duration(seconds: 4)); // Tap Zoom In and Zoom Out await tester.tap(find.byKey(const ValueKey('zoom_in_button'))); await tester.pumpAndSettle(); await tester.tap(find.byKey(const ValueKey('zoom_out_button'))); await tester.pumpAndSettle(); // Tap Compass Orient button await tester.tap(find.byKey(const ValueKey('compass_orient_button'))); await tester.pumpAndSettle(); // Recenter button becomes visible once user interacted expect(find.byKey(const ValueKey('recenter_rider_button')), findsOneWidget); await tester.tap(find.byKey(const ValueKey('recenter_rider_button'))); await tester.pumpAndSettle(); expect(find.byKey(const ValueKey('recenter_rider_button')), findsNothing); }); testWidgets('Offline Cached Only banner renders when offlineOnly is true', (tester) async { SharedPreferences.setMockInitialValues({'tactical_offline_only': true}); await tester.binding.setSurfaceSize(const Size(1080, 2400)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget(_buildTestApp( child: const MapScreen(), overrides: [ locationStreamProvider.overrideWith((ref) => Stream.value(_createPosition(speed: 0.0))), activeAlertsStreamProvider.overrideWith((ref) => Stream.value([])), ], )); await tester.pumpAndSettle(); await tester.pump(const Duration(seconds: 4)); expect(find.text('OFFLINE TILES ONLY'), findsOneWidget); }); }); }