286 lines
9.7 KiB
Dart
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);
|
|
});
|
|
});
|
|
}
|