MotorSignal-App/test/road_alert_test.dart

283 lines
9.4 KiB
Dart

import 'package:flutter/services.dart';
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/tomtom_road_alert_provider.dart';
import 'package:motorcycle_dash/services/alerts/local_road_alert_provider.dart';
import 'package:motorcycle_dash/services/alerts/road_alert_providers.dart';
import 'package:motorcycle_dash/services/alerts/road_alert_service.dart';
import 'package:motorcycle_dash/services/field_test/field_test_logger.dart';
import 'package:motorcycle_dash/models/field_test_event.dart';
class _NoOpFieldTestLogger extends Fake implements FieldTestLogger {
@override
void log({
required FieldTestEventType eventType,
required EventSeverity severity,
required String message,
int? rideId,
DateTime? timestamp,
DateTime? telemetryTimestamp,
int? gpsAgeMs,
double? speed,
double? latitude,
double? longitude,
double? gpsAccuracy,
double? leanAngle,
double? longitudinalG,
double? lateralG,
double? overallConfidence,
Map<String, dynamic>? metadata,
}) {}
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
const flutterTtsChannel = MethodChannel('flutter_tts');
setUpAll(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(flutterTtsChannel, (call) async {
switch (call.method) {
case 'setLanguage':
case 'setSpeechRate':
case 'setVolume':
case 'speak':
case 'stop':
return 1;
default:
return null;
}
});
});
tearDownAll(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(flutterTtsChannel, null);
});
group('Road Alert System Tests', () {
test('RoadAlertCategory display names and colors', () {
expect(RoadAlertCategory.police.displayName, equals('Police'));
expect(RoadAlertCategory.accident.displayName, equals('Accident'));
expect(RoadAlertCategory.hazard.displayName, equals('Hazard'));
expect(RoadAlertCategory.police.color, isNotNull);
expect(RoadAlertCategory.accident.icon, isNotNull);
});
test('TomTomRoadAlertProvider capability advertising', () {
final provider = TomTomRoadAlertProvider();
expect(provider.id, equals('tomtom'));
expect(provider.name, equals('TomTom Traffic'));
// Verify TomTom explicitly advertises its supported categories (No police/speed-traps)
expect(
provider.supportedCategories,
contains(RoadAlertCategory.accident),
);
expect(provider.supportedCategories, contains(RoadAlertCategory.hazard));
expect(
provider.supportedCategories,
contains(RoadAlertCategory.construction),
);
expect(
provider.supportedCategories,
contains(RoadAlertCategory.roadClosure),
);
expect(provider.supportedCategories, contains(RoadAlertCategory.traffic));
expect(
provider.supportedCategories,
isNot(contains(RoadAlertCategory.police)),
);
expect(
provider.supportedCategories,
isNot(contains(RoadAlertCategory.speedCamera)),
);
});
test('LocalRoadAlertProvider returns demo alerts', () async {
final localProvider = LocalRoadAlertProvider();
final alerts = await localProvider.getAlerts(
latitude: 37.7749,
longitude: -122.4194,
radiusMeters: 1000.0,
);
expect(alerts.isNotEmpty, isTrue);
expect(alerts.any((a) => a.category == RoadAlertCategory.hazard), isTrue);
expect(
alerts.any((a) => a.category == RoadAlertCategory.construction),
isTrue,
);
});
test('Road alert providers & setting defaults', () {
SharedPreferences.setMockInitialValues({});
final container = ProviderContainer();
addTearDown(container.dispose);
expect(container.read(roadAlertsEnabledProvider), isTrue);
expect(container.read(audioAlertsEnabledProvider), isTrue);
expect(container.read(alertRadiusMetersProvider), equals(804.7));
expect(container.read(tomtomApiKeyProvider), isEmpty);
expect(
container.read(tomtomStatusProvider),
equals(RoadAlertProviderStatus.notConfigured),
);
});
test(
'RoadAlertService dismissAlert and clearAllAlerts function correctly',
() async {
SharedPreferences.setMockInitialValues({});
final container = ProviderContainer(
overrides: [
fieldTestLoggerProvider.overrideWithValue(_NoOpFieldTestLogger()),
],
);
addTearDown(container.dispose);
final service = container.read(roadAlertServiceProvider);
final future1 = service.alertsStream.skip(1).first;
service.triggerTestAlert(category: RoadAlertCategory.police);
final alerts1 = await future1;
expect(alerts1.isNotEmpty, isTrue);
final alertId = alerts1.first.id;
final future2 = service.alertsStream.skip(1).first;
service.dismissAlert(alertId);
final alerts2 = await future2;
expect(alerts2.any((a) => a.id == alertId), isFalse);
final future3 = service.alertsStream.skip(1).first;
service.triggerTestAlert(category: RoadAlertCategory.hazard);
final alerts3 = await future3;
expect(alerts3.isNotEmpty, isTrue);
final future4 = service.alertsStream.skip(1).first;
service.clearAllAlerts();
final alerts4 = await future4;
expect(alerts4.isEmpty, isTrue);
},
);
test('diagnostic alert bypasses disabled production alerts', () async {
SharedPreferences.setMockInitialValues({'road_alerts_enabled': false});
final container = ProviderContainer(
overrides: [
fieldTestLoggerProvider.overrideWithValue(_NoOpFieldTestLogger()),
],
);
addTearDown(container.dispose);
await container
.read(roadAlertsEnabledProvider.notifier)
.setEnabled(false);
expect(container.read(roadAlertsEnabledProvider), isFalse);
final service = container.read(roadAlertServiceProvider);
service.simulateAlert(
RoadAlert(
id: 'production_simulation',
providerId: 'mock',
category: RoadAlertCategory.police,
latitude: 0,
longitude: 0,
description: 'Production simulation',
reportedAt: DateTime.now(),
lastUpdated: DateTime.now(),
source: 'test',
),
);
expect(await service.alertsStream.first, isEmpty);
service.triggerTestAlert(category: RoadAlertCategory.police);
expect(await service.alertsStream.first, isNotEmpty);
});
});
group('Road alert replay semantics', () {
ProviderContainer createContainer() {
SharedPreferences.setMockInitialValues({});
final container = ProviderContainer(
overrides: [
fieldTestLoggerProvider.overrideWithValue(_NoOpFieldTestLogger()),
],
);
addTearDown(container.dispose);
return container;
}
RoadAlert alert(String id, {DateTime? expiresAt}) {
final now = DateTime.now();
return RoadAlert(
id: id,
providerId: 'test',
category: RoadAlertCategory.hazard,
latitude: 34,
longitude: -85,
description: 'Test hazard',
reportedAt: now,
expiresAt: expiresAt ?? now.add(const Duration(minutes: 5)),
lastUpdated: now,
source: 'test',
);
}
test('late subscriber receives the active alert', () async {
final service = createContainer().read(roadAlertServiceProvider);
service.simulateAlert(alert('active'));
final replayed = await service.alertsStream.first;
expect(replayed.map((item) => item.id), ['active']);
});
test('dismissed alert is not replayed or re-added', () async {
final service = createContainer().read(roadAlertServiceProvider);
final dismissed = alert('dismissed');
service.simulateAlert(dismissed);
service.dismissAlert(dismissed.id);
service.simulateAlert(dismissed);
expect(await service.alertsStream.first, isEmpty);
});
test('cleared alerts replay an empty state', () async {
final service = createContainer().read(roadAlertServiceProvider);
service.simulateAlert(alert('cleared'));
service.clearAllAlerts();
expect(await service.alertsStream.first, isEmpty);
});
test('expired alert does not reappear', () async {
final service = createContainer().read(roadAlertServiceProvider);
service.simulateAlert(
alert(
'expired',
expiresAt: DateTime.now().subtract(const Duration(seconds: 1)),
),
);
expect(await service.alertsStream.first, isEmpty);
});
test('subscriber remount does not duplicate an active alert', () async {
final service = createContainer().read(roadAlertServiceProvider);
service.simulateAlert(alert('stable'));
final firstMount = await service.alertsStream.first;
final secondMount = await service.alertsStream.first;
expect(firstMount.map((item) => item.id), ['stable']);
expect(secondMount.map((item) => item.id), ['stable']);
});
});
}