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
@@ -0,0 +1,146 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import 'package:emergency/features/home/country_picker_screen.dart';
import 'package:emergency/l10n/app_localizations.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/repositories/emergency_number_repository.dart';
import 'package:emergency/services/country_code_sources/country_code_source.dart';
import 'package:emergency/services/location_service.dart';
/// Liefert eine feste Länderliste, ohne das echte JSON-Asset laden zu
/// müssen (analog zum Fake-Repository-Muster in `emergency_provider_test.dart`).
class _FakeRepository extends EmergencyNumberRepository {
@override
List<Country> get allCountries => const [
Country(
isoCode: 'DE',
nameDe: 'Deutschland',
nameEn: 'Germany',
flag: '🇩🇪',
numbers: [
EmergencyNumber(type: EmergencyType.general, number: '112', labelDe: 'Notruf', labelEn: 'Emergency'),
],
),
Country(
isoCode: 'FR',
nameDe: 'Frankreich',
nameEn: 'France',
flag: '🇫🇷',
numbers: [
EmergencyNumber(type: EmergencyType.general, number: '112', labelDe: 'Notruf', labelEn: 'Emergency'),
],
),
];
}
class _NoopSource implements CountryCodeSource {
const _NoopSource();
@override
Future<String?> resolve() async => null;
}
/// Fake-Provider: überschreibt `selectCountryManually`, damit der Test ohne
/// echte `LocationService`-Ermittlungskette prüfen kann, mit welchem
/// ISO-Code der Screen die Auswahl auslöst.
class _FakeProvider extends EmergencyProvider {
_FakeProvider({required super.repository})
: super(
locationService: LocationService(
simSource: const _NoopSource(),
gpsSource: const _NoopSource(),
),
);
String? selectedIsoCode;
int selectCallCount = 0;
@override
Future<void> selectCountryManually(String isoCode) async {
selectCallCount++;
selectedIsoCode = isoCode;
}
}
void main() {
Future<void> pumpPickerBehindAButton(WidgetTester tester, EmergencyProvider provider) async {
// CountryPickerScreen ruft `Navigator.pop()` beim Auswählen auf dafür
// muss eine vorherige Route existieren, sonst wäre der Pop ein No-op.
await tester.pumpWidget(
ChangeNotifierProvider<EmergencyProvider>.value(
value: provider,
child: MaterialApp(
// Feste Locale, damit die (unveränderten) deutschen Text-Assertions
// dieser Tests unabhängig von der Test-Umgebungs-/Systemsprache
// funktionieren; das eigentliche Umschalten der App-Sprache wird
// gezielt in `test/core/country_locale_map_test.dart` bzw.
// `test/app_locale_test.dart` (AP7) geprüft.
locale: const Locale('de'),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Builder(
builder: (context) => ElevatedButton(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const CountryPickerScreen()),
),
child: const Text('Öffnen'),
),
),
),
),
);
await tester.tap(find.text('Öffnen'));
await tester.pumpAndSettle();
}
testWidgets('zeigt die Länderliste an', (tester) async {
final provider = _FakeProvider(repository: _FakeRepository());
await pumpPickerBehindAButton(tester, provider);
expect(find.text('Deutschland'), findsOneWidget);
expect(find.text('Frankreich'), findsOneWidget);
});
testWidgets('Suchfeld filtert die Länderliste', (tester) async {
final provider = _FakeProvider(repository: _FakeRepository());
await pumpPickerBehindAButton(tester, provider);
await tester.enterText(find.byType(TextField), 'Deutsch');
await tester.pump();
expect(find.text('Deutschland'), findsOneWidget);
expect(find.text('Frankreich'), findsNothing);
});
testWidgets('zeigt einen Hinweis, wenn kein Land zum Filter passt', (tester) async {
final provider = _FakeProvider(repository: _FakeRepository());
await pumpPickerBehindAButton(tester, provider);
await tester.enterText(find.byType(TextField), 'Nichtland');
await tester.pump();
expect(find.text('Kein Land gefunden.'), findsOneWidget);
});
testWidgets('Tap auf ein Land löst selectCountryManually aus und schließt den Screen', (
tester,
) async {
final provider = _FakeProvider(repository: _FakeRepository());
await pumpPickerBehindAButton(tester, provider);
await tester.tap(find.text('Deutschland'));
await tester.pumpAndSettle();
expect(provider.selectCallCount, 1);
expect(provider.selectedIsoCode, 'DE');
// Screen wurde geschlossen (zurück zum "Öffnen"-Button).
expect(find.text('Öffnen'), findsOneWidget);
expect(find.text('Deutschland'), findsNothing);
});
}