feat(03-02): rewrite HomeScreen with daily plan UI, completion animation, empty states, and tests

- Complete HomeScreen rewrite: progress card, overdue/today/tomorrow sections
- Animated task completion with SizeTransition + SlideTransition on checkbox tap
- "All clear" celebration state when all tasks done, "no tasks" state for first-run
- Room name tags navigate to room task list via context.go
- 6 widget tests covering empty, all-clear, normal state, overdue, tomorrow sections
- Fixed app_shell_test to override dailyPlanProvider for new HomeScreen dependency

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-16 12:39:04 +01:00
parent 4e3a3ed3c2
commit 444213ece1
3 changed files with 606 additions and 4 deletions

View File

@@ -0,0 +1,248 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:household_keeper/core/database/database.dart';
import 'package:household_keeper/core/router/router.dart';
import 'package:household_keeper/features/home/domain/daily_plan_models.dart';
import 'package:household_keeper/features/home/presentation/daily_plan_providers.dart';
import 'package:household_keeper/features/rooms/presentation/room_providers.dart';
import 'package:household_keeper/features/tasks/domain/effort_level.dart';
import 'package:household_keeper/features/tasks/domain/frequency.dart';
import 'package:household_keeper/l10n/app_localizations.dart';
/// Helper to create a test [Task] with sensible defaults.
Task _makeTask({
int id = 1,
int roomId = 1,
String name = 'Test Task',
required DateTime nextDueDate,
}) {
return Task(
id: id,
roomId: roomId,
name: name,
intervalType: IntervalType.weekly,
intervalDays: 7,
effortLevel: EffortLevel.medium,
nextDueDate: nextDueDate,
createdAt: DateTime(2026, 1, 1),
);
}
/// Helper to create a [TaskWithRoom].
TaskWithRoom _makeTaskWithRoom({
int id = 1,
int roomId = 1,
String taskName = 'Test Task',
String roomName = 'Kueche',
required DateTime nextDueDate,
}) {
return TaskWithRoom(
task: _makeTask(
id: id,
roomId: roomId,
name: taskName,
nextDueDate: nextDueDate,
),
roomName: roomName,
roomId: roomId,
);
}
/// Build the app with dailyPlanProvider overridden to the given state.
Widget _buildApp(DailyPlanState planState) {
return ProviderScope(
overrides: [
dailyPlanProvider.overrideWith(
(ref) => Stream.value(planState),
),
roomWithStatsListProvider.overrideWith(
(ref) => Stream.value([]),
),
],
child: MaterialApp.router(
routerConfig: router,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: const [Locale('de')],
locale: const Locale('de'),
),
);
}
void main() {
setUp(() {
SharedPreferences.setMockInitialValues({});
});
group('HomeScreen empty states', () {
testWidgets('shows no-tasks empty state when no tasks exist at all',
(tester) async {
await tester.pumpWidget(_buildApp(const DailyPlanState(
overdueTasks: [],
todayTasks: [],
tomorrowTasks: [],
completedTodayCount: 0,
totalTodayCount: 0,
)));
await tester.pumpAndSettle();
// Should show "Noch keine Aufgaben angelegt" (dailyPlanNoTasks)
expect(find.text('Noch keine Aufgaben angelegt'), findsOneWidget);
// Should show action button to create a room
expect(find.text('Raum erstellen'), findsOneWidget);
});
testWidgets('shows all-clear state when all tasks are done',
(tester) async {
await tester.pumpWidget(_buildApp(const DailyPlanState(
overdueTasks: [],
todayTasks: [],
tomorrowTasks: [],
completedTodayCount: 3,
totalTodayCount: 3,
)));
await tester.pumpAndSettle();
// Should show celebration empty state
expect(find.text('Alles erledigt! \u{1F31F}'), findsOneWidget);
expect(find.byIcon(Icons.celebration_outlined), findsOneWidget);
// Progress card should show 3/3
expect(find.text('3 von 3 erledigt'), findsOneWidget);
});
});
group('HomeScreen normal state', () {
testWidgets('shows progress card with correct counts', (tester) async {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
await tester.pumpWidget(_buildApp(DailyPlanState(
overdueTasks: [],
todayTasks: [
_makeTaskWithRoom(
id: 1,
taskName: 'Staubsaugen',
roomName: 'Wohnzimmer',
nextDueDate: today,
),
],
tomorrowTasks: [],
completedTodayCount: 2,
totalTodayCount: 3,
)));
await tester.pumpAndSettle();
// Progress card should show 2/3
expect(find.text('2 von 3 erledigt'), findsOneWidget);
expect(find.byType(LinearProgressIndicator), findsOneWidget);
});
testWidgets('shows overdue section when overdue tasks exist',
(tester) async {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final yesterday = today.subtract(const Duration(days: 1));
await tester.pumpWidget(_buildApp(DailyPlanState(
overdueTasks: [
_makeTaskWithRoom(
id: 1,
taskName: 'Boden wischen',
roomName: 'Kueche',
nextDueDate: yesterday,
),
],
todayTasks: [
_makeTaskWithRoom(
id: 2,
taskName: 'Staubsaugen',
roomName: 'Wohnzimmer',
nextDueDate: today,
),
],
tomorrowTasks: [],
completedTodayCount: 0,
totalTodayCount: 2,
)));
await tester.pumpAndSettle();
// Should show overdue section header
expect(find.text('\u00dcberf\u00e4llig'), findsOneWidget);
// Should show today section header (may also appear as relative date)
expect(find.text('Heute'), findsAtLeast(1));
// Should show both tasks
expect(find.text('Boden wischen'), findsOneWidget);
expect(find.text('Staubsaugen'), findsOneWidget);
// Should show room name tags
expect(find.text('Kueche'), findsOneWidget);
expect(find.text('Wohnzimmer'), findsOneWidget);
});
testWidgets('shows collapsed tomorrow section with count',
(tester) async {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final tomorrow = today.add(const Duration(days: 1));
await tester.pumpWidget(_buildApp(DailyPlanState(
overdueTasks: [],
todayTasks: [
_makeTaskWithRoom(
id: 1,
taskName: 'Staubsaugen',
roomName: 'Wohnzimmer',
nextDueDate: today,
),
],
tomorrowTasks: [
_makeTaskWithRoom(
id: 2,
taskName: 'Fenster putzen',
roomName: 'Schlafzimmer',
nextDueDate: tomorrow,
),
_makeTaskWithRoom(
id: 3,
taskName: 'Bett beziehen',
roomName: 'Schlafzimmer',
nextDueDate: tomorrow,
),
],
completedTodayCount: 0,
totalTodayCount: 1,
)));
await tester.pumpAndSettle();
// Should show collapsed tomorrow section with count
expect(find.text('Demn\u00e4chst (2)'), findsOneWidget);
// Tomorrow tasks should NOT be visible (collapsed by default)
expect(find.text('Fenster putzen'), findsNothing);
});
testWidgets('today tasks have checkboxes', (tester) async {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
await tester.pumpWidget(_buildApp(DailyPlanState(
overdueTasks: [],
todayTasks: [
_makeTaskWithRoom(
id: 1,
taskName: 'Staubsaugen',
roomName: 'Wohnzimmer',
nextDueDate: today,
),
],
tomorrowTasks: [],
completedTodayCount: 0,
totalTodayCount: 1,
)));
await tester.pumpAndSettle();
// Today task should have a checkbox
expect(find.byType(Checkbox), findsOneWidget);
});
});
}

View File

@@ -4,6 +4,8 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:household_keeper/core/router/router.dart';
import 'package:household_keeper/features/home/domain/daily_plan_models.dart';
import 'package:household_keeper/features/home/presentation/daily_plan_providers.dart';
import 'package:household_keeper/features/rooms/presentation/room_providers.dart';
import 'package:household_keeper/l10n/app_localizations.dart';
@@ -13,7 +15,7 @@ void main() {
SharedPreferences.setMockInitialValues({});
});
/// Helper to build the app with room provider overridden to empty list.
/// Helper to build the app with providers overridden for testing.
Widget buildApp() {
return ProviderScope(
overrides: [
@@ -22,6 +24,17 @@ void main() {
roomWithStatsListProvider.overrideWith(
(ref) => Stream.value([]),
),
// Override daily plan to return empty state so HomeScreen
// renders without a database.
dailyPlanProvider.overrideWith(
(ref) => Stream.value(const DailyPlanState(
overdueTasks: [],
todayTasks: [],
tomorrowTasks: [],
completedTodayCount: 0,
totalTodayCount: 0,
)),
),
],
child: MaterialApp.router(
routerConfig: router,
@@ -52,7 +65,8 @@ void main() {
await tester.pumpAndSettle();
// Initially on Home tab (index 0) -- verify home empty state is shown
expect(find.text('Noch nichts zu tun!'), findsOneWidget);
// (dailyPlanNoTasks text from the daily plan empty state)
expect(find.text('Noch keine Aufgaben angelegt'), findsOneWidget);
// Tap the Rooms tab (second destination)
await tester.tap(find.text('R\u00e4ume'));