diff --git a/docs/handoff.md b/docs/handoff.md index e08cef8..f74522b 100644 --- a/docs/handoff.md +++ b/docs/handoff.md @@ -36,9 +36,13 @@ Private Flutter-App (iOS+Android) zur Pflanzenpflege für Haushalt + Pflanzen-Si **Stand nach erstem Gerätetest (2026-07-23):** Deploy erfolgreich, Grundfunktion vom Nutzer bestätigt. Drei Nachbesserungen aus dem Test umgesetzt (Commit `b665bae`): (1) `speciesHint` ist jetzt eine unbestätigte Angabe — Claude prüft selbst, welche Pflanze zu sehen ist, beurteilt bei Abweichung die Pflanze im Bild und meldet `matchesSpecies=false`, worauf die App einen Warnhinweis im Ergebnis-Sheet zeigt; (2) Prompt verbietet HTML/Markdown (Nutzer hatte ein `
` im Text); (3) durchgängiges Duzen vorgeschrieben (war gemischt). +**Feinschliff deployt und bestätigt** („sieht sehr gut aus", 2026-07-23). + +**Erweiterung Untersuchungshistorie (2026-07-23):** Jede Diagnose wird automatisch unter `households/{id}/plants/{id}/diagnoses` gespeichert (healthy, matchesSpecies, Texte, `createdAt` Server-Timestamp). Im Pflanzen-Detail unter dem Untersuchen-Button: Liste „Untersuchungen" (neueste zuerst, Ampel-Icon + Kurzbefund + Datum), Tipp öffnet dasselbe Ergebnis-Sheet inkl. „Untersucht am …". Volle Mitglieder können Einträge löschen (Bestätigungsdialog); Sitter dürfen speichern/lesen, Einträge sind nachträglich unveränderbar (Rules). Neuer Widget-Test navigiert bis ins Sheet — alle 11 Tests grün. + **Noch zu tun (Nutzer):** -1. **Re-Deploy** der nachgebesserten Function (Berechtigungsfilter blockiert Deploys in der Session): `firebase deploy --only functions:diagnosePlant --project leaf-it-to-me-app` -2. Nachtest: falsches Pflanzen-Foto → Warnhinweis „zeigt offenbar eine andere Pflanze"; Texte ohne HTML, durchgängig „du". +1. **Rules-Deploy** (neue diagnoses-Regeln; ohne sie schlägt das Speichern der Historie still fehl): `firebase deploy --only firestore --project leaf-it-to-me-app` +2. App neu bauen, Untersuchung durchführen → Eintrag erscheint in „Untersuchungen", antippen zeigt den vollen Text, Löschen fragt nach. **Danach V3 weiter:** Stellplatz-Bewertung (Zwei-Ebenen-Konzept), Umtopf-Erinnerungen. diff --git a/firestore.rules b/firestore.rules index 45d749b..fcf10b3 100644 --- a/firestore.rules +++ b/firestore.rules @@ -90,6 +90,16 @@ service cloud.firestore { || request.resource.data.diff(resource.data).affectedKeys() .hasOnly(['lastWatered', 'lastWateredBy', 'lastFertilized', 'lastFertilizedBy'])); + + // Untersuchungshistorie (KI-Diagnosen). Speichern und lesen darf + // jede*r im Haushalt (auch Sitter — die dürfen den Check nutzen); + // löschen nur volle Mitglieder, nachträglich ändern niemand. + match /diagnoses/{diagnosisId} { + allow read, create: if isMember(householdId); + allow delete: if isMember(householdId) + && roleOf(householdId) == 'member'; + allow update: if false; + } } match /locations/{locationId} { diff --git a/lib/features/plants/data/plant_diagnosis_service.dart b/lib/features/plants/data/plant_diagnosis_service.dart index aaf87ab..57afc6a 100644 --- a/lib/features/plants/data/plant_diagnosis_service.dart +++ b/lib/features/plants/data/plant_diagnosis_service.dart @@ -1,9 +1,13 @@ import 'dart:convert'; import 'dart:typed_data'; +import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_functions/cloud_functions.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../core/firebase/firebase_providers.dart'; +import '../../household/data/household_providers.dart'; + /// Ergebnis der KI-Krankheits-Diagnose (Cloud Function diagnosePlant). class DiagnosisResult { const DiagnosisResult({ @@ -27,12 +31,47 @@ class DiagnosisResult { final String prevention; } +/// Ein gespeicherter Eintrag der Untersuchungshistorie +/// (households/{id}/plants/{id}/diagnoses). +class PlantDiagnosisEntry { + const PlantDiagnosisEntry({ + required this.id, + required this.createdAt, + required this.result, + }); + + final String id; + + /// null, solange der Server-Zeitstempel noch nicht bestätigt ist. + final DateTime? createdAt; + + final DiagnosisResult result; +} + class PlantDiagnosisService { - PlantDiagnosisService(this._functionsGetter); + PlantDiagnosisService( + this._functionsGetter, + this._firestore, + this._householdIdGetter, + ); // Lazy, damit Widget-Tests ohne initialisiertes Firebase auskommen — // FirebaseFunctions.instanceFor wirft sonst schon beim Erzeugen. final FirebaseFunctions Function() _functionsGetter; + final FirebaseFirestore _firestore; + final String? Function() _householdIdGetter; + + CollectionReference> _diagnosesRef( + String householdId, + String plantId, + ) { + return _firestore + .collection('households') + .doc(householdId) + .collection('plants') + .doc(plantId) + .collection('diagnoses'); + } /// Schickt das Foto an die Cloud Function (Claude) – dauert einige Sekunden. /// [speciesHint] ist die bekannte Art der Pflanze und hilft der Diagnose. @@ -56,10 +95,67 @@ class PlantDiagnosisService { prevention: data['prevention'] as String? ?? '', ); } + + /// Legt einen Eintrag in der Untersuchungshistorie der Pflanze an. + Future saveDiagnosis(String plantId, DiagnosisResult result) async { + final householdId = _householdIdGetter(); + if (householdId == null) return; + await _diagnosesRef(householdId, plantId).add({ + 'healthy': result.healthy, + 'matchesSpecies': result.matchesSpecies, + 'summary': result.summary, + 'details': result.details, + 'treatment': result.treatment, + 'prevention': result.prevention, + 'createdAt': FieldValue.serverTimestamp(), + }); + } + + /// Entfernt einen Eintrag aus der Historie (nur volle Mitglieder). + Future deleteDiagnosis(String plantId, String diagnosisId) async { + final householdId = _householdIdGetter(); + if (householdId == null) return; + await _diagnosesRef(householdId, plantId).doc(diagnosisId).delete(); + } } final plantDiagnosisServiceProvider = Provider((ref) { return PlantDiagnosisService( () => FirebaseFunctions.instanceFor(region: 'europe-west3'), + ref.watch(firestoreProvider), + () => ref.read(householdIdProvider).value, ); }); + +/// Untersuchungshistorie einer Pflanze, neueste zuerst, live aus Firestore. +final plantDiagnosesProvider = + StreamProvider.family, String>((ref, plantId) { + final householdId = ref.watch(householdIdProvider).value; + if (householdId == null) return Stream.value(const []); + + return ref + .watch(firestoreProvider) + .collection('households') + .doc(householdId) + .collection('plants') + .doc(plantId) + .collection('diagnoses') + .orderBy('createdAt', descending: true) + .snapshots() + .map((snapshot) => [ + for (final doc in snapshot.docs) + PlantDiagnosisEntry( + id: doc.id, + createdAt: (doc.data()['createdAt'] as Timestamp?)?.toDate(), + result: DiagnosisResult( + healthy: doc.data()['healthy'] as bool? ?? false, + matchesSpecies: + doc.data()['matchesSpecies'] as bool? ?? true, + summary: doc.data()['summary'] as String? ?? '', + details: doc.data()['details'] as String? ?? '', + treatment: doc.data()['treatment'] as String? ?? '', + prevention: doc.data()['prevention'] as String? ?? '', + ), + ), + ]); +}); diff --git a/lib/features/plants/presentation/plant_detail_screen.dart b/lib/features/plants/presentation/plant_detail_screen.dart index 5fe1e3f..dbaa5eb 100644 --- a/lib/features/plants/presentation/plant_detail_screen.dart +++ b/lib/features/plants/presentation/plant_detail_screen.dart @@ -150,12 +150,231 @@ class PlantDetailScreen extends ConsumerWidget { ], const Divider(height: 32), _DiagnosisSection(plant: plant), + _DiagnosisHistory(plant: plant), ], ), ); } } +/// Zeigt das Diagnose-Ergebnis als scrollbares Bottom-Sheet — sowohl für +/// eine frische Untersuchung als auch für Einträge aus der Historie +/// ([createdAt] gesetzt). +Future _showDiagnosisSheet( + BuildContext context, { + required DiagnosisResult result, + required String plantNickname, + DateTime? createdAt, +}) { + final l10n = AppLocalizations.of(context); + final locale = Localizations.localeOf(context).toString(); + return showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (sheetContext) { + final theme = Theme.of(sheetContext); + final healthyColor = theme.brightness == Brightness.dark + ? Colors.green.shade300 + : Colors.green.shade700; + return DraggableScrollableSheet( + expand: false, + initialChildSize: 0.6, + maxChildSize: 0.95, + builder: (context, scrollController) => ListView( + controller: scrollController, + padding: const EdgeInsets.all(24), + children: [ + if (!result.matchesSpecies) ...[ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: theme.colorScheme.errorContainer, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.swap_horiz, + color: theme.colorScheme.onErrorContainer, + ), + const SizedBox(width: 12), + Expanded( + child: Text( + l10n.diagnosisSpeciesMismatch(plantNickname), + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onErrorContainer, + ), + ), + ), + ], + ), + ), + const SizedBox(height: 16), + ], + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + result.healthy + ? Icons.check_circle + : Icons.warning_amber_rounded, + size: 32, + color: + result.healthy ? healthyColor : theme.colorScheme.error, + ), + const SizedBox(width: 12), + Expanded( + child: Text( + result.summary, + style: theme.textTheme.titleLarge, + ), + ), + ], + ), + if (createdAt != null) ...[ + const SizedBox(height: 8), + Text( + l10n.diagnosisDate( + DateFormat('d. MMMM y, HH:mm', locale).format(createdAt), + ), + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], + if (result.details.isNotEmpty) ...[ + const SizedBox(height: 16), + Text(result.details, style: theme.textTheme.bodyLarge), + ], + if (result.treatment.isNotEmpty) ...[ + const SizedBox(height: 20), + Text( + l10n.diagnosisTreatmentTitle, + style: theme.textTheme.titleMedium, + ), + const SizedBox(height: 4), + Text(result.treatment, style: theme.textTheme.bodyLarge), + ], + if (result.prevention.isNotEmpty) ...[ + const SizedBox(height: 20), + Text( + l10n.diagnosisPreventionTitle, + style: theme.textTheme.titleMedium, + ), + const SizedBox(height: 4), + Text(result.prevention, style: theme.textTheme.bodyLarge), + ], + const SizedBox(height: 16), + Text( + l10n.diagnosisDisclaimer, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ); + }, + ); +} + +/// Liste der gespeicherten Untersuchungen dieser Pflanze (neueste zuerst). +class _DiagnosisHistory extends ConsumerWidget { + const _DiagnosisHistory({required this.plant}); + + final Plant plant; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final l10n = AppLocalizations.of(context); + final theme = Theme.of(context); + final entries = ref.watch(plantDiagnosesProvider(plant.id)).value ?? []; + if (entries.isEmpty) return const SizedBox.shrink(); + + final canEdit = ref.watch(myRoleProvider) == HouseholdRole.member; + final locale = Localizations.localeOf(context).toString(); + final dateFormat = DateFormat('d. MMMM y, HH:mm', locale); + final healthyColor = theme.brightness == Brightness.dark + ? Colors.green.shade300 + : Colors.green.shade700; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 24), + Text(l10n.diagnosisHistoryTitle, style: theme.textTheme.titleMedium), + for (final entry in entries) + ListTile( + contentPadding: EdgeInsets.zero, + leading: Icon( + entry.result.healthy + ? Icons.check_circle + : Icons.warning_amber_rounded, + color: entry.result.healthy + ? healthyColor + : theme.colorScheme.error, + ), + title: Text( + entry.result.summary, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + subtitle: entry.createdAt != null + ? Text(dateFormat.format(entry.createdAt!)) + : null, + trailing: canEdit + ? IconButton( + icon: const Icon(Icons.delete_outline), + tooltip: l10n.delete, + onPressed: () => _confirmDelete(context, ref, entry), + ) + : null, + onTap: () => _showDiagnosisSheet( + context, + result: entry.result, + plantNickname: plant.nickname, + createdAt: entry.createdAt, + ), + ), + ], + ); + } + + Future _confirmDelete( + BuildContext context, + WidgetRef ref, + PlantDiagnosisEntry entry, + ) async { + final l10n = AppLocalizations.of(context); + final locale = Localizations.localeOf(context).toString(); + final dateText = entry.createdAt != null + ? DateFormat('d. MMMM y', locale).format(entry.createdAt!) + : ''; + final confirmed = await showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: Text(l10n.deleteDiagnosisQuestion(dateText)), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialogContext, false), + child: Text(l10n.cancel), + ), + FilledButton( + onPressed: () => Navigator.pop(dialogContext, true), + child: Text(l10n.delete), + ), + ], + ), + ); + if (confirmed == true) { + await ref + .read(plantDiagnosisServiceProvider) + .deleteDiagnosis(plant.id, entry.id); + } + } +} + /// Krankheits-Check (V3): Foto aufnehmen → Claude beurteilt den Zustand /// und gibt Behandlungstipps. Auch für Sitter nutzbar (nur lesend). class _DiagnosisSection extends ConsumerStatefulWidget { @@ -205,12 +424,24 @@ class _DiagnosisSectionState extends ConsumerState<_DiagnosisSection> { setState(() => _busy = true); try { - final result = await ref.read(plantDiagnosisServiceProvider).diagnose( - bytes, - speciesHint: widget.plant.species, - ); + final service = ref.read(plantDiagnosisServiceProvider); + final result = await service.diagnose( + bytes, + speciesHint: widget.plant.species, + ); + // In der Historie ablegen — ein Speicherfehler soll die Anzeige des + // Ergebnisses aber nicht verhindern. + try { + await service.saveDiagnosis(widget.plant.id, result); + } catch (error) { + debugPrint('Untersuchung speichern fehlgeschlagen: $error'); + } if (!mounted) return; - await _showResult(result); + await _showDiagnosisSheet( + context, + result: result, + plantNickname: widget.plant.nickname, + ); } on FirebaseFunctionsException catch (e) { if (!mounted) return; // Die Function liefert deutsche Meldungen (z. B. „keine Pflanze @@ -230,108 +461,6 @@ class _DiagnosisSectionState extends ConsumerState<_DiagnosisSection> { } } - Future _showResult(DiagnosisResult result) { - final l10n = AppLocalizations.of(context); - return showModalBottomSheet( - context: context, - isScrollControlled: true, - builder: (sheetContext) { - final theme = Theme.of(sheetContext); - final healthyColor = theme.brightness == Brightness.dark - ? Colors.green.shade300 - : Colors.green.shade700; - return DraggableScrollableSheet( - expand: false, - initialChildSize: 0.6, - maxChildSize: 0.95, - builder: (context, scrollController) => ListView( - controller: scrollController, - padding: const EdgeInsets.all(24), - children: [ - if (!result.matchesSpecies) ...[ - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: theme.colorScheme.errorContainer, - borderRadius: BorderRadius.circular(12), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon( - Icons.swap_horiz, - color: theme.colorScheme.onErrorContainer, - ), - const SizedBox(width: 12), - Expanded( - child: Text( - l10n.diagnosisSpeciesMismatch(widget.plant.nickname), - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onErrorContainer, - ), - ), - ), - ], - ), - ), - const SizedBox(height: 16), - ], - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon( - result.healthy - ? Icons.check_circle - : Icons.warning_amber_rounded, - size: 32, - color: result.healthy - ? healthyColor - : theme.colorScheme.error, - ), - const SizedBox(width: 12), - Expanded( - child: Text( - result.summary, - style: theme.textTheme.titleLarge, - ), - ), - ], - ), - if (result.details.isNotEmpty) ...[ - const SizedBox(height: 16), - Text(result.details, style: theme.textTheme.bodyLarge), - ], - if (result.treatment.isNotEmpty) ...[ - const SizedBox(height: 20), - Text( - l10n.diagnosisTreatmentTitle, - style: theme.textTheme.titleMedium, - ), - const SizedBox(height: 4), - Text(result.treatment, style: theme.textTheme.bodyLarge), - ], - if (result.prevention.isNotEmpty) ...[ - const SizedBox(height: 20), - Text( - l10n.diagnosisPreventionTitle, - style: theme.textTheme.titleMedium, - ), - const SizedBox(height: 4), - Text(result.prevention, style: theme.textTheme.bodyLarge), - ], - const SizedBox(height: 16), - Text( - l10n.diagnosisDisclaimer, - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ); - }, - ); - } @override Widget build(BuildContext context) { diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index aa5ba5b..d919182 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -240,6 +240,19 @@ } }, "diagnosisFailed": "Die Untersuchung hat nicht geklappt. Bitte versuche es erneut.", + "diagnosisHistoryTitle": "Untersuchungen", + "diagnosisDate": "Untersucht am {date}", + "@diagnosisDate": { + "placeholders": { + "date": { "type": "String" } + } + }, + "deleteDiagnosisQuestion": "Untersuchung vom {date} löschen?", + "@deleteDiagnosisQuestion": { + "placeholders": { + "date": { "type": "String" } + } + }, "saveError": "Speichern fehlgeschlagen. Bitte versuche es erneut.", "saveErrorDetail": "Speichern fehlgeschlagen ({detail}). Bitte versuche es erneut.", "@saveErrorDetail": { diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 0d46777..9f80b6a 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -874,6 +874,24 @@ abstract class AppLocalizations { /// **'Die Untersuchung hat nicht geklappt. Bitte versuche es erneut.'** String get diagnosisFailed; + /// No description provided for @diagnosisHistoryTitle. + /// + /// In de, this message translates to: + /// **'Untersuchungen'** + String get diagnosisHistoryTitle; + + /// No description provided for @diagnosisDate. + /// + /// In de, this message translates to: + /// **'Untersucht am {date}'** + String diagnosisDate(String date); + + /// No description provided for @deleteDiagnosisQuestion. + /// + /// In de, this message translates to: + /// **'Untersuchung vom {date} löschen?'** + String deleteDiagnosisQuestion(String date); + /// No description provided for @saveError. /// /// In de, this message translates to: diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index 8d683af..c7ef0a0 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -478,6 +478,19 @@ class AppLocalizationsDe extends AppLocalizations { String get diagnosisFailed => 'Die Untersuchung hat nicht geklappt. Bitte versuche es erneut.'; + @override + String get diagnosisHistoryTitle => 'Untersuchungen'; + + @override + String diagnosisDate(String date) { + return 'Untersucht am $date'; + } + + @override + String deleteDiagnosisQuestion(String date) { + return 'Untersuchung vom $date löschen?'; + } + @override String get saveError => 'Speichern fehlgeschlagen. Bitte versuche es erneut.'; diff --git a/test/widget_test.dart b/test/widget_test.dart index d3041d1..c7dd011 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -341,6 +341,84 @@ void main() { 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();