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>
798 lines
29 KiB
Dart
798 lines
29 KiB
Dart
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);
|
||
});
|
||
}
|