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>
215 lines
6.2 KiB
Dart
215 lines
6.2 KiB
Dart
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]);
|
||
});
|
||
});
|
||
}
|