MotorSignal-App/test/tactical_map_layers_test.dart

387 lines
14 KiB
Dart

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<Override> 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);
});
});
}