387 lines
14 KiB
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);
|
|
});
|
|
});
|
|
}
|