leafittome/test/widget_test.dart
cschlaefke e81ce48543 V3: Nächstes Fälligkeitsdatum bei Gießen/Düngen/Umtopfen in Pflanzen-Detail
Nutzer vermisste beim Umtopf-Test eine Info, wann das nächste Umtopfen
ansteht (Formular/Checkliste zeigten nur "alle X Monate"/"zuletzt am").
Berechnung aus due_tasks_provider public gemacht statt dupliziert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 15:13:04 +02:00

798 lines
29 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-Button ist aktiv (Untersuchungen vorhanden) → Seite öffnen.
await tester.scrollUntilVisible(find.text('Frühere Untersuchungen'), 200);
await tester.tap(find.text('Frühere Untersuchungen'));
await tester.pumpAndSettle();
// Untersuchungs-Seite listet den Eintrag.
expect(find.textContaining('Untersuchungen'), findsWidgets);
expect(find.text('Die Blätter zeigen braune Flecken.'), 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(
'Ohne gespeicherte Untersuchungen ist der Historie-Button deaktiviert',
(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('Meine Pflanzen'));
await tester.pumpAndSettle();
await tester.tap(find.text('Monstera'));
await tester.pumpAndSettle();
await tester.scrollUntilVisible(find.text('Frühere Untersuchungen'), 200);
final button = tester.widget<OutlinedButton>(find.ancestor(
of: find.text('Frühere Untersuchungen'),
matching: find.byType(OutlinedButton),
));
expect(button.onPressed, isNull);
});
testWidgets(
'Stellplatz-Bewertung: Standort-Analyse und aktuelle Eignungs-Sterne '
'sind sichtbar, auch verlinkt im Pflanzen-Detail', (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 analyzedAt = DateTime(2026, 7, 20, 10, 0);
await firestore
.collection('households')
.doc('h1')
.collection('locations')
.doc('loc1')
.set({
'name': 'Fensterbank',
'lightCategory': 'Hell, indirekt',
'lightAssessment': 'Viel indirektes Licht, keine direkte Sonne.',
'analyzedAt': Timestamp.fromDate(analyzedAt),
});
await firestore
.collection('households')
.doc('h1')
.collection('plants')
.doc('p1')
.set({
'nickname': 'Monstera',
'species': 'Monstera deliciosa',
'locationId': 'loc1',
'description': '',
'careNotes': '',
'wateringIntervalDays': 7,
'fertilizingIntervalDays': 28,
'lastWatered': Timestamp.fromDate(DateTime.now()),
'lastFertilized': Timestamp.fromDate(DateTime.now()),
'fitStars': 4,
'fitReasoning': 'Passt gut zu diesem Standort.',
'fitLocationId': 'loc1',
// Nach der letzten Analyse bewertet → nicht veraltet.
'fitAssessedAt':
Timestamp.fromDate(analyzedAt.add(const Duration(minutes: 5))),
});
await tester.pumpWidget(ProviderScope(
overrides: [
sharedPreferencesProvider.overrideWithValue(prefs),
firebaseAuthProvider.overrideWithValue(auth),
firestoreProvider.overrideWithValue(firestore),
pushRegistrationServiceProvider
.overrideWithValue(_FakePushRegistrationService()),
],
child: const LeafItToMeApp(),
));
await tester.pumpAndSettle();
// Pflanzen-Detail zeigt den Stellplatz samt aktuellen Sternen kompakt an.
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();
expect(find.textContaining('Fensterbank · ★★★★☆'), findsOneWidget);
// Nächste Fälligkeit wird zusätzlich zum letzten Datum angezeigt.
expect(find.textContaining('Nächstes Gießen:'), findsOneWidget);
expect(find.textContaining('Nächstes Düngen:'), findsOneWidget);
// Antippen navigiert zur Stellplatz-Detailseite mit voller Analyse.
await tester.tap(find.textContaining('Fensterbank · ★★★★☆'));
await tester.pumpAndSettle();
expect(find.text('Fensterbank'), findsWidgets);
expect(find.text('Hell, indirekt'), findsOneWidget);
expect(find.text('Viel indirektes Licht, keine direkte Sonne.'),
findsOneWidget);
expect(find.text('Zugeordnete Pflanzen'), findsOneWidget);
expect(find.text('★★★★☆'), findsOneWidget);
expect(find.text('Passt gut zu diesem Standort.'), findsOneWidget);
});
testWidgets(
'Stellplatz-Bewertung: veraltete Eignung wird nach neuer Analyse '
'als solche markiert statt der alten Sterne', (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 analyzedAt = DateTime(2026, 7, 20, 10, 0);
await firestore
.collection('households')
.doc('h1')
.collection('locations')
.doc('loc1')
.set({
'name': 'Fensterbank',
'lightCategory': 'Hell, indirekt',
'lightAssessment': 'Viel indirektes Licht, keine direkte Sonne.',
'analyzedAt': Timestamp.fromDate(analyzedAt),
});
await firestore
.collection('households')
.doc('h1')
.collection('plants')
.doc('p1')
.set({
'nickname': 'Monstera',
'species': 'Monstera deliciosa',
'locationId': 'loc1',
'description': '',
'careNotes': '',
'wateringIntervalDays': 7,
'fertilizingIntervalDays': 28,
'lastWatered': Timestamp.fromDate(DateTime.now()),
'lastFertilized': Timestamp.fromDate(DateTime.now()),
'fitStars': 3,
'fitReasoning': 'Alte Begründung.',
'fitLocationId': 'loc1',
// Vor der letzten Analyse bewertet → gilt als veraltet.
'fitAssessedAt':
Timestamp.fromDate(analyzedAt.subtract(const Duration(days: 1))),
});
await tester.pumpWidget(ProviderScope(
overrides: [
sharedPreferencesProvider.overrideWithValue(prefs),
firebaseAuthProvider.overrideWithValue(auth),
firestoreProvider.overrideWithValue(firestore),
pushRegistrationServiceProvider
.overrideWithValue(_FakePushRegistrationService()),
],
child: const LeafItToMeApp(),
));
await tester.pumpAndSettle();
await tester.tap(find.byIcon(Icons.menu));
await tester.pumpAndSettle();
await tester.tap(find.text('Stellplätze'));
await tester.pumpAndSettle();
await tester.tap(find.text('Fensterbank'));
await tester.pumpAndSettle();
expect(find.text('★★★☆☆'), findsNothing);
expect(find.text('Stellplatz hat sich geändert neu prüfen'),
findsOneWidget);
});
testWidgets(
'Stellplatz-Bewertung: ohne Standort-Analyse ist der '
'Eignungs-Check deaktiviert', (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'},
},
});
await firestore
.collection('households')
.doc('h1')
.collection('locations')
.doc('loc1')
.set({'name': 'Fensterbank'});
await firestore
.collection('households')
.doc('h1')
.collection('plants')
.doc('p1')
.set({
'nickname': 'Monstera',
'species': 'Monstera deliciosa',
'locationId': 'loc1',
'description': '',
'careNotes': '',
'wateringIntervalDays': 7,
'fertilizingIntervalDays': 28,
'lastWatered': Timestamp.fromDate(DateTime.now()),
'lastFertilized': Timestamp.fromDate(DateTime.now()),
});
await tester.pumpWidget(ProviderScope(
overrides: [
sharedPreferencesProvider.overrideWithValue(prefs),
firebaseAuthProvider.overrideWithValue(auth),
firestoreProvider.overrideWithValue(firestore),
pushRegistrationServiceProvider
.overrideWithValue(_FakePushRegistrationService()),
],
child: const LeafItToMeApp(),
));
await tester.pumpAndSettle();
await tester.tap(find.byIcon(Icons.menu));
await tester.pumpAndSettle();
await tester.tap(find.text('Stellplätze'));
await tester.pumpAndSettle();
await tester.tap(find.text('Fensterbank'));
await tester.pumpAndSettle();
expect(
find.text(
'Noch keine Analyse. Foto aufnehmen, damit die KI die '
'Lichtverhältnisse einschätzen kann.'),
findsOneWidget);
final fitButton = tester.widget<OutlinedButton>(find.ancestor(
of: find.text('Erst den Stellplatz analysieren (Foto oben)'),
matching: find.byType(OutlinedButton),
));
expect(fitButton.onPressed, isNull);
});
testWidgets(
'Umtopf-Erinnerung: fällige Aufgabe erscheint in der Checkliste und '
'Bestätigen schreibt lastRepotted', (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 now = DateTime.now();
// Gießen/Düngen frisch erledigt — nur das Umtopfen (vor 13 Monaten,
// Intervall 12 Monate) ist überfällig.
await firestore
.collection('households')
.doc('h1')
.collection('plants')
.doc('p1')
.set({
'nickname': 'Monstera',
'species': 'Monstera deliciosa',
'locationId': null,
'description': '',
'careNotes': '',
'wateringIntervalDays': 7,
'fertilizingIntervalDays': 28,
'lastWatered': Timestamp.fromDate(now),
'lastFertilized': Timestamp.fromDate(now),
'repottingIntervalMonths': 12,
'lastRepotted': Timestamp.fromDate(
DateTime(now.year - 1, now.month - 1, now.day)),
});
await tester.pumpWidget(ProviderScope(
overrides: [
sharedPreferencesProvider.overrideWithValue(prefs),
firebaseAuthProvider.overrideWithValue(auth),
firestoreProvider.overrideWithValue(firestore),
pushRegistrationServiceProvider
.overrideWithValue(_FakePushRegistrationService()),
],
child: const LeafItToMeApp(),
));
await tester.pumpAndSettle();
expect(find.textContaining('Monstera umtopfen'), findsOneWidget);
await tester.tap(find.text('Erledigt'));
await tester.pumpAndSettle();
expect(find.text('Alles versorgt!'), findsOneWidget);
final doc = await firestore
.collection('households')
.doc('h1')
.collection('plants')
.doc('p1')
.get();
final lastRepotted = (doc.data()?['lastRepotted'] as Timestamp).toDate();
expect(now.difference(lastRepotted).inDays, 0);
expect(doc.data()?['lastRepottedBy'], 'test');
// Detailseite zeigt jetzt zusätzlich die nächste Fälligkeit an.
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();
expect(find.textContaining('Nächstes Umtopfen:'), 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);
});
}