leafittome/test/widget_test.dart
cschlaefke 9caaa2ef79 V3: Untersuchungshistorie je Pflanze
Jede Diagnose landet automatisch in households/{id}/plants/{id}/diagnoses;
das Pflanzen-Detail zeigt die Liste (Ampel-Icon, Kurzbefund, Datum),
Antippen öffnet das Ergebnis-Sheet mit Untersuchungsdatum, volle
Mitglieder können Einträge löschen. Rules: read/create für alle im
Haushalt (auch Sitter), delete nur member, update nie. Widget-Test
navigiert bis ins Sheet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 22:55:07 +02:00

440 lines
16 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:fake_cloud_firestore/fake_cloud_firestore.dart';
import 'package:firebase_auth_mocks/firebase_auth_mocks.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:leafittome/app.dart';
import 'package:leafittome/core/firebase/firebase_providers.dart';
import 'package:leafittome/core/settings/settings_provider.dart';
import 'package:leafittome/features/notifications/data/push_registration_service.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// Tests brauchen kein echtes FCM Push-Registrierung ist ein No-Op.
class _FakePushRegistrationService implements PushRegistrationService {
@override
Future<void> registerDevice(User user) async {}
@override
Future<void> syncReminderTime(TimeOfDay time) async {}
}
/// Baut die App mit gemocktem Firebase: angemeldeter Nutzer 'u1' als Besitzer
/// des Haushalts 'h1' mit einer Monstera, deren Gießen überfällig ist.
/// Mit [secondHousehold] ist u1 zusätzlich Sitter im fremden Haushalt 'h2'
/// (Besitzer 'u2'), in dem ein Ficus fällig ist.
/// Mit [extraMember] hat 'h1' zusätzlich den Sitter 'u3'.
Future<Widget> buildTestApp({
String role = 'member',
bool secondHousehold = false,
bool extraMember = false,
}) async {
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
final auth = MockFirebaseAuth(
signedIn: true,
mockUser: MockUser(uid: 'u1', email: 'test@example.com'),
);
final firestore = FakeFirebaseFirestore();
await firestore.collection('users').doc('u1').set({
'email': 'test@example.com',
'householdId': 'h1',
});
await firestore.collection('households').doc('h1').set({
'name': 'Mein Haushalt',
'ownerUid': 'u1',
'memberUids': ['u1', if (extraMember) 'u3'],
'members': {
'u1': {'role': role, 'email': 'test@example.com'},
if (extraMember) 'u3': {'role': 'sitter', 'email': 'sitter@example.com'},
},
});
if (secondHousehold) {
await firestore.collection('households').doc('h2').set({
'name': 'Zweiter Haushalt',
'ownerUid': 'u2',
'memberUids': ['u2', 'u1'],
'members': {
'u1': {'role': 'sitter', 'email': 'test@example.com'},
'u2': {'role': 'member', 'email': 'other@example.com'},
},
});
await firestore
.collection('households')
.doc('h2')
.collection('plants')
.add({
'nickname': 'Ficus',
'species': 'Ficus benjamina',
'locationId': null,
'description': '',
'careNotes': '',
'wateringIntervalDays': 7,
'fertilizingIntervalDays': 28,
'lastWatered': null,
'lastFertilized': Timestamp.fromDate(
DateTime.now().subtract(const Duration(days: 3))),
});
}
final now = DateTime.now();
await firestore
.collection('households')
.doc('h1')
.collection('plants')
.add({
'nickname': 'Monstera',
'species': 'Monstera deliciosa',
'locationId': null,
'description': '',
'careNotes': '',
'wateringIntervalDays': 7,
'fertilizingIntervalDays': 28,
'lastWatered':
Timestamp.fromDate(now.subtract(const Duration(days: 8))),
'lastFertilized':
Timestamp.fromDate(now.subtract(const Duration(days: 3))),
});
return ProviderScope(
overrides: [
sharedPreferencesProvider.overrideWithValue(prefs),
firebaseAuthProvider.overrideWithValue(auth),
firestoreProvider.overrideWithValue(firestore),
pushRegistrationServiceProvider
.overrideWithValue(_FakePushRegistrationService()),
],
child: const LeafItToMeApp(),
);
}
void main() {
testWidgets('Angemeldeter Nutzer sieht die Heute-Checkliste mit fälliger Aufgabe',
(tester) async {
await tester.pumpWidget(await buildTestApp());
await tester.pumpAndSettle();
expect(find.text('Heute'), findsOneWidget);
expect(find.textContaining('offene Aufgabe'), findsOneWidget);
expect(find.textContaining('Monstera gießen'), findsOneWidget);
});
testWidgets('Aufgabe bestätigen schreibt nach Firestore und leert die Liste',
(tester) async {
await tester.pumpWidget(await buildTestApp());
await tester.pumpAndSettle();
await tester.tap(find.text('Erledigt').first);
await tester.pumpAndSettle();
expect(find.text('Erledigt'), findsNothing);
expect(find.text('Alles versorgt!'), findsOneWidget);
});
testWidgets('Sitter kann abhaken, sieht aber keinen Anlegen-Button',
(tester) async {
await tester.pumpWidget(await buildTestApp(role: 'sitter'));
await tester.pumpAndSettle();
// Abhaken ist erlaubt …
expect(find.text('Erledigt'), findsWidgets);
// … aber Pflanzen anlegen nicht.
expect(find.text('Pflanze hinzufügen'), findsNothing);
});
testWidgets('Mitglied mehrerer Haushalte kann den aktiven Haushalt wechseln',
(tester) async {
await tester.pumpWidget(await buildTestApp(secondHousehold: true));
await tester.pumpAndSettle();
// Über das Menü zur Haushalts-Ansicht.
await tester.tap(find.byIcon(Icons.menu));
await tester.pumpAndSettle();
await tester.tap(find.text('Haushalt'));
await tester.pumpAndSettle();
// Beide Haushalte sind gelistet, h1 ist aktiv, Besitzer sind ausgewiesen.
expect(find.text('Meine Haushalte'), findsOneWidget);
expect(find.text('Aktiv'), findsOneWidget);
expect(find.text('Zweiter Haushalt'), findsOneWidget);
expect(find.text('Dein Haushalt'), findsOneWidget);
expect(find.text('Besitzer: other@example.com'), findsOneWidget);
// Wechsel nach h2: Zeiger in users/u1 wird umgestellt …
await tester.tap(find.text('Zweiter Haushalt'));
await tester.pumpAndSettle();
// … und die Ansicht zeigt den neuen Haushalt (AppBar + Listeneintrag).
expect(find.text('Zweiter Haushalt'), findsNWidgets(2));
expect(find.text('other@example.com'), findsOneWidget);
// Im fremden Haushalt (nicht Besitzer): Austreten möglich, Entfernen nicht.
expect(find.text('Haushalt verlassen'), findsOneWidget);
expect(find.byIcon(Icons.person_remove), findsNothing);
});
testWidgets('Checkliste gruppiert Aufgaben aus mehreren Haushalten',
(tester) async {
await tester.pumpWidget(await buildTestApp(secondHousehold: true));
await tester.pumpAndSettle();
// Aufgaben beider Haushalte mit Zwischenüberschriften.
expect(find.textContaining('Monstera gießen'), findsOneWidget);
expect(find.textContaining('Ficus gießen'), findsOneWidget);
expect(find.text('Mein Haushalt'), findsOneWidget);
expect(find.text('Zweiter Haushalt'), findsOneWidget);
// Die Ficus-Aufgabe (zweite Gruppe) bestätigen → sie schreibt in den
// richtigen Haushalt, die h2-Gruppe verschwindet mitsamt Überschriften.
await tester.tap(find.text('Erledigt').last);
await tester.pumpAndSettle();
expect(find.textContaining('Ficus gießen'), findsNothing);
expect(find.textContaining('Monstera gießen'), findsOneWidget);
expect(find.text('Zweiter Haushalt'), findsNothing);
});
testWidgets('Besitzer sieht Besitzer-Kennzeichnung und kann entfernen, '
'aber nicht austreten', (tester) async {
await tester.pumpWidget(await buildTestApp(extraMember: true));
await tester.pumpAndSettle();
await tester.tap(find.byIcon(Icons.menu));
await tester.pumpAndSettle();
await tester.tap(find.text('Haushalt'));
await tester.pumpAndSettle();
// Besitzer-Kennzeichnung am eigenen Eintrag, Entfernen-Knopf beim Sitter.
expect(find.text('Mitglied · Besitzer'), findsOneWidget);
expect(find.byIcon(Icons.person_remove), findsOneWidget);
expect(find.text('Haushalt verlassen'), findsNothing);
// Entfernen fragt nach.
await tester.tap(find.byIcon(Icons.person_remove));
await tester.pumpAndSettle();
expect(find.text('Mitglied entfernen?'), findsOneWidget);
});
testWidgets(
'Alt-Haushalt ohne members-Eintrag: eigene E-Mail wird nachgetragen',
(tester) async {
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
final auth = MockFirebaseAuth(
signedIn: true,
mockUser: MockUser(uid: 'u1', email: 'test@example.com'),
);
final firestore = FakeFirebaseFirestore();
await firestore.collection('users').doc('u1').set({
'email': 'test@example.com',
'householdId': 'h1',
});
// V1-Zeit: kein ownerUid, keine members-Map nur memberUids.
await firestore.collection('households').doc('h1').set({
'name': 'Mein Haushalt',
'memberUids': ['u1'],
});
await tester.pumpWidget(ProviderScope(
overrides: [
sharedPreferencesProvider.overrideWithValue(prefs),
firebaseAuthProvider.overrideWithValue(auth),
firestoreProvider.overrideWithValue(firestore),
pushRegistrationServiceProvider
.overrideWithValue(_FakePushRegistrationService()),
],
child: const LeafItToMeApp(),
));
await tester.pumpAndSettle();
final doc = await firestore.collection('households').doc('h1').get();
final members = doc.data()?['members'] as Map<String, dynamic>?;
expect((members?['u1'] as Map<String, dynamic>?)?['email'],
'test@example.com');
expect((members?['u1'] as Map<String, dynamic>?)?['role'], 'member');
});
testWidgets(
'Erster Login ohne Profil (z. B. Apple): Haushalt wird reaktiv angelegt',
(tester) async {
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
// Angemeldet (wie nach signInWithProvider), aber Firestore ist leer
// genau der Zustand, in dem Pflanzen-Speichern an den Rules scheiterte.
final auth = MockFirebaseAuth(
signedIn: true,
mockUser: MockUser(uid: 'apple1', email: 'apple@example.com'),
);
final firestore = FakeFirebaseFirestore();
await tester.pumpWidget(ProviderScope(
overrides: [
sharedPreferencesProvider.overrideWithValue(prefs),
firebaseAuthProvider.overrideWithValue(auth),
firestoreProvider.overrideWithValue(firestore),
pushRegistrationServiceProvider
.overrideWithValue(_FakePushRegistrationService()),
],
child: const LeafItToMeApp(),
));
await tester.pumpAndSettle();
// Das BootstrapGate hat Profil + Haushalt angelegt …
final userDoc = await firestore.collection('users').doc('apple1').get();
expect(userDoc.exists, isTrue);
final householdId = userDoc.data()?['householdId'] as String?;
expect(householdId, isNotNull);
final household =
await firestore.collection('households').doc(householdId).get();
expect(household.data()?['ownerUid'], 'apple1');
expect(household.data()?['memberUids'], ['apple1']);
expect(
((household.data()?['members'] as Map<String, dynamic>)['apple1']
as Map<String, dynamic>)['role'],
'member');
// … und die App ist normal nutzbar (Heute-Screen, keine Aufgaben).
expect(find.text('Heute'), findsOneWidget);
expect(find.text('Alles versorgt!'), findsOneWidget);
});
testWidgets(
'users-Doc existiert ohne householdId (Push-Registrierung war schneller): '
'Haushalt wird nachgezogen, Push-Felder bleiben erhalten',
(tester) async {
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
final auth = MockFirebaseAuth(
signedIn: true,
mockUser: MockUser(uid: 'apple1', email: 'apple@example.com'),
);
final firestore = FakeFirebaseFirestore();
// Exakt der Zustand des kaputten Apple-Accounts: Die Push-Registrierung
// hat das users-Dokument angelegt, der Bootstrap kam nie durch.
await firestore.collection('users').doc('apple1').set({
'fcmTokens': ['token-1'],
'timezone': 'Europe/Berlin',
});
await tester.pumpWidget(ProviderScope(
overrides: [
sharedPreferencesProvider.overrideWithValue(prefs),
firebaseAuthProvider.overrideWithValue(auth),
firestoreProvider.overrideWithValue(firestore),
pushRegistrationServiceProvider
.overrideWithValue(_FakePushRegistrationService()),
],
child: const LeafItToMeApp(),
));
await tester.pumpAndSettle();
final userDoc = await firestore.collection('users').doc('apple1').get();
final householdId = userDoc.data()?['householdId'] as String?;
expect(householdId, isNotNull);
// Die Push-Felder wurden nicht überschrieben (merge statt set).
expect(userDoc.data()?['fcmTokens'], ['token-1']);
expect(userDoc.data()?['timezone'], 'Europe/Berlin');
final household =
await firestore.collection('households').doc(householdId).get();
expect(household.data()?['memberUids'], ['apple1']);
expect(find.text('Heute'), findsOneWidget);
});
testWidgets(
'Untersuchungshistorie: gespeicherte Diagnosen sind im Detail sichtbar',
(tester) async {
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
final auth = MockFirebaseAuth(
signedIn: true,
mockUser: MockUser(uid: 'u1', email: 'test@example.com'),
);
final firestore = FakeFirebaseFirestore();
await firestore.collection('users').doc('u1').set({
'email': 'test@example.com',
'householdId': 'h1',
});
await firestore.collection('households').doc('h1').set({
'name': 'Mein Haushalt',
'ownerUid': 'u1',
'memberUids': ['u1'],
'members': {
'u1': {'role': 'member', 'email': 'test@example.com'},
},
});
final plantRef =
firestore.collection('households').doc('h1').collection('plants');
await plantRef.doc('p1').set({
'nickname': 'Monstera',
'species': 'Monstera deliciosa',
'locationId': null,
'description': '',
'careNotes': '',
'wateringIntervalDays': 7,
'fertilizingIntervalDays': 28,
'lastWatered': Timestamp.fromDate(DateTime.now()),
'lastFertilized': Timestamp.fromDate(DateTime.now()),
});
await plantRef.doc('p1').collection('diagnoses').add({
'healthy': false,
'matchesSpecies': true,
'summary': 'Die Blätter zeigen braune Flecken.',
'details': 'Vermutlich zu viel direkte Sonne.',
'treatment': 'Stelle die Pflanze etwas schattiger.',
'prevention': 'Kein direktes Mittagslicht.',
'createdAt': Timestamp.fromDate(DateTime(2026, 7, 20, 14, 30)),
});
await tester.pumpWidget(ProviderScope(
overrides: [
sharedPreferencesProvider.overrideWithValue(prefs),
firebaseAuthProvider.overrideWithValue(auth),
firestoreProvider.overrideWithValue(firestore),
pushRegistrationServiceProvider
.overrideWithValue(_FakePushRegistrationService()),
],
child: const LeafItToMeApp(),
));
await tester.pumpAndSettle();
// Zur Pflanze navigieren: Menü → Meine Pflanzen → Monstera.
await tester.tap(find.byIcon(Icons.menu));
await tester.pumpAndSettle();
await tester.tap(find.text('Meine Pflanzen'));
await tester.pumpAndSettle();
await tester.tap(find.text('Monstera'));
await tester.pumpAndSettle();
// Historie ist sichtbar.
await tester.scrollUntilVisible(
find.text('Die Blätter zeigen braune Flecken.'), 200);
expect(find.text('Untersuchungen'), findsOneWidget);
expect(find.textContaining('20. Juli 2026'), findsOneWidget);
// Eintrag öffnen → volles Ergebnis-Sheet mit Behandlungstipps.
await tester.tap(find.text('Die Blätter zeigen braune Flecken.'));
await tester.pumpAndSettle();
expect(find.text('Was du tun kannst'), findsOneWidget);
expect(find.text('Stelle die Pflanze etwas schattiger.'), findsOneWidget);
});
testWidgets('Mitglied kann den Haushalt umbenennen', (tester) async {
await tester.pumpWidget(await buildTestApp());
await tester.pumpAndSettle();
await tester.tap(find.byIcon(Icons.menu));
await tester.pumpAndSettle();
await tester.tap(find.text('Haushalt'));
await tester.pumpAndSettle();
await tester.tap(find.byIcon(Icons.edit));
await tester.pumpAndSettle();
await tester.enterText(find.byType(TextField).last, 'Wohnung Chris');
await tester.tap(find.text('Speichern'));
await tester.pumpAndSettle();
// Der neue Name kommt live aus Firestore zurück (AppBar).
expect(find.text('Wohnung Chris'), findsOneWidget);
});
}