feat(05-02): build CalendarStrip, CalendarTaskRow, CalendarDayList widgets
- Add totalTaskCount field to CalendarDayState to distinguish first-run from celebration - Add getTaskCount() to CalendarDao (SELECT COUNT from tasks) - CalendarStrip: 181-day horizontal scroll with German abbreviations, today highlighting, month boundary labels, scroll-to-today controller - CalendarTaskRow: task name + room tag chip + checkbox, no relative date, isOverdue coral styling - CalendarDayList: loading/error/first-run-empty/empty-day/celebration/has-tasks states, overdue section (today only), slide-out completion animation - Update home_screen_test.dart and app_shell_test.dart to test new calendar providers instead of dailyPlanProvider
This commit is contained in:
@@ -5,12 +5,13 @@ 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/home/domain/calendar_models.dart';
|
||||
import 'package:household_keeper/features/home/presentation/calendar_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';
|
||||
import 'package:household_keeper/features/home/domain/daily_plan_models.dart';
|
||||
|
||||
/// Helper to create a test [Task] with sensible defaults.
|
||||
Task _makeTask({
|
||||
@@ -51,15 +52,16 @@ TaskWithRoom _makeTaskWithRoom({
|
||||
);
|
||||
}
|
||||
|
||||
/// Build the app with dailyPlanProvider overridden to the given state.
|
||||
/// Build the app with calendarDayProvider overridden to the given state.
|
||||
///
|
||||
/// Uses [UncontrolledProviderScope] with a [ProviderContainer] to avoid
|
||||
/// the riverpod_lint scoped_providers_should_specify_dependencies warning.
|
||||
Widget _buildApp(DailyPlanState planState) {
|
||||
Widget _buildApp(CalendarDayState dayState) {
|
||||
final container = ProviderContainer(overrides: [
|
||||
dailyPlanProvider.overrideWith(
|
||||
(ref) => Stream.value(planState),
|
||||
calendarDayProvider.overrideWith(
|
||||
(ref) => Stream.value(dayState),
|
||||
),
|
||||
selectedDateProvider.overrideWith(SelectedDateNotifier.new),
|
||||
roomWithStatsListProvider.overrideWith(
|
||||
(ref) => Stream.value([]),
|
||||
),
|
||||
@@ -81,17 +83,21 @@ void main() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
});
|
||||
|
||||
final now = DateTime.now();
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
final yesterday = today.subtract(const Duration(days: 1));
|
||||
|
||||
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.pumpWidget(_buildApp(CalendarDayState(
|
||||
selectedDate: today,
|
||||
dayTasks: const [],
|
||||
overdueTasks: const [],
|
||||
totalTaskCount: 0,
|
||||
)));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
// Should show "Noch keine Aufgaben angelegt" (dailyPlanNoTasks)
|
||||
expect(find.text('Noch keine Aufgaben angelegt'), findsOneWidget);
|
||||
@@ -99,58 +105,53 @@ void main() {
|
||||
expect(find.text('Raum erstellen'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows all-clear state when all tasks are done',
|
||||
testWidgets('shows celebration state when tasks exist but today is clear',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(_buildApp(const DailyPlanState(
|
||||
overdueTasks: [],
|
||||
todayTasks: [],
|
||||
tomorrowTasks: [],
|
||||
completedTodayCount: 3,
|
||||
totalTodayCount: 3,
|
||||
await tester.pumpWidget(_buildApp(CalendarDayState(
|
||||
selectedDate: today,
|
||||
dayTasks: const [],
|
||||
overdueTasks: const [],
|
||||
totalTaskCount: 5, // tasks exist elsewhere
|
||||
)));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
// Should show celebration empty state
|
||||
expect(find.text('Alles erledigt! \u{1F31F}'), findsOneWidget);
|
||||
// Should show celebration state
|
||||
expect(find.byIcon(Icons.celebration_outlined), findsOneWidget);
|
||||
// Progress card should show 3/3
|
||||
expect(find.text('3 von 3 erledigt'), findsOneWidget);
|
||||
expect(find.text('Alles erledigt! \u{1F31F}'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows empty-day state for non-today date with no tasks',
|
||||
(tester) async {
|
||||
final tomorrow = today.add(const Duration(days: 1));
|
||||
await tester.pumpWidget(_buildApp(CalendarDayState(
|
||||
selectedDate: tomorrow,
|
||||
dayTasks: const [],
|
||||
overdueTasks: const [],
|
||||
totalTaskCount: 5, // tasks exist on other days
|
||||
)));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
// Should show "Keine Aufgaben" (not celebration — not today)
|
||||
expect(find.text('Keine Aufgaben'), findsOneWidget);
|
||||
expect(find.byIcon(Icons.event_available), 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: [
|
||||
testWidgets('shows overdue section when overdue tasks exist (today)',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(_buildApp(CalendarDayState(
|
||||
selectedDate: today,
|
||||
dayTasks: [
|
||||
_makeTaskWithRoom(
|
||||
id: 1,
|
||||
id: 2,
|
||||
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,
|
||||
@@ -159,24 +160,13 @@ void main() {
|
||||
nextDueDate: yesterday,
|
||||
),
|
||||
],
|
||||
todayTasks: [
|
||||
_makeTaskWithRoom(
|
||||
id: 2,
|
||||
taskName: 'Staubsaugen',
|
||||
roomName: 'Wohnzimmer',
|
||||
nextDueDate: today,
|
||||
),
|
||||
],
|
||||
tomorrowTasks: [],
|
||||
completedTodayCount: 0,
|
||||
totalTodayCount: 2,
|
||||
totalTaskCount: 2,
|
||||
)));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
// 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);
|
||||
@@ -185,54 +175,37 @@ void main() {
|
||||
expect(find.text('Wohnzimmer'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows collapsed tomorrow section with count',
|
||||
testWidgets('does not show overdue section for non-today date',
|
||||
(tester) async {
|
||||
final now = DateTime.now();
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
// On a future date, overdueTasks will be empty (calendarDayProvider
|
||||
// only populates overdueTasks when isToday).
|
||||
final tomorrow = today.add(const Duration(days: 1));
|
||||
|
||||
await tester.pumpWidget(_buildApp(DailyPlanState(
|
||||
overdueTasks: [],
|
||||
todayTasks: [
|
||||
await tester.pumpWidget(_buildApp(CalendarDayState(
|
||||
selectedDate: tomorrow,
|
||||
dayTasks: [
|
||||
_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,
|
||||
overdueTasks: const [], // No overdue for non-today
|
||||
totalTaskCount: 1,
|
||||
)));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
// 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);
|
||||
// Should NOT show overdue section header
|
||||
expect(find.text('\u00dcberf\u00e4llig'), findsNothing);
|
||||
// Should show day task
|
||||
expect(find.text('Staubsaugen'), findsOneWidget);
|
||||
});
|
||||
|
||||
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: [
|
||||
testWidgets('tasks have checkboxes', (tester) async {
|
||||
await tester.pumpWidget(_buildApp(CalendarDayState(
|
||||
selectedDate: today,
|
||||
dayTasks: [
|
||||
_makeTaskWithRoom(
|
||||
id: 1,
|
||||
taskName: 'Staubsaugen',
|
||||
@@ -240,14 +213,29 @@ void main() {
|
||||
nextDueDate: today,
|
||||
),
|
||||
],
|
||||
tomorrowTasks: [],
|
||||
completedTodayCount: 0,
|
||||
totalTodayCount: 1,
|
||||
overdueTasks: const [],
|
||||
totalTaskCount: 1,
|
||||
)));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
// Today task should have a checkbox
|
||||
// Task should have a checkbox
|
||||
expect(find.byType(Checkbox), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('calendar strip is shown', (tester) async {
|
||||
await tester.pumpWidget(_buildApp(CalendarDayState(
|
||||
selectedDate: today,
|
||||
dayTasks: const [],
|
||||
overdueTasks: const [],
|
||||
totalTaskCount: 0,
|
||||
)));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
// The strip is a horizontal ListView — verify it exists by finding
|
||||
// ListView widgets (strip + potentially the task list).
|
||||
expect(find.byType(ListView), findsWidgets);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user