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:
root
2026-07-24 11:29:38 +02:00
co-authored by Claude Sonnet 5
parent 40e95cc495
commit e3aa088bc2
54 changed files with 7550 additions and 145 deletions
+214
View File
@@ -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]);
});
});
}