diff --git a/test/features/tasks/data/task_history_dao_test.dart b/test/features/tasks/data/task_history_dao_test.dart new file mode 100644 index 0000000..0f05abf --- /dev/null +++ b/test/features/tasks/data/task_history_dao_test.dart @@ -0,0 +1,158 @@ +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:drift/drift.dart'; +import 'package:household_keeper/core/database/database.dart'; +import 'package:household_keeper/features/tasks/domain/effort_level.dart'; +import 'package:household_keeper/features/tasks/domain/frequency.dart'; + +void main() { + late AppDatabase db; + late int roomId; + + setUp(() async { + db = AppDatabase(NativeDatabase.memory()); + roomId = await db.roomsDao.insertRoom( + RoomsCompanion.insert(name: 'Kueche', iconName: 'kitchen'), + ); + }); + + tearDown(() async { + await db.close(); + }); + + group('TasksDao.watchCompletionsForTask', () { + test('returns empty list when task has no completions', () async { + final taskId = await db.tasksDao.insertTask( + TasksCompanion.insert( + roomId: roomId, + name: 'Staubsaugen', + intervalType: IntervalType.weekly, + effortLevel: EffortLevel.medium, + nextDueDate: DateTime(2026, 3, 15), + ), + ); + + final completions = + await db.tasksDao.watchCompletionsForTask(taskId).first; + + expect(completions, isEmpty); + }); + + test('returns completion after completeTask is called', () async { + final taskId = await db.tasksDao.insertTask( + TasksCompanion.insert( + roomId: roomId, + name: 'Abspuelen', + intervalType: IntervalType.daily, + effortLevel: EffortLevel.low, + nextDueDate: DateTime(2026, 3, 15), + ), + ); + + final completionTime = DateTime(2026, 3, 15, 10, 30); + await db.tasksDao.completeTask(taskId, now: completionTime); + + final completions = + await db.tasksDao.watchCompletionsForTask(taskId).first; + + expect(completions.length, 1); + expect(completions.first.taskId, taskId); + expect(completions.first.completedAt, completionTime); + }); + + test('returns multiple completions in reverse-chronological order', () async { + final taskId = await db.tasksDao.insertTask( + TasksCompanion.insert( + roomId: roomId, + name: 'Fenster putzen', + intervalType: IntervalType.monthly, + effortLevel: EffortLevel.high, + nextDueDate: DateTime(2026, 1, 1), + ), + ); + + // Complete multiple times with specific timestamps + final time1 = DateTime(2026, 1, 10, 9, 0); + final time2 = DateTime(2026, 2, 12, 14, 30); + final time3 = DateTime(2026, 3, 15, 8, 0); + + // Insert out of order to verify ordering is enforced by query + await db.tasksDao.completeTask(taskId, now: time1); + await db.tasksDao.completeTask(taskId, now: time2); + await db.tasksDao.completeTask(taskId, now: time3); + + final completions = + await db.tasksDao.watchCompletionsForTask(taskId).first; + + expect(completions.length, 3); + // Newest first (reverse-chronological) + expect(completions[0].completedAt, time3); + expect(completions[1].completedAt, time2); + expect(completions[2].completedAt, time1); + }); + + test('completions for different tasks are isolated', () async { + final taskId1 = await db.tasksDao.insertTask( + TasksCompanion.insert( + roomId: roomId, + name: 'Task A', + intervalType: IntervalType.daily, + effortLevel: EffortLevel.low, + nextDueDate: DateTime(2026, 3, 15), + ), + ); + + final taskId2 = await db.tasksDao.insertTask( + TasksCompanion.insert( + roomId: roomId, + name: 'Task B', + intervalType: IntervalType.daily, + effortLevel: EffortLevel.low, + nextDueDate: DateTime(2026, 3, 15), + ), + ); + + await db.tasksDao.completeTask(taskId1, now: DateTime(2026, 3, 15)); + + final completionsForTask1 = + await db.tasksDao.watchCompletionsForTask(taskId1).first; + final completionsForTask2 = + await db.tasksDao.watchCompletionsForTask(taskId2).first; + + expect(completionsForTask1.length, 1); + expect(completionsForTask1.first.taskId, taskId1); + expect(completionsForTask2, isEmpty); + }); + + test('stream emits updated list after new completion is added', () async { + final taskId = await db.tasksDao.insertTask( + TasksCompanion.insert( + roomId: roomId, + name: 'Bodenwischen', + intervalType: IntervalType.weekly, + effortLevel: EffortLevel.medium, + nextDueDate: DateTime(2026, 3, 15), + ), + ); + + // Collect stream emissions + final emissions = >[]; + final subscription = db.tasksDao + .watchCompletionsForTask(taskId) + .listen(emissions.add); + + // Wait for initial empty emission + await Future.delayed(const Duration(milliseconds: 50)); + expect(emissions.isNotEmpty, isTrue); + expect(emissions.last, isEmpty); + + // Complete the task + await db.tasksDao.completeTask(taskId, now: DateTime(2026, 3, 15, 9, 0)); + await Future.delayed(const Duration(milliseconds: 50)); + + expect(emissions.last.length, 1); + + await subscription.cancel(); + }); + }); +}