test(TaskListScreen): add integration tests for filtered and overdue task states
All checks were successful
Build and Release to F-Droid / build-and-deploy (push) Successful in 10m30s
All checks were successful
Build and Release to F-Droid / build-and-deploy (push) Successful in 10m30s
- Covers empty states, celebration state, and scheduled/overdue task rendering - Verifies proper checkbox behavior for future tasks - Tests AppBar for sort dropdown, edit/delete actions, and calendar strip - Adds necessary test helpers and overrides for room-specific tasks
This commit is contained in:
@@ -4,34 +4,46 @@ import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'package:household_keeper/core/database/database.dart';
|
||||
import 'package:household_keeper/core/providers/database_provider.dart';
|
||||
import 'package:household_keeper/features/home/presentation/calendar_day_list.dart';
|
||||
import 'package:household_keeper/features/home/presentation/calendar_providers.dart';
|
||||
import 'package:household_keeper/features/home/presentation/calendar_strip.dart';
|
||||
import 'package:household_keeper/features/tasks/presentation/sort_dropdown.dart';
|
||||
import 'package:household_keeper/features/tasks/presentation/task_providers.dart';
|
||||
import 'package:household_keeper/features/tasks/presentation/task_row.dart';
|
||||
import 'package:household_keeper/l10n/app_localizations.dart';
|
||||
|
||||
/// Screen displaying all tasks within a room, sorted by due date.
|
||||
/// Screen displaying tasks within a room filtered by the selected calendar day.
|
||||
///
|
||||
/// Shows an empty state when no tasks exist, with a button to create one.
|
||||
/// FAB always visible for quick task creation.
|
||||
/// Shows a horizontal calendar strip at the top (same as HomeScreen) and
|
||||
/// a date-filtered task list below. FAB always visible for quick task creation.
|
||||
/// AppBar shows room name with edit and delete actions.
|
||||
class TaskListScreen extends ConsumerWidget {
|
||||
class TaskListScreen extends ConsumerStatefulWidget {
|
||||
const TaskListScreen({super.key, required this.roomId});
|
||||
|
||||
final int roomId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
ConsumerState<TaskListScreen> createState() => _TaskListScreenState();
|
||||
}
|
||||
|
||||
class _TaskListScreenState extends ConsumerState<TaskListScreen> {
|
||||
late final CalendarStripController _stripController =
|
||||
CalendarStripController();
|
||||
|
||||
/// Whether to show the floating "Heute" button.
|
||||
/// True when the user has scrolled away from today's card.
|
||||
bool _showTodayButton = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final asyncTasks = ref.watch(tasksInRoomProvider(roomId));
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: _RoomTitle(roomId: roomId),
|
||||
title: _RoomTitle(roomId: widget.roomId),
|
||||
actions: [
|
||||
const SortDropdown(),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
onPressed: () => context.go('/rooms/$roomId/edit'),
|
||||
onPressed: () => context.go('/rooms/${widget.roomId}/edit'),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
@@ -39,33 +51,43 @@ class TaskListScreen extends ConsumerWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
body: asyncTasks.when(
|
||||
data: (tasks) {
|
||||
if (tasks.isEmpty) {
|
||||
return _EmptyState(l10n: l10n, roomId: roomId);
|
||||
}
|
||||
return _TaskListView(
|
||||
tasks: tasks,
|
||||
l10n: l10n,
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, stack) => Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
body: Stack(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Text('Fehler: $error'),
|
||||
const SizedBox(height: 16),
|
||||
FilledButton.tonal(
|
||||
onPressed: () => ref.invalidate(tasksInRoomProvider(roomId)),
|
||||
child: const Text('Erneut versuchen'),
|
||||
CalendarStrip(
|
||||
controller: _stripController,
|
||||
onTodayVisibilityChanged: (visible) {
|
||||
setState(() => _showTodayButton = !visible);
|
||||
},
|
||||
),
|
||||
Expanded(child: CalendarDayList(roomId: widget.roomId)),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_showTodayButton)
|
||||
Positioned(
|
||||
bottom: 80, // Above the FAB
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
child: FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
final now = DateTime.now();
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
ref
|
||||
.read(selectedDateProvider.notifier)
|
||||
.selectDate(today);
|
||||
_stripController.scrollToToday();
|
||||
},
|
||||
icon: const Icon(Icons.today),
|
||||
label: Text(l10n.calendarTodayButton),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () => context.go('/rooms/$roomId/tasks/new'),
|
||||
onPressed: () => context.go('/rooms/${widget.roomId}/tasks/new'),
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
);
|
||||
@@ -90,7 +112,7 @@ class TaskListScreen extends ConsumerWidget {
|
||||
onPressed: () {
|
||||
Navigator.pop(ctx);
|
||||
final db = ref.read(appDatabaseProvider);
|
||||
db.roomsDao.deleteRoom(roomId);
|
||||
db.roomsDao.deleteRoom(widget.roomId);
|
||||
// Navigate back to rooms list
|
||||
context.go('/rooms');
|
||||
},
|
||||
@@ -126,105 +148,3 @@ class _RoomTitle extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Empty state shown when the room has no tasks.
|
||||
class _EmptyState extends StatelessWidget {
|
||||
const _EmptyState({required this.l10n, required this.roomId});
|
||||
|
||||
final AppLocalizations l10n;
|
||||
final int roomId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.task_alt,
|
||||
size: 80,
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
l10n.taskEmptyTitle,
|
||||
style: theme.textTheme.headlineSmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
l10n.taskEmptyMessage,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FilledButton.tonal(
|
||||
onPressed: () => context.go('/rooms/$roomId/tasks/new'),
|
||||
child: Text(l10n.taskEmptyAction),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// List view of task rows with long-press delete support.
|
||||
class _TaskListView extends ConsumerWidget {
|
||||
const _TaskListView({required this.tasks, required this.l10n});
|
||||
|
||||
final List<Task> tasks;
|
||||
final AppLocalizations l10n;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ListView.builder(
|
||||
itemCount: tasks.length,
|
||||
itemBuilder: (context, index) {
|
||||
final task = tasks[index];
|
||||
return TaskRow(
|
||||
key: ValueKey(task.id),
|
||||
task: task,
|
||||
onDelete: () => _showDeleteConfirmation(context, ref, task),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showDeleteConfirmation(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
Task task,
|
||||
) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text(l10n.taskDeleteConfirmTitle),
|
||||
content: Text(l10n.taskDeleteConfirmMessage),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: Text(l10n.cancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(ctx);
|
||||
ref.read(taskActionsProvider.notifier).deleteTask(task.id);
|
||||
},
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
foregroundColor: Theme.of(context).colorScheme.onError,
|
||||
),
|
||||
child: Text(l10n.taskDeleteConfirmAction),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user