Files
emergency/test/providers/emergency_provider_test.dart
rootandClaude Sonnet 5 e3aa088bc2 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>
2026-07-24 11:29:38 +02:00

248 lines
8.5 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:emergency/models/country.dart';
import 'package:emergency/models/emergency_number.dart';
import 'package:emergency/models/emergency_type.dart';
import 'package:emergency/providers/emergency_provider.dart';
import 'package:emergency/providers/emergency_status.dart';
import 'package:emergency/repositories/emergency_number_repository.dart';
import 'package:emergency/services/country_code_sources/country_code_source.dart';
import 'package:emergency/services/location_service.dart';
/// Einfache manuelle Fakes statt Mocking-Package (siehe AP4-Vorgabe: kein
/// neues Mocking-Package einführen). Beide Basisklassen sind nicht `final`/
/// `sealed`, daher genügt es, `load`/`lookup` bzw. `resolveCountryCode` zu
/// überschreiben, statt ein eigenes Interface einzuführen.
class _FakeRepository extends EmergencyNumberRepository {
_FakeRepository({this.loadError, this.countryToReturn});
Object? loadError;
Country? countryToReturn;
int loadCallCount = 0;
@override
Future<void> load() async {
loadCallCount++;
final error = loadError;
if (error != null) throw error;
}
@override
Country? lookup(String isoCode) => countryToReturn;
}
/// Nur zur Erfüllung des `super()`-Konstruktors von [LocationService] nötig
/// (wird nie aufgerufen, da [_FakeLocationService.resolveCountryCode]
/// überschrieben ist). Ohne einen expliziten `gpsSource` würde
/// `LocationService()` intern ein echtes `GpsCountryCodeSource` (und damit
/// `Geocoding()`) konstruieren, was außerhalb eines registrierten
/// Platform-Interface (siehe `gps_country_code_source_test.dart`) sofort wirft.
class _NoopSource implements CountryCodeSource {
const _NoopSource();
@override
Future<String?> resolve() async => null;
}
class _FakeLocationService extends LocationService {
_FakeLocationService(this.isoCode)
: super(simSource: const _NoopSource(), gpsSource: const _NoopSource());
/// Simuliert das Ergebnis der automatischen Kette (SIM → GPS → Cache).
String? isoCode;
/// Simuliert die manuelle Auswahl (AP6): hat, analog zur echten
/// Priorisierung in `LocationService`, Vorrang vor [isoCode].
String? manualIsoCode;
int resolveCallCount = 0;
int setManualCallCount = 0;
String? lastManualCode;
@override
Future<String?> resolveCountryCode() async {
resolveCallCount++;
return manualIsoCode ?? isoCode;
}
@override
Future<void> setManualCountryCode(String? code) async {
setManualCallCount++;
lastManualCode = code;
manualIsoCode = code;
}
}
const _germany = Country(
isoCode: 'DE',
nameDe: 'Deutschland',
nameEn: 'Germany',
flag: '🇩🇪',
numbers: [
EmergencyNumber(type: EmergencyType.general, number: '112', labelDe: 'Notruf', labelEn: 'Emergency'),
],
);
const _france = Country(
isoCode: 'FR',
nameDe: 'Frankreich',
nameEn: 'France',
flag: '🇫🇷',
numbers: [
EmergencyNumber(type: EmergencyType.general, number: '112', labelDe: 'Notruf', labelEn: 'Emergency'),
],
);
void main() {
group('EmergencyProvider.init()', () {
test('erfolgreicher Durchlauf -> status ready mit Land', () async {
final provider = EmergencyProvider(
repository: _FakeRepository(countryToReturn: _germany),
locationService: _FakeLocationService('DE'),
);
await provider.init();
expect(provider.status, EmergencyStatus.ready);
expect(provider.country, _germany);
expect(provider.errorMessage, isNull);
});
test('Ländercode nicht ermittelbar -> status countryUnknown', () async {
final provider = EmergencyProvider(
repository: _FakeRepository(countryToReturn: _germany),
locationService: _FakeLocationService(null),
);
await provider.init();
expect(provider.status, EmergencyStatus.countryUnknown);
expect(provider.country, isNull);
});
test('Land nicht in Datenbasis -> status ready mit country == null', () async {
final provider = EmergencyProvider(
repository: _FakeRepository(countryToReturn: null),
locationService: _FakeLocationService('XX'),
);
await provider.init();
expect(provider.status, EmergencyStatus.ready);
expect(provider.country, isNull);
});
test('repository.load() wirft -> status error mit errorMessage', () async {
final repository = _FakeRepository(loadError: const FormatException('kaputtes Asset'));
final locationService = _FakeLocationService('DE');
final provider = EmergencyProvider(repository: repository, locationService: locationService);
await provider.init();
expect(provider.status, EmergencyStatus.error);
expect(provider.errorMessage, isNotNull);
expect(repository.loadCallCount, 1);
// Ländererkennung darf nach einem Ladefehler nicht mehr angestoßen werden.
expect(locationService.resolveCallCount, 0);
});
test('durchläuft die Status initializing -> locating -> ready', () async {
final provider = EmergencyProvider(
repository: _FakeRepository(countryToReturn: _germany),
locationService: _FakeLocationService('DE'),
);
final observedStatuses = <EmergencyStatus>[];
provider.addListener(() => observedStatuses.add(provider.status));
await provider.init();
expect(observedStatuses, [
EmergencyStatus.initializing,
EmergencyStatus.locating,
EmergencyStatus.ready,
]);
});
});
group('EmergencyProvider.retry()', () {
test('wiederholt init() und kann nach einem Fehler erfolgreich sein', () async {
final repository = _FakeRepository(
loadError: const FormatException('kaputtes Asset'),
countryToReturn: _germany,
);
final locationService = _FakeLocationService('DE');
final provider = EmergencyProvider(repository: repository, locationService: locationService);
await provider.init();
expect(provider.status, EmergencyStatus.error);
expect(repository.loadCallCount, 1);
repository.loadError = null;
await provider.retry();
expect(provider.status, EmergencyStatus.ready);
expect(provider.country, _germany);
expect(provider.errorMessage, isNull);
expect(repository.loadCallCount, 2);
});
});
group('EmergencyProvider.selectCountryManually()', () {
test('persistiert die Auswahl und stößt init() erneut an', () async {
final repository = _FakeRepository(countryToReturn: _germany);
final locationService = _FakeLocationService('DE');
final provider = EmergencyProvider(repository: repository, locationService: locationService);
await provider.init();
expect(provider.country, _germany);
repository.countryToReturn = _france;
await provider.selectCountryManually('FR');
expect(locationService.setManualCallCount, 1);
expect(locationService.lastManualCode, 'FR');
// init() wurde ein zweites Mal komplett durchlaufen.
expect(locationService.resolveCallCount, 2);
expect(provider.status, EmergencyStatus.ready);
expect(provider.country, _france);
});
});
group('EmergencyProvider.clearManualSelection()', () {
test('löscht die manuelle Auswahl und ermittelt danach erneut automatisch', () async {
// isoCode simuliert das Ergebnis der automatischen Kette (z. B. SIM),
// manualIsoCode eine bereits zuvor gesetzte manuelle Auswahl.
final locationService = _FakeLocationService('DE')..manualIsoCode = 'FR';
final repository = _FakeRepository(countryToReturn: _france);
final provider = EmergencyProvider(repository: repository, locationService: locationService);
await provider.init();
expect(provider.country, _france); // manuelle Auswahl hat Vorrang
repository.countryToReturn = _germany;
await provider.clearManualSelection();
expect(locationService.setManualCallCount, 1);
expect(locationService.lastManualCode, isNull);
expect(locationService.manualIsoCode, isNull);
expect(locationService.resolveCallCount, 2);
expect(provider.status, EmergencyStatus.ready);
// Nach dem Löschen greift wieder das automatische Ergebnis (DE).
expect(provider.country, _germany);
});
});
group('EmergencyProvider.repository', () {
test('exponiert das intern verwendete Repository', () {
final repository = _FakeRepository(countryToReturn: _germany);
final provider = EmergencyProvider(
repository: repository,
locationService: _FakeLocationService('DE'),
);
expect(provider.repository, same(repository));
});
});
}