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:
@@ -1,16 +1,103 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_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/home/presentation/daily_plan_task_row.dart';
|
||||||
|
import 'package:household_keeper/features/home/presentation/progress_card.dart';
|
||||||
|
import 'package:household_keeper/features/tasks/presentation/task_providers.dart';
|
||||||
import 'package:household_keeper/l10n/app_localizations.dart';
|
import 'package:household_keeper/l10n/app_localizations.dart';
|
||||||
|
|
||||||
class HomeScreen extends StatelessWidget {
|
/// Warm coral/terracotta color for overdue section header.
|
||||||
|
const _overdueColor = Color(0xFFE07A5F);
|
||||||
|
|
||||||
|
/// The app's primary screen: daily plan showing what's due today,
|
||||||
|
/// overdue tasks, and a preview of tomorrow.
|
||||||
|
///
|
||||||
|
/// Replaces the former placeholder with a full daily workflow:
|
||||||
|
/// see what's due, check it off, feel progress.
|
||||||
|
class HomeScreen extends ConsumerStatefulWidget {
|
||||||
const HomeScreen({super.key});
|
const HomeScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<HomeScreen> createState() => _HomeScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HomeScreenState extends ConsumerState<HomeScreen> {
|
||||||
|
/// Task IDs currently animating out after completion.
|
||||||
|
final Set<int> _completingTaskIds = {};
|
||||||
|
|
||||||
|
void _onTaskCompleted(int taskId) {
|
||||||
|
setState(() {
|
||||||
|
_completingTaskIds.add(taskId);
|
||||||
|
});
|
||||||
|
ref.read(taskActionsProvider.notifier).completeTask(taskId);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context);
|
final l10n = AppLocalizations.of(context);
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final dailyPlan = ref.watch(dailyPlanProvider);
|
||||||
|
|
||||||
|
return dailyPlan.when(
|
||||||
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
|
error: (error, _) => Center(child: Text(error.toString())),
|
||||||
|
data: (state) {
|
||||||
|
// Clean up completing IDs that are no longer in the data
|
||||||
|
_completingTaskIds.removeWhere((id) =>
|
||||||
|
!state.overdueTasks.any((t) => t.task.id == id) &&
|
||||||
|
!state.todayTasks.any((t) => t.task.id == id));
|
||||||
|
|
||||||
|
return _buildDailyPlan(context, state, l10n, theme);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDailyPlan(
|
||||||
|
BuildContext context,
|
||||||
|
DailyPlanState state,
|
||||||
|
AppLocalizations l10n,
|
||||||
|
ThemeData theme,
|
||||||
|
) {
|
||||||
|
// Case a: No tasks at all (user hasn't created any rooms/tasks)
|
||||||
|
if (state.totalTodayCount == 0 &&
|
||||||
|
state.tomorrowTasks.isEmpty &&
|
||||||
|
state.completedTodayCount == 0) {
|
||||||
|
return _buildNoTasksState(l10n, theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case b: All clear -- there WERE tasks today but all are done
|
||||||
|
if (state.overdueTasks.isEmpty &&
|
||||||
|
state.todayTasks.isEmpty &&
|
||||||
|
state.completedTodayCount > 0 &&
|
||||||
|
state.tomorrowTasks.isEmpty) {
|
||||||
|
return _buildAllClearState(state, l10n, theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case c: Nothing today, but stuff tomorrow -- show celebration + tomorrow
|
||||||
|
if (state.overdueTasks.isEmpty &&
|
||||||
|
state.todayTasks.isEmpty &&
|
||||||
|
state.completedTodayCount == 0 &&
|
||||||
|
state.tomorrowTasks.isNotEmpty) {
|
||||||
|
return _buildAllClearWithTomorrow(state, l10n, theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case b extended: all clear with tomorrow tasks
|
||||||
|
if (state.overdueTasks.isEmpty &&
|
||||||
|
state.todayTasks.isEmpty &&
|
||||||
|
state.completedTodayCount > 0 &&
|
||||||
|
state.tomorrowTasks.isNotEmpty) {
|
||||||
|
return _buildAllClearWithTomorrow(state, l10n, theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case d: Normal state -- tasks exist
|
||||||
|
return _buildNormalState(state, l10n, theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// No tasks at all -- first-run empty state.
|
||||||
|
Widget _buildNoTasksState(AppLocalizations l10n, ThemeData theme) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||||
@@ -24,7 +111,7 @@ class HomeScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Text(
|
Text(
|
||||||
l10n.homeEmptyTitle,
|
l10n.dailyPlanNoTasks,
|
||||||
style: theme.textTheme.headlineSmall,
|
style: theme.textTheme.headlineSmall,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
@@ -46,4 +133,257 @@ class HomeScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// All tasks done, no tomorrow tasks -- celebration state.
|
||||||
|
Widget _buildAllClearState(
|
||||||
|
DailyPlanState state,
|
||||||
|
AppLocalizations l10n,
|
||||||
|
ThemeData theme,
|
||||||
|
) {
|
||||||
|
return Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
ProgressCard(
|
||||||
|
completed: state.completedTodayCount,
|
||||||
|
total: state.totalTodayCount,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Icon(
|
||||||
|
Icons.celebration_outlined,
|
||||||
|
size: 80,
|
||||||
|
color: theme.colorScheme.onSurface.withValues(alpha: 0.4),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Text(
|
||||||
|
l10n.dailyPlanAllClearTitle,
|
||||||
|
style: theme.textTheme.headlineSmall,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
l10n.dailyPlanAllClearMessage,
|
||||||
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All clear for today but tomorrow tasks exist.
|
||||||
|
Widget _buildAllClearWithTomorrow(
|
||||||
|
DailyPlanState state,
|
||||||
|
AppLocalizations l10n,
|
||||||
|
ThemeData theme,
|
||||||
|
) {
|
||||||
|
return ListView(
|
||||||
|
children: [
|
||||||
|
ProgressCard(
|
||||||
|
completed: state.completedTodayCount,
|
||||||
|
total: state.totalTodayCount,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Center(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.celebration_outlined,
|
||||||
|
size: 80,
|
||||||
|
color: theme.colorScheme.onSurface.withValues(alpha: 0.4),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Text(
|
||||||
|
l10n.dailyPlanAllClearTitle,
|
||||||
|
style: theme.textTheme.headlineSmall,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
l10n.dailyPlanAllClearMessage,
|
||||||
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildTomorrowSection(state, l10n, theme),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Normal state with overdue/today/tomorrow sections.
|
||||||
|
Widget _buildNormalState(
|
||||||
|
DailyPlanState state,
|
||||||
|
AppLocalizations l10n,
|
||||||
|
ThemeData theme,
|
||||||
|
) {
|
||||||
|
return ListView(
|
||||||
|
children: [
|
||||||
|
ProgressCard(
|
||||||
|
completed: state.completedTodayCount,
|
||||||
|
total: state.totalTodayCount,
|
||||||
|
),
|
||||||
|
// Overdue section (conditional)
|
||||||
|
if (state.overdueTasks.isNotEmpty) ...[
|
||||||
|
_buildSectionHeader(
|
||||||
|
l10n.dailyPlanSectionOverdue,
|
||||||
|
theme,
|
||||||
|
color: _overdueColor,
|
||||||
|
),
|
||||||
|
...state.overdueTasks.map(
|
||||||
|
(tw) => _buildAnimatedTaskRow(tw, showCheckbox: true),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
// Today section
|
||||||
|
_buildSectionHeader(
|
||||||
|
l10n.dailyPlanSectionToday,
|
||||||
|
theme,
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
if (state.todayTasks.isEmpty)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
child: Text(
|
||||||
|
l10n.dailyPlanAllClearMessage,
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
...state.todayTasks.map(
|
||||||
|
(tw) => _buildAnimatedTaskRow(tw, showCheckbox: true),
|
||||||
|
),
|
||||||
|
// Tomorrow section (conditional, collapsed)
|
||||||
|
if (state.tomorrowTasks.isNotEmpty)
|
||||||
|
_buildTomorrowSection(state, l10n, theme),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSectionHeader(
|
||||||
|
String title,
|
||||||
|
ThemeData theme, {
|
||||||
|
required Color color,
|
||||||
|
}) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
style: theme.textTheme.titleMedium?.copyWith(color: color),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAnimatedTaskRow(
|
||||||
|
TaskWithRoom tw, {
|
||||||
|
required bool showCheckbox,
|
||||||
|
}) {
|
||||||
|
final isCompleting = _completingTaskIds.contains(tw.task.id);
|
||||||
|
|
||||||
|
if (isCompleting) {
|
||||||
|
return _CompletingTaskRow(
|
||||||
|
key: ValueKey('completing-${tw.task.id}'),
|
||||||
|
taskWithRoom: tw,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DailyPlanTaskRow(
|
||||||
|
key: ValueKey('task-${tw.task.id}'),
|
||||||
|
taskWithRoom: tw,
|
||||||
|
showCheckbox: showCheckbox,
|
||||||
|
onCompleted: () => _onTaskCompleted(tw.task.id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTomorrowSection(
|
||||||
|
DailyPlanState state,
|
||||||
|
AppLocalizations l10n,
|
||||||
|
ThemeData theme,
|
||||||
|
) {
|
||||||
|
return ExpansionTile(
|
||||||
|
initiallyExpanded: false,
|
||||||
|
title: Text(
|
||||||
|
l10n.dailyPlanUpcomingCount(state.tomorrowTasks.length),
|
||||||
|
style: theme.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
children: state.tomorrowTasks
|
||||||
|
.map(
|
||||||
|
(tw) => DailyPlanTaskRow(
|
||||||
|
key: ValueKey('tomorrow-${tw.task.id}'),
|
||||||
|
taskWithRoom: tw,
|
||||||
|
showCheckbox: false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A task row that animates to zero height on completion.
|
||||||
|
class _CompletingTaskRow extends StatefulWidget {
|
||||||
|
const _CompletingTaskRow({
|
||||||
|
super.key,
|
||||||
|
required this.taskWithRoom,
|
||||||
|
});
|
||||||
|
|
||||||
|
final TaskWithRoom taskWithRoom;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_CompletingTaskRow> createState() => _CompletingTaskRowState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CompletingTaskRowState extends State<_CompletingTaskRow>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late final AnimationController _controller;
|
||||||
|
late final Animation<double> _sizeAnimation;
|
||||||
|
late final Animation<Offset> _slideAnimation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = AnimationController(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
_sizeAnimation = Tween<double>(begin: 1.0, end: 0.0).animate(
|
||||||
|
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
|
||||||
|
);
|
||||||
|
_slideAnimation = Tween<Offset>(
|
||||||
|
begin: Offset.zero,
|
||||||
|
end: const Offset(1.0, 0.0),
|
||||||
|
).animate(
|
||||||
|
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
|
||||||
|
);
|
||||||
|
_controller.forward();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizeTransition(
|
||||||
|
sizeFactor: _sizeAnimation,
|
||||||
|
child: SlideTransition(
|
||||||
|
position: _slideAnimation,
|
||||||
|
child: DailyPlanTaskRow(
|
||||||
|
taskWithRoom: widget.taskWithRoom,
|
||||||
|
showCheckbox: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
248
test/features/home/presentation/home_screen_test.dart
Normal file
248
test/features/home/presentation/home_screen_test.dart
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import 'package:household_keeper/core/router/router.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/rooms/presentation/room_providers.dart';
|
||||||
import 'package:household_keeper/l10n/app_localizations.dart';
|
import 'package:household_keeper/l10n/app_localizations.dart';
|
||||||
|
|
||||||
@@ -13,7 +15,7 @@ void main() {
|
|||||||
SharedPreferences.setMockInitialValues({});
|
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() {
|
Widget buildApp() {
|
||||||
return ProviderScope(
|
return ProviderScope(
|
||||||
overrides: [
|
overrides: [
|
||||||
@@ -22,6 +24,17 @@ void main() {
|
|||||||
roomWithStatsListProvider.overrideWith(
|
roomWithStatsListProvider.overrideWith(
|
||||||
(ref) => Stream.value([]),
|
(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(
|
child: MaterialApp.router(
|
||||||
routerConfig: router,
|
routerConfig: router,
|
||||||
@@ -52,7 +65,8 @@ void main() {
|
|||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
// Initially on Home tab (index 0) -- verify home empty state is shown
|
// 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)
|
// Tap the Rooms tab (second destination)
|
||||||
await tester.tap(find.text('R\u00e4ume'));
|
await tester.tap(find.text('R\u00e4ume'));
|
||||||
|
|||||||
Reference in New Issue
Block a user