283 lines
9.4 KiB
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']);
|
|
});
|
|
});
|
|
}
|