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,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