MotorSignal-App/test/road_alert_phase4_test.dart

291 lines
9.9 KiB
Dart

import 'dart:async';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:motorcycle_dash/services/alerts/road_alert_models.dart';
import 'package:motorcycle_dash/services/alerts/road_alert_compliance.dart';
import 'package:motorcycle_dash/services/alerts/road_alert_providers.dart';
import 'package:motorcycle_dash/services/alerts/road_alert_service.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() async {
SharedPreferences.setMockInitialValues({});
});
group('RoadAlertCompliance Jurisdiction Evaluation Tests', () {
test('correctly classifies United States locations as standard', () {
// Los Angeles, CA
expect(
RoadAlertCompliance.evaluateJurisdiction(34.0522, -118.2437),
equals(AlertComplianceJurisdiction.standard),
);
// New York, NY
expect(
RoadAlertCompliance.evaluateJurisdiction(40.7128, -74.0060),
equals(AlertComplianceJurisdiction.standard),
);
// Deals Gap, NC / TN (Tail of the Dragon)
expect(
RoadAlertCompliance.evaluateJurisdiction(35.4712, -83.9234),
equals(AlertComplianceJurisdiction.standard),
);
});
test('correctly classifies Germany and Switzerland as prohibited', () {
// Berlin, Germany
expect(
RoadAlertCompliance.evaluateJurisdiction(52.5200, 13.4050),
equals(AlertComplianceJurisdiction.prohibited),
);
// Munich, Germany
expect(
RoadAlertCompliance.evaluateJurisdiction(48.1351, 11.5820),
equals(AlertComplianceJurisdiction.prohibited),
);
// Zurich, Switzerland
expect(
RoadAlertCompliance.evaluateJurisdiction(47.3769, 8.5417),
equals(AlertComplianceJurisdiction.prohibited),
);
// Geneva, Switzerland
expect(
RoadAlertCompliance.evaluateJurisdiction(46.2044, 6.1432),
equals(AlertComplianceJurisdiction.prohibited),
);
});
test('correctly classifies France as frenchDangerZone', () {
// Paris, France
expect(
RoadAlertCompliance.evaluateJurisdiction(48.8566, 2.3522),
equals(AlertComplianceJurisdiction.frenchDangerZone),
);
// Lyon, France
expect(
RoadAlertCompliance.evaluateJurisdiction(45.7640, 4.8357),
equals(AlertComplianceJurisdiction.frenchDangerZone),
);
});
});
group('RoadAlertCompliance Alert Transformation Tests', () {
final now = DateTime.now();
final List<RoadAlert> testAlerts = [
RoadAlert(
id: 'cam_1',
providerId: 'here_safety_cameras',
category: RoadAlertCategory.speedCamera,
latitude: 48.8566,
longitude: 2.3522,
description: 'Fixed speed camera 50 km/h',
reportedAt: now,
lastUpdated: now,
source: 'HERE Technologies',
),
RoadAlert(
id: 'police_trap_1',
providerId: 'here_safety_cameras',
category: RoadAlertCategory.police,
latitude: 52.5200,
longitude: 13.4050,
description: 'Mobile Radar Speed Trap',
reportedAt: now,
lastUpdated: now,
source: 'HERE Technologies',
),
RoadAlert(
id: 'hazard_1',
providerId: 'swarm_alerts',
category: RoadAlertCategory.hazard,
latitude: 52.5200,
longitude: 13.4050,
description: 'Gravel on road',
reportedAt: now,
lastUpdated: now,
source: 'MotorSignal Rider Swarm',
),
RoadAlert(
id: 'accident_1',
providerId: 'tomtom',
category: RoadAlertCategory.accident,
latitude: 48.8566,
longitude: 2.3522,
description: 'Multi-vehicle collision',
reportedAt: now,
lastUpdated: now,
source: 'TomTom',
),
];
test('Germany / Switzerland: speed camera and radar trap suppressed; hazards preserved', () {
final filtered = RoadAlertCompliance.applyCompliance(
testAlerts,
userLat: 52.5200,
userLng: 13.4050,
complianceEnabled: true,
);
// Speed camera and radar trap must be filtered out
expect(filtered.any((a) => a.id == 'cam_1'), isFalse);
expect(filtered.any((a) => a.id == 'police_trap_1'), isFalse);
// Hazards and accidents must remain
expect(filtered.any((a) => a.id == 'hazard_1'), isTrue);
expect(filtered.any((a) => a.id == 'accident_1'), isTrue);
});
test('France: speed camera converted into Danger Zone hazard advisory', () {
final filtered = RoadAlertCompliance.applyCompliance(
testAlerts,
userLat: 48.8566,
userLng: 2.3522,
complianceEnabled: true,
);
final camAlert = filtered.firstWhere((a) => a.id == 'cam_1');
expect(camAlert.category, equals(RoadAlertCategory.hazard));
expect(camAlert.description, contains('Zone de Danger'));
});
test('United States: full exact alerts preserved with 100% fidelity', () {
final usAlerts = RoadAlertCompliance.applyCompliance(
testAlerts,
userLat: 34.0522,
userLng: -118.2437,
complianceEnabled: true,
);
expect(usAlerts.length, equals(testAlerts.length));
expect(usAlerts.firstWhere((a) => a.id == 'cam_1').category, equals(RoadAlertCategory.speedCamera));
expect(usAlerts.firstWhere((a) => a.id == 'police_trap_1').category, equals(RoadAlertCategory.police));
});
test('When compliance is disabled, alerts pass through unmodified', () {
final passThrough = RoadAlertCompliance.applyCompliance(
testAlerts,
userLat: 52.5200, // Germany
userLng: 13.4050,
complianceEnabled: false,
);
expect(passThrough.length, equals(testAlerts.length));
expect(passThrough.any((a) => a.id == 'cam_1'), isTrue);
});
});
group('Phase 4 Riverpod State Management Tests', () {
test('roadAlertAudioModeProvider defaults to voice and switches to tacticalTone', () async {
final container = ProviderContainer();
addTearDown(container.dispose);
expect(container.read(roadAlertAudioModeProvider), equals(RoadAlertAudioMode.voice));
await container
.read(roadAlertAudioModeProvider.notifier)
.setMode(RoadAlertAudioMode.tacticalTone);
expect(container.read(roadAlertAudioModeProvider), equals(RoadAlertAudioMode.tacticalTone));
});
test('leanInterlockEnabledProvider defaults to true and toggles properly', () async {
final container = ProviderContainer();
addTearDown(container.dispose);
expect(container.read(leanInterlockEnabledProvider), isTrue);
await container.read(leanInterlockEnabledProvider.notifier).setEnabled(false);
expect(container.read(leanInterlockEnabledProvider), isFalse);
});
test('jurisdictionComplianceEnabledProvider defaults to true and toggles properly', () async {
final container = ProviderContainer();
addTearDown(container.dispose);
expect(container.read(jurisdictionComplianceEnabledProvider), isTrue);
await container.read(jurisdictionComplianceEnabledProvider.notifier).setEnabled(false);
expect(container.read(jurisdictionComplianceEnabledProvider), isFalse);
});
});
group('Lean-Angle Cornering Safety Interlock Tests', () {
test('RoadAlertService tracks lean angle and cornering state correctly', () async {
final leanController = StreamController<double>.broadcast();
addTearDown(leanController.close);
final container = ProviderContainer(
overrides: [
leanStreamOverrideProvider.overrideWithValue(leanController.stream),
],
);
addTearDown(container.dispose);
final service = container.read(roadAlertServiceProvider);
// Initial lean is 0 (upright)
expect(service.currentLeanAngle, equals(0.0));
expect(service.isCornering, isFalse);
// Lean into right turn: 22 degrees
leanController.add(22.0);
await Future.delayed(const Duration(milliseconds: 50));
expect(service.currentLeanAngle, equals(22.0));
expect(service.isCornering, isTrue);
// Return to upright: 3 degrees
leanController.add(3.0);
await Future.delayed(const Duration(milliseconds: 50));
// Just returned, but under 1.5s recovery gate: still marked cornering
expect(service.currentLeanAngle, equals(3.0));
expect(service.isCornering, isTrue);
// Wait 1.6 seconds for upright recovery
await Future.delayed(const Duration(milliseconds: 1600));
expect(service.isCornering, isFalse);
});
test('Alert audio is deferred when bike is cornering at 25 degrees', () async {
final leanController = StreamController<double>.broadcast();
addTearDown(leanController.close);
final container = ProviderContainer(
overrides: [
leanStreamOverrideProvider.overrideWithValue(leanController.stream),
],
);
addTearDown(container.dispose);
final service = container.read(roadAlertServiceProvider);
// Set bike cornering
leanController.add(25.0);
await Future.delayed(const Duration(milliseconds: 50));
expect(service.isCornering, isTrue);
final now = DateTime.now();
// Inject alert 200m ahead
final alert = RoadAlert(
id: 'apex_trap_1',
providerId: 'here_safety_cameras',
category: RoadAlertCategory.police,
latitude: 34.0522,
longitude: -118.2437,
description: 'Speed enforcement trap',
reportedAt: now,
lastUpdated: now,
distanceFromUser: 200.0,
source: 'HERE Technologies',
);
service.simulateAlert(alert);
await Future.delayed(const Duration(milliseconds: 50));
// Alert should be in cached alerts
expect(service.alertsStream, emitsThrough(predicate<List<RoadAlert>>((l) => l.isNotEmpty)));
});
});
}