- ownerUid je Haushalt (Alt-Haushalte: erster memberUids-Eintrag = Ersteller); Besitzer-Anzeige in Mitglieder- und Haushaltsliste - Haushalt umbenennen (Dialog im Haushalts-Screen); Rules erlauben Clients am Haushalts-Dokument nur noch das Namensfeld - Cloud Functions leaveHousehold (selbst austreten, auch als Sitter) und removeMember (nur Besitzer); beide biegen den aktiven Zeiger des Betroffenen um und legen notfalls einen frischen eigenen Haushalt an - Haushalts-Wechsler im Drawer (PopupMenu, ab zwei Haushalten) - Tages-Checkliste und nächste Fälligkeit aggregieren über alle Haushalte, mit Zwischenüberschrift je Haushalt; Bestätigen schreibt in den Herkunfts-Haushalt der Aufgabe - Widget-Tests: Gruppierung, Besitzer-Rechte, Umbenennen Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
237 lines
8.6 KiB
Dart
237 lines
8.6 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('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);
|
||
});
|
||
}
|