Notrufnummern-Datenbasis, Ländererkennung, State-Management und Lokalisierung (AP1-AP7)
Implementiert den kompletten Kern-Flow der Emergency App gemäß docs/plan.md: - Notrufnummern-Datenbasis für 199 Länder + Modelle/Repository (AP1) - DialerService für tel:-Notrufe ohne Auto-Dial (AP2) - Statisches UI mit Panik-Button-Fallback (AP3) - Priorisierte Ländererkennung: SIM/Netz -> GPS/Geocoding -> Cache (AP4) - EmergencyProvider verdrahtet Repository/LocationService mit der UI (AP5) - Manuelle Länderauswahl als Fallback samt eigener Priorität (AP6) - UI-Lokalisierung (de/en/fr/es), an erkanntes Land gekoppelt (AP7) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:emergency/services/country_code_sources/cached_country_code_source.dart';
|
||||
|
||||
/// Im Gegensatz zu den SIM-/GPS-Quellen ist `shared_preferences` über
|
||||
/// `SharedPreferences.setMockInitialValues` vollständig ohne echten
|
||||
/// Plattform-Kanal testbar – daher hier ein echter Verhaltenstest statt nur
|
||||
/// eines Struktur-Tests.
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('CachedCountryCodeSource', () {
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
});
|
||||
|
||||
test('resolve() liefert null, wenn noch kein Code gespeichert ist', () async {
|
||||
const source = CachedCountryCodeSource();
|
||||
|
||||
expect(await source.resolve(), isNull);
|
||||
});
|
||||
|
||||
test('save() gefolgt von resolve() liefert den gespeicherten, normalisierten Code', () async {
|
||||
const source = CachedCountryCodeSource();
|
||||
|
||||
await source.save('de');
|
||||
|
||||
expect(await source.resolve(), 'DE');
|
||||
});
|
||||
|
||||
test('resolve() liefert null, wenn der gespeicherte Wert leer ist', () async {
|
||||
SharedPreferences.setMockInitialValues({'last_known_country_code': ' '});
|
||||
const source = CachedCountryCodeSource();
|
||||
|
||||
expect(await source.resolve(), isNull);
|
||||
});
|
||||
|
||||
test('save() überschreibt einen zuvor gespeicherten Code', () async {
|
||||
const source = CachedCountryCodeSource();
|
||||
|
||||
await source.save('DE');
|
||||
await source.save('fr');
|
||||
|
||||
expect(await source.resolve(), 'FR');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
import 'package:flutter/widgets.dart' show Locale;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:geocoding_platform_interface/geocoding_platform_interface.dart' as pi;
|
||||
import 'package:geolocator_platform_interface/geolocator_platform_interface.dart';
|
||||
|
||||
import 'package:emergency/services/country_code_sources/gps_country_code_source.dart';
|
||||
|
||||
/// Analog zum `url_launcher`-Test von `DialerService`: `geolocator` und
|
||||
/// `geocoding` erlauben es, ihre `*Platform.instance` durch eine eigene
|
||||
/// Implementierung zu ersetzen – der offiziell vorgesehene Weg, diese
|
||||
/// Packages ohne echten Plattform-Kanal zu testen.
|
||||
class _FakeGeolocatorPlatform extends GeolocatorPlatform {
|
||||
_FakeGeolocatorPlatform({
|
||||
required this.permission,
|
||||
this.position,
|
||||
this.currentPositionError,
|
||||
});
|
||||
|
||||
LocationPermission permission;
|
||||
Position? position;
|
||||
Object? currentPositionError;
|
||||
|
||||
@override
|
||||
Future<LocationPermission> checkPermission() async => permission;
|
||||
|
||||
@override
|
||||
Future<LocationPermission> requestPermission() async => permission;
|
||||
|
||||
@override
|
||||
Future<Position> getCurrentPosition({LocationSettings? locationSettings}) async {
|
||||
if (currentPositionError != null) throw currentPositionError!;
|
||||
return position!;
|
||||
}
|
||||
}
|
||||
|
||||
class _FakeGeocodingPlatform extends pi.Geocoding {
|
||||
_FakeGeocodingPlatform({this.placemarks = const [], this.error})
|
||||
: super.implementation(const pi.GeocodingCreationParams());
|
||||
|
||||
final List<pi.Placemark> placemarks;
|
||||
Object? error;
|
||||
|
||||
@override
|
||||
Future<List<pi.Placemark>> placemarkFromCoordinates(
|
||||
double latitude,
|
||||
double longitude, {
|
||||
Locale? locale,
|
||||
}) async {
|
||||
if (error != null) throw error!;
|
||||
return placemarks;
|
||||
}
|
||||
}
|
||||
|
||||
class _FakeGeocodingPlatformFactory extends pi.GeocodingPlatformFactory {
|
||||
_FakeGeocodingPlatformFactory(this.geocoding);
|
||||
|
||||
final pi.Geocoding geocoding;
|
||||
|
||||
@override
|
||||
pi.Geocoding createGeocoding(pi.GeocodingCreationParams params) => geocoding;
|
||||
}
|
||||
|
||||
Position _somePosition() => Position(
|
||||
latitude: 52.5,
|
||||
longitude: 13.4,
|
||||
timestamp: DateTime.utc(2026, 1, 1),
|
||||
accuracy: 0,
|
||||
altitude: 0,
|
||||
altitudeAccuracy: 0,
|
||||
heading: 0,
|
||||
headingAccuracy: 0,
|
||||
speed: 0,
|
||||
speedAccuracy: 0,
|
||||
);
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
void setUpPlatforms({
|
||||
required LocationPermission permission,
|
||||
Position? position,
|
||||
Object? currentPositionError,
|
||||
List<pi.Placemark> placemarks = const [],
|
||||
Object? geocodingError,
|
||||
}) {
|
||||
GeolocatorPlatform.instance = _FakeGeolocatorPlatform(
|
||||
permission: permission,
|
||||
position: position,
|
||||
currentPositionError: currentPositionError,
|
||||
);
|
||||
pi.GeocodingPlatformFactory.instance = _FakeGeocodingPlatformFactory(
|
||||
_FakeGeocodingPlatform(placemarks: placemarks, error: geocodingError),
|
||||
);
|
||||
}
|
||||
|
||||
group('GpsCountryCodeSource', () {
|
||||
test('liefert den ISO-Code aus dem ersten Placemark bei erteilter Berechtigung', () async {
|
||||
setUpPlatforms(
|
||||
permission: LocationPermission.whileInUse,
|
||||
position: _somePosition(),
|
||||
placemarks: const [pi.Placemark(isoCountryCode: 'de')],
|
||||
);
|
||||
|
||||
final source = GpsCountryCodeSource();
|
||||
|
||||
expect(await source.resolve(), 'DE');
|
||||
});
|
||||
|
||||
test('fordert die Berechtigung an, wenn sie noch nicht erteilt ist', () async {
|
||||
setUpPlatforms(
|
||||
permission: LocationPermission.denied,
|
||||
position: _somePosition(),
|
||||
placemarks: const [pi.Placemark(isoCountryCode: 'fr')],
|
||||
);
|
||||
|
||||
final source = GpsCountryCodeSource();
|
||||
|
||||
// Der Fake liefert bei checkPermission() und requestPermission() denselben
|
||||
// Wert; hier reicht es zu prüfen, dass bei "denied" nachgefragt wird und
|
||||
// trotzdem kein Ergebnis zustande kommt (Fake bleibt bei "denied").
|
||||
expect(await source.resolve(), isNull);
|
||||
});
|
||||
|
||||
test('liefert null, wenn die Berechtigung dauerhaft verweigert ist', () async {
|
||||
setUpPlatforms(
|
||||
permission: LocationPermission.deniedForever,
|
||||
position: _somePosition(),
|
||||
placemarks: const [pi.Placemark(isoCountryCode: 'de')],
|
||||
);
|
||||
|
||||
final source = GpsCountryCodeSource();
|
||||
|
||||
expect(await source.resolve(), isNull);
|
||||
});
|
||||
|
||||
test('liefert null, wenn getCurrentPosition wirft (z. B. GPS aus)', () async {
|
||||
setUpPlatforms(
|
||||
permission: LocationPermission.whileInUse,
|
||||
currentPositionError: const LocationServiceDisabledException(),
|
||||
);
|
||||
|
||||
final source = GpsCountryCodeSource();
|
||||
|
||||
expect(await source.resolve(), isNull);
|
||||
});
|
||||
|
||||
test('liefert null, wenn das Reverse-Geocoding wirft (z. B. kein Netz)', () async {
|
||||
setUpPlatforms(
|
||||
permission: LocationPermission.whileInUse,
|
||||
position: _somePosition(),
|
||||
geocodingError: Exception('network error'),
|
||||
);
|
||||
|
||||
final source = GpsCountryCodeSource();
|
||||
|
||||
expect(await source.resolve(), isNull);
|
||||
});
|
||||
|
||||
test('liefert null, wenn keine Placemarks gefunden werden', () async {
|
||||
setUpPlatforms(
|
||||
permission: LocationPermission.whileInUse,
|
||||
position: _somePosition(),
|
||||
placemarks: const [],
|
||||
);
|
||||
|
||||
final source = GpsCountryCodeSource();
|
||||
|
||||
expect(await source.resolve(), isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:emergency/services/country_code_sources/manual_country_code_source.dart';
|
||||
|
||||
/// Analog zu `cached_country_code_source_test.dart`: `shared_preferences`
|
||||
/// ist über `SharedPreferences.setMockInitialValues` vollständig ohne echten
|
||||
/// Plattform-Kanal testbar.
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('ManualCountryCodeSource', () {
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
});
|
||||
|
||||
test('resolve() liefert null, wenn noch keine manuelle Auswahl gespeichert ist', () async {
|
||||
const source = ManualCountryCodeSource();
|
||||
|
||||
expect(await source.resolve(), isNull);
|
||||
});
|
||||
|
||||
test('save() gefolgt von resolve() liefert den gespeicherten, normalisierten Code', () async {
|
||||
const source = ManualCountryCodeSource();
|
||||
|
||||
await source.save('fr');
|
||||
|
||||
expect(await source.resolve(), 'FR');
|
||||
});
|
||||
|
||||
test('save(null) löscht eine zuvor gesetzte manuelle Auswahl wieder', () async {
|
||||
const source = ManualCountryCodeSource();
|
||||
|
||||
await source.save('FR');
|
||||
expect(await source.resolve(), 'FR');
|
||||
|
||||
await source.save(null);
|
||||
expect(await source.resolve(), isNull);
|
||||
});
|
||||
|
||||
test('verwendet einen eigenen Key, unabhängig vom automatischen Cache', () async {
|
||||
SharedPreferences.setMockInitialValues({'last_known_country_code': 'US'});
|
||||
const source = ManualCountryCodeSource();
|
||||
|
||||
// Der automatische Cache-Wert darf die manuelle Auswahl nicht
|
||||
// beeinflussen, solange keine manuelle Auswahl gesetzt wurde.
|
||||
expect(await source.resolve(), isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:emergency/services/country_code_sources/sim_country_code_source.dart';
|
||||
|
||||
/// `carrier_info` spricht direkt (ohne Platform-Interface-Package) den
|
||||
/// MethodChannel `plugins.chizi.tech/carrier_info` an. Flutter erlaubt es,
|
||||
/// diesen Kanal in Tests mit einem Mock-Handler zu belegen – dadurch lässt
|
||||
/// sich die eigentliche Priorisierungs-/Normalisierungslogik von
|
||||
/// [SimCountryCodeSource] (anders als bei GPS, siehe dortigen Struktur-Test)
|
||||
/// echt testen, ganz ohne reales Gerät/Emulator.
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
const channel = MethodChannel('plugins.chizi.tech/carrier_info');
|
||||
|
||||
void mockHandler(Future<Object?> Function(MethodCall call) handler) {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, handler);
|
||||
}
|
||||
|
||||
Map<String, dynamic> androidInfo({
|
||||
List<Map<String, dynamic>> telephonyInfo = const [],
|
||||
List<Map<String, dynamic>> subscriptionsInfo = const [],
|
||||
}) {
|
||||
return {
|
||||
'isVoiceCapable': true,
|
||||
'isDataEnabled': true,
|
||||
'subscriptionsInfo': subscriptionsInfo,
|
||||
'isDataCapable': true,
|
||||
'isMultiSimSupported': 'MULTISIM_NOT_SUPPORTED_BY_HARDWARE',
|
||||
'isSmsCapable': true,
|
||||
'telephonyInfo': telephonyInfo,
|
||||
};
|
||||
}
|
||||
|
||||
Map<String, dynamic> iosInfo({
|
||||
List<Map<String, dynamic>> carrierData = const [],
|
||||
}) {
|
||||
return {
|
||||
'carrierData': carrierData,
|
||||
'supportsEmbeddedSIM': false,
|
||||
'carrierRadioAccessTechnologyTypeList': <String>['LTE'],
|
||||
'isSIMInserted': true,
|
||||
};
|
||||
}
|
||||
|
||||
tearDown(() {
|
||||
debugDefaultTargetPlatformOverride = null;
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, null);
|
||||
});
|
||||
|
||||
group('SimCountryCodeSource auf Android', () {
|
||||
setUp(() {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||
});
|
||||
|
||||
test('bevorzugt networkCountryIso (aktuelles Netz, roaming-fest) vor allem anderen', () async {
|
||||
mockHandler((call) async {
|
||||
expect(call.method, 'getAndroidInfo');
|
||||
return androidInfo(
|
||||
telephonyInfo: [
|
||||
{'networkCountryIso': 'fr', 'isoCountryCode': 'de'},
|
||||
],
|
||||
subscriptionsInfo: [
|
||||
{'countryIso': 'de'},
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
const source = SimCountryCodeSource();
|
||||
|
||||
expect(await source.resolve(), 'FR');
|
||||
});
|
||||
|
||||
test('fällt auf isoCountryCode zurück, wenn networkCountryIso leer ist', () async {
|
||||
mockHandler((call) async {
|
||||
return androidInfo(
|
||||
telephonyInfo: [
|
||||
{'networkCountryIso': '', 'isoCountryCode': 'de'},
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
const source = SimCountryCodeSource();
|
||||
|
||||
expect(await source.resolve(), 'DE');
|
||||
});
|
||||
|
||||
test('fällt auf subscriptionsInfo.countryIso zurück, wenn telephonyInfo leer ist', () async {
|
||||
mockHandler((call) async {
|
||||
return androidInfo(
|
||||
subscriptionsInfo: [
|
||||
{'countryIso': 'us'},
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
const source = SimCountryCodeSource();
|
||||
|
||||
expect(await source.resolve(), 'US');
|
||||
});
|
||||
|
||||
test('liefert null, wenn alle Quellen leer sind (z. B. kein SIM-Slot belegt)', () async {
|
||||
mockHandler((call) async => androidInfo());
|
||||
|
||||
const source = SimCountryCodeSource();
|
||||
|
||||
expect(await source.resolve(), isNull);
|
||||
});
|
||||
|
||||
test('liefert null statt zu werfen, wenn das Plugin eine PlatformException wirft', () async {
|
||||
mockHandler((call) async {
|
||||
throw PlatformException(code: 'UNAVAILABLE', message: 'no telephony service');
|
||||
});
|
||||
|
||||
const source = SimCountryCodeSource();
|
||||
|
||||
expect(await source.resolve(), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('SimCountryCodeSource auf iOS', () {
|
||||
setUp(() {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
|
||||
});
|
||||
|
||||
test('liefert isoCountryCode auf iOS < 16 (CTCarrier noch verfügbar)', () async {
|
||||
mockHandler((call) async {
|
||||
expect(call.method, 'getIosInfo');
|
||||
return iosInfo(
|
||||
carrierData: [
|
||||
{'isoCountryCode': 'ng', 'carrierName': 'Airtel'},
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
const source = SimCountryCodeSource();
|
||||
|
||||
expect(await source.resolve(), 'NG');
|
||||
});
|
||||
|
||||
test(
|
||||
'liefert null auf iOS 16+ (CTCarrier deprecated, isoCountryCode ist nil) – '
|
||||
'bekanntes, akzeptiertes Risiko laut docs/plan.md Abschnitt 9',
|
||||
() async {
|
||||
mockHandler((call) async {
|
||||
return iosInfo(
|
||||
carrierData: [
|
||||
{'isoCountryCode': null, 'carrierName': null},
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
const source = SimCountryCodeSource();
|
||||
|
||||
expect(await source.resolve(), isNull);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:url_launcher_platform_interface/link.dart';
|
||||
import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';
|
||||
|
||||
import 'package:emergency/services/dialer_service.dart';
|
||||
|
||||
/// Fake-Implementierung der `url_launcher`-Plattformschnittstelle.
|
||||
///
|
||||
/// Vermeidet Mocking auf `MethodChannel`-Ebene: `url_launcher` erlaubt es,
|
||||
/// [UrlLauncherPlatform.instance] durch eine eigene Implementierung zu
|
||||
/// ersetzen – das ist der offiziell vorgesehene Weg, das Package in reinen
|
||||
/// Unit-Tests ohne echte Plattform-Kanäle zu testen.
|
||||
class _FakeUrlLauncherPlatform extends UrlLauncherPlatform {
|
||||
_FakeUrlLauncherPlatform({required this.canLaunchResult});
|
||||
|
||||
final bool canLaunchResult;
|
||||
|
||||
String? launchedUrl;
|
||||
int launchCallCount = 0;
|
||||
|
||||
@override
|
||||
LinkDelegate? get linkDelegate => null;
|
||||
|
||||
@override
|
||||
Future<bool> canLaunch(String url) async => canLaunchResult;
|
||||
|
||||
@override
|
||||
Future<bool> launchUrl(String url, LaunchOptions options) async {
|
||||
launchCallCount++;
|
||||
launchedUrl = url;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('DialerService', () {
|
||||
const dialerService = DialerService();
|
||||
|
||||
test('call() öffnet den Wähler mit tel:-URI und liefert true, wenn canLaunch true ist', () async {
|
||||
final fakePlatform = _FakeUrlLauncherPlatform(canLaunchResult: true);
|
||||
UrlLauncherPlatform.instance = fakePlatform;
|
||||
|
||||
final result = await dialerService.call('112');
|
||||
|
||||
expect(result, isTrue);
|
||||
expect(fakePlatform.launchCallCount, 1);
|
||||
expect(fakePlatform.launchedUrl, 'tel:112');
|
||||
});
|
||||
|
||||
test('call() liefert false ohne launch(), wenn canLaunch false ist', () async {
|
||||
final fakePlatform = _FakeUrlLauncherPlatform(canLaunchResult: false);
|
||||
UrlLauncherPlatform.instance = fakePlatform;
|
||||
|
||||
final result = await dialerService.call('911');
|
||||
|
||||
expect(result, isFalse);
|
||||
expect(fakePlatform.launchCallCount, 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:emergency/services/country_code_sources/country_code_source.dart';
|
||||
import 'package:emergency/services/location_service.dart';
|
||||
|
||||
/// Einfache manuelle Fake-Quelle statt Mocking-Package (siehe AP4-Vorgabe:
|
||||
/// kein neues Mocking-Package einführen).
|
||||
class _FakeSource implements CountryCodeSource {
|
||||
_FakeSource(this._result);
|
||||
|
||||
final String? _result;
|
||||
int callCount = 0;
|
||||
|
||||
@override
|
||||
Future<String?> resolve() async {
|
||||
callCount++;
|
||||
return _result;
|
||||
}
|
||||
}
|
||||
|
||||
class _FakeCache implements CountryCodeCache {
|
||||
_FakeCache({String? initial}) : _stored = initial;
|
||||
|
||||
String? _stored;
|
||||
int resolveCallCount = 0;
|
||||
final List<String> savedCodes = [];
|
||||
|
||||
@override
|
||||
Future<String?> resolve() async {
|
||||
resolveCallCount++;
|
||||
return _stored;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> save(String isoCode) async {
|
||||
savedCodes.add(isoCode);
|
||||
_stored = isoCode;
|
||||
}
|
||||
}
|
||||
|
||||
/// Fake für die manuelle Auswahl (AP6). Ohne explizite Injektion würde
|
||||
/// `LocationService()` intern ein echtes `ManualCountryCodeSource` (und damit
|
||||
/// `shared_preferences`) ansprechen, was in diesen reinen Unit-Tests (kein
|
||||
/// `TestWidgetsFlutterBinding.ensureInitialized()`/Mock-Setup) nicht
|
||||
/// notwendig sein soll – die bestehenden SIM/GPS/Cache-Tests sollen von der
|
||||
/// manuellen Auswahl unbeeinflusst bleiben.
|
||||
class _FakeManualSource implements ManualCountryCodeStore {
|
||||
_FakeManualSource([this._result]);
|
||||
|
||||
final String? _result;
|
||||
int resolveCallCount = 0;
|
||||
final List<String?> savedCodes = [];
|
||||
|
||||
@override
|
||||
Future<String?> resolve() async {
|
||||
resolveCallCount++;
|
||||
return _result;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> save(String? isoCode) async {
|
||||
savedCodes.add(isoCode);
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('LocationService.resolveCountryCode()', () {
|
||||
test(
|
||||
'SIM liefert Ergebnis -> wird verwendet, GPS/Cache werden nicht abgefragt, '
|
||||
'Ergebnis wird gecacht',
|
||||
() async {
|
||||
final sim = _FakeSource('DE');
|
||||
final gps = _FakeSource('FR');
|
||||
final cache = _FakeCache(initial: 'US');
|
||||
final service = LocationService(
|
||||
simSource: sim,
|
||||
gpsSource: gps,
|
||||
cacheSource: cache,
|
||||
manualSource: _FakeManualSource(),
|
||||
);
|
||||
|
||||
final result = await service.resolveCountryCode();
|
||||
|
||||
expect(result, 'DE');
|
||||
expect(sim.callCount, 1);
|
||||
expect(gps.callCount, 0);
|
||||
expect(cache.resolveCallCount, 0);
|
||||
expect(cache.savedCodes, ['DE']);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'SIM liefert null, GPS liefert Ergebnis -> wird verwendet, Cache wird nicht '
|
||||
'gelesen, aber beschrieben',
|
||||
() async {
|
||||
final sim = _FakeSource(null);
|
||||
final gps = _FakeSource('FR');
|
||||
final cache = _FakeCache(initial: 'US');
|
||||
final service = LocationService(
|
||||
simSource: sim,
|
||||
gpsSource: gps,
|
||||
cacheSource: cache,
|
||||
manualSource: _FakeManualSource(),
|
||||
);
|
||||
|
||||
final result = await service.resolveCountryCode();
|
||||
|
||||
expect(result, 'FR');
|
||||
expect(sim.callCount, 1);
|
||||
expect(gps.callCount, 1);
|
||||
expect(cache.resolveCallCount, 0);
|
||||
expect(cache.savedCodes, ['FR']);
|
||||
},
|
||||
);
|
||||
|
||||
test('SIM und GPS liefern null, Cache liefert Ergebnis -> wird verwendet', () async {
|
||||
final sim = _FakeSource(null);
|
||||
final gps = _FakeSource(null);
|
||||
final cache = _FakeCache(initial: 'US');
|
||||
final service = LocationService(
|
||||
simSource: sim,
|
||||
gpsSource: gps,
|
||||
cacheSource: cache,
|
||||
manualSource: _FakeManualSource(),
|
||||
);
|
||||
|
||||
final result = await service.resolveCountryCode();
|
||||
|
||||
expect(result, 'US');
|
||||
expect(cache.resolveCallCount, 1);
|
||||
// Kein erneutes Schreiben, da das Ergebnis ja bereits aus dem Cache kam.
|
||||
expect(cache.savedCodes, isEmpty);
|
||||
});
|
||||
|
||||
test('Alle drei automatischen Quellen liefern null -> Ergebnis ist null', () async {
|
||||
final sim = _FakeSource(null);
|
||||
final gps = _FakeSource(null);
|
||||
final cache = _FakeCache();
|
||||
final service = LocationService(
|
||||
simSource: sim,
|
||||
gpsSource: gps,
|
||||
cacheSource: cache,
|
||||
manualSource: _FakeManualSource(),
|
||||
);
|
||||
|
||||
final result = await service.resolveCountryCode();
|
||||
|
||||
expect(result, isNull);
|
||||
expect(cache.savedCodes, isEmpty);
|
||||
});
|
||||
|
||||
test(
|
||||
'manuelle Auswahl gesetzt -> hat Vorrang vor SIM/GPS/Cache, die anderen '
|
||||
'Quellen werden gar nicht erst abgefragt',
|
||||
() async {
|
||||
final sim = _FakeSource('DE');
|
||||
final gps = _FakeSource('FR');
|
||||
final cache = _FakeCache(initial: 'US');
|
||||
final manual = _FakeManualSource('IT');
|
||||
final service = LocationService(
|
||||
simSource: sim,
|
||||
gpsSource: gps,
|
||||
cacheSource: cache,
|
||||
manualSource: manual,
|
||||
);
|
||||
|
||||
final result = await service.resolveCountryCode();
|
||||
|
||||
expect(result, 'IT');
|
||||
expect(sim.callCount, 0);
|
||||
expect(gps.callCount, 0);
|
||||
expect(cache.resolveCallCount, 0);
|
||||
// Die manuelle Auswahl wird bewusst nicht in den automatischen Cache
|
||||
// geschrieben (getrennte Mechanismen, siehe Doc-Kommentar).
|
||||
expect(cache.savedCodes, isEmpty);
|
||||
},
|
||||
);
|
||||
|
||||
test('keine manuelle Auswahl gesetzt -> Kette fällt normal auf SIM zurück', () async {
|
||||
final sim = _FakeSource('DE');
|
||||
final gps = _FakeSource('FR');
|
||||
final cache = _FakeCache(initial: 'US');
|
||||
final manual = _FakeManualSource(null);
|
||||
final service = LocationService(
|
||||
simSource: sim,
|
||||
gpsSource: gps,
|
||||
cacheSource: cache,
|
||||
manualSource: manual,
|
||||
);
|
||||
|
||||
final result = await service.resolveCountryCode();
|
||||
|
||||
expect(result, 'DE');
|
||||
expect(manual.resolveCallCount, 1);
|
||||
});
|
||||
});
|
||||
|
||||
group('LocationService.setManualCountryCode()', () {
|
||||
test('delegiert an die manuelle Quelle', () async {
|
||||
final manual = _FakeManualSource();
|
||||
final service = LocationService(
|
||||
simSource: _FakeSource(null),
|
||||
gpsSource: _FakeSource(null),
|
||||
cacheSource: _FakeCache(),
|
||||
manualSource: manual,
|
||||
);
|
||||
|
||||
await service.setManualCountryCode('IT');
|
||||
await service.setManualCountryCode(null);
|
||||
|
||||
expect(manual.savedCodes, ['IT', null]);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user