MotorSignal-App/test/destination_autocomplete_re...

286 lines
9.7 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/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<List<PlaceSearchResult>> 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<ResolvedPlace> 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<RouteCalculationResult> calculateRoute(
List<RouteStop> 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<Card>(find.byKey(const ValueKey('search_results_overlay')));
expect(overlayCard.color, isNotNull);
// Verify title text is rendered and readable
final titleText = tester.widget<Text>(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<Text>(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<Positioned>(
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);
});
});
}