feat(02-01): Drift tables, DAOs, scheduling utility, domain models with tests
- Add Rooms, Tasks, TaskCompletions Drift tables with schema v2 migration - Create RoomsDao with CRUD, watchAll, watchWithStats, cascade delete, reorder - Create TasksDao with CRUD, watchInRoom (sorted by due), completeTask, overdue detection - Implement calculateNextDueDate and catchUpToPresent pure scheduling functions - Define IntervalType enum (8 types), EffortLevel enum, FrequencyInterval model - Add formatRelativeDate German formatter and curatedRoomIcons icon list - Enable PRAGMA foreign_keys in beforeOpen migration strategy - All 30 unit tests passing (17 scheduling + 6 rooms DAO + 7 tasks DAO) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -18,8 +18,8 @@ void main() {
|
||||
expect(db, isNotNull);
|
||||
});
|
||||
|
||||
test('has schemaVersion 1', () {
|
||||
expect(db.schemaVersion, equals(1));
|
||||
test('has schemaVersion 2', () {
|
||||
expect(db.schemaVersion, equals(2));
|
||||
});
|
||||
|
||||
test('can be closed without error', () async {
|
||||
|
||||
62
test/drift/household_keeper/migration_test.dart
Normal file
62
test/drift/household_keeper/migration_test.dart
Normal file
@@ -0,0 +1,62 @@
|
||||
// dart format width=80
|
||||
// ignore_for_file: unused_local_variable, unused_import
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_dev/api/migrations_native.dart';
|
||||
import 'package:household_keeper/core/database/database.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'generated/schema.dart';
|
||||
|
||||
import 'generated/schema_v1.dart' as v1;
|
||||
import 'generated/schema_v2.dart' as v2;
|
||||
|
||||
void main() {
|
||||
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
|
||||
late SchemaVerifier verifier;
|
||||
|
||||
setUpAll(() {
|
||||
verifier = SchemaVerifier(GeneratedHelper());
|
||||
});
|
||||
|
||||
group('simple database migrations', () {
|
||||
// These simple tests verify all possible schema updates with a simple (no
|
||||
// data) migration. This is a quick way to ensure that written database
|
||||
// migrations properly alter the schema.
|
||||
const versions = GeneratedHelper.versions;
|
||||
for (final (i, fromVersion) in versions.indexed) {
|
||||
group('from $fromVersion', () {
|
||||
for (final toVersion in versions.skip(i + 1)) {
|
||||
test('to $toVersion', () async {
|
||||
final schema = await verifier.schemaAt(fromVersion);
|
||||
final db = AppDatabase(schema.newConnection());
|
||||
await verifier.migrateAndValidate(db, toVersion);
|
||||
await db.close();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// The following template shows how to write tests ensuring your migrations
|
||||
// preserve existing data.
|
||||
// Testing this can be useful for migrations that change existing columns
|
||||
// (e.g. by alterating their type or constraints). Migrations that only add
|
||||
// tables or columns typically don't need these advanced tests. For more
|
||||
// information, see https://drift.simonbinder.eu/migrations/tests/#verifying-data-integrity
|
||||
// TODO: This generated template shows how these tests could be written. Adopt
|
||||
// it to your own needs when testing migrations with data integrity.
|
||||
test('migration from v1 to v2 does not corrupt data', () async {
|
||||
// Add data to insert into the old database, and the expected rows after the
|
||||
// migration.
|
||||
// TODO: Fill these lists
|
||||
|
||||
await verifier.testWithDataIntegrity(
|
||||
oldVersion: 1,
|
||||
newVersion: 2,
|
||||
createOld: v1.DatabaseAtV1.new,
|
||||
createNew: v2.DatabaseAtV2.new,
|
||||
openTestedDatabase: AppDatabase.new,
|
||||
createItems: (batch, oldDb) {},
|
||||
validateItems: (newDb) async {},
|
||||
);
|
||||
});
|
||||
}
|
||||
183
test/features/rooms/data/rooms_dao_test.dart
Normal file
183
test/features/rooms/data/rooms_dao_test.dart
Normal file
@@ -0,0 +1,183 @@
|
||||
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;
|
||||
|
||||
setUp(() {
|
||||
db = AppDatabase(NativeDatabase.memory());
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
group('RoomsDao', () {
|
||||
test('insertRoom returns a valid id', () async {
|
||||
final id = await db.roomsDao.insertRoom(
|
||||
RoomsCompanion.insert(name: 'Kueche', iconName: 'kitchen'),
|
||||
);
|
||||
expect(id, greaterThan(0));
|
||||
});
|
||||
|
||||
test('watchAllRooms emits rooms ordered by sortOrder', () async {
|
||||
// Insert rooms with different sort orders
|
||||
await db.roomsDao.insertRoom(
|
||||
RoomsCompanion.insert(
|
||||
name: 'Badezimmer',
|
||||
iconName: 'bathtub',
|
||||
sortOrder: Value(2),
|
||||
),
|
||||
);
|
||||
await db.roomsDao.insertRoom(
|
||||
RoomsCompanion.insert(
|
||||
name: 'Kueche',
|
||||
iconName: 'kitchen',
|
||||
sortOrder: Value(0),
|
||||
),
|
||||
);
|
||||
await db.roomsDao.insertRoom(
|
||||
RoomsCompanion.insert(
|
||||
name: 'Schlafzimmer',
|
||||
iconName: 'bed',
|
||||
sortOrder: Value(1),
|
||||
),
|
||||
);
|
||||
|
||||
final rooms = await db.roomsDao.watchAllRooms().first;
|
||||
|
||||
expect(rooms.length, 3);
|
||||
expect(rooms[0].name, 'Kueche');
|
||||
expect(rooms[1].name, 'Schlafzimmer');
|
||||
expect(rooms[2].name, 'Badezimmer');
|
||||
});
|
||||
|
||||
test('updateRoom changes name and iconName', () async {
|
||||
final id = await db.roomsDao.insertRoom(
|
||||
RoomsCompanion.insert(name: 'Kueche', iconName: 'kitchen'),
|
||||
);
|
||||
|
||||
final room = await db.roomsDao.getRoomById(id);
|
||||
final updated = room.copyWith(name: 'Wohnkueche', iconName: 'dining');
|
||||
await db.roomsDao.updateRoom(updated);
|
||||
|
||||
final result = await db.roomsDao.getRoomById(id);
|
||||
expect(result.name, 'Wohnkueche');
|
||||
expect(result.iconName, 'dining');
|
||||
});
|
||||
|
||||
test('deleteRoom cascades to associated tasks and completions', () async {
|
||||
// Create room
|
||||
final roomId = await db.roomsDao.insertRoom(
|
||||
RoomsCompanion.insert(name: 'Kueche', iconName: 'kitchen'),
|
||||
);
|
||||
|
||||
// Create task in the room
|
||||
final taskId = await db.tasksDao.insertTask(
|
||||
TasksCompanion.insert(
|
||||
roomId: roomId,
|
||||
name: 'Abspuelen',
|
||||
intervalType: IntervalType.daily,
|
||||
effortLevel: EffortLevel.low,
|
||||
nextDueDate: DateTime(2026, 3, 15),
|
||||
),
|
||||
);
|
||||
|
||||
// Complete the task to create a completion record
|
||||
await db.tasksDao.completeTask(taskId, now: DateTime(2026, 3, 15));
|
||||
|
||||
// Verify task and completion exist
|
||||
final tasksBefore = await db.tasksDao.watchTasksInRoom(roomId).first;
|
||||
expect(tasksBefore.length, 1);
|
||||
|
||||
// Delete room
|
||||
await db.roomsDao.deleteRoom(roomId);
|
||||
|
||||
// Verify room is gone
|
||||
final rooms = await db.roomsDao.watchAllRooms().first;
|
||||
expect(rooms, isEmpty);
|
||||
|
||||
// Verify tasks are gone
|
||||
final tasksAfter = await db.tasksDao.watchTasksInRoom(roomId).first;
|
||||
expect(tasksAfter, isEmpty);
|
||||
});
|
||||
|
||||
test('reorderRooms updates sortOrder for all rooms', () async {
|
||||
final id1 = await db.roomsDao.insertRoom(
|
||||
RoomsCompanion.insert(name: 'Kueche', iconName: 'kitchen'),
|
||||
);
|
||||
final id2 = await db.roomsDao.insertRoom(
|
||||
RoomsCompanion.insert(name: 'Bad', iconName: 'bathtub'),
|
||||
);
|
||||
final id3 = await db.roomsDao.insertRoom(
|
||||
RoomsCompanion.insert(name: 'Schlafzimmer', iconName: 'bed'),
|
||||
);
|
||||
|
||||
// Reorder: Bad first, then Schlafzimmer, then Kueche
|
||||
await db.roomsDao.reorderRooms([id2, id3, id1]);
|
||||
|
||||
final rooms = await db.roomsDao.watchAllRooms().first;
|
||||
expect(rooms[0].name, 'Bad');
|
||||
expect(rooms[1].name, 'Schlafzimmer');
|
||||
expect(rooms[2].name, 'Kueche');
|
||||
});
|
||||
|
||||
test('watchRoomWithStats emits room with due task count and cleanliness ratio', () async {
|
||||
final roomId = await db.roomsDao.insertRoom(
|
||||
RoomsCompanion.insert(name: 'Kueche', iconName: 'kitchen'),
|
||||
);
|
||||
|
||||
final today = DateTime(2026, 3, 15);
|
||||
|
||||
// Add a task due today (counts as "due")
|
||||
await db.tasksDao.insertTask(
|
||||
TasksCompanion.insert(
|
||||
roomId: roomId,
|
||||
name: 'Task Due Today',
|
||||
intervalType: IntervalType.daily,
|
||||
effortLevel: EffortLevel.low,
|
||||
nextDueDate: today,
|
||||
),
|
||||
);
|
||||
|
||||
// Add an overdue task (yesterday)
|
||||
await db.tasksDao.insertTask(
|
||||
TasksCompanion.insert(
|
||||
roomId: roomId,
|
||||
name: 'Overdue Task',
|
||||
intervalType: IntervalType.weekly,
|
||||
effortLevel: EffortLevel.medium,
|
||||
nextDueDate: DateTime(2026, 3, 14),
|
||||
),
|
||||
);
|
||||
|
||||
// Add a future task
|
||||
await db.tasksDao.insertTask(
|
||||
TasksCompanion.insert(
|
||||
roomId: roomId,
|
||||
name: 'Future Task',
|
||||
intervalType: IntervalType.monthly,
|
||||
effortLevel: EffortLevel.low,
|
||||
nextDueDate: DateTime(2026, 3, 20),
|
||||
),
|
||||
);
|
||||
|
||||
final stats = await db.roomsDao.watchRoomWithStats(today: today).first;
|
||||
|
||||
expect(stats.length, 1);
|
||||
final roomStats = stats.first;
|
||||
expect(roomStats.room.name, 'Kueche');
|
||||
expect(roomStats.totalTasks, 3);
|
||||
// "Due" means on or before today: today (Mar 15) + overdue (Mar 14) = 2
|
||||
expect(roomStats.dueTasks, 2);
|
||||
// "Overdue" means strictly before today: Mar 14 = 1
|
||||
expect(roomStats.overdueCount, 1);
|
||||
// Cleanliness: (3 - 1) / 3 = 0.6667
|
||||
expect(roomStats.cleanlinessRatio, closeTo(0.6667, 0.001));
|
||||
});
|
||||
});
|
||||
}
|
||||
208
test/features/tasks/data/tasks_dao_test.dart
Normal file
208
test/features/tasks/data/tasks_dao_test.dart
Normal file
@@ -0,0 +1,208 @@
|
||||
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());
|
||||
// Create a room for task tests
|
||||
roomId = await db.roomsDao.insertRoom(
|
||||
RoomsCompanion.insert(name: 'Kueche', iconName: 'kitchen'),
|
||||
);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
group('TasksDao', () {
|
||||
test('insertTask returns a valid id', () async {
|
||||
final id = await db.tasksDao.insertTask(
|
||||
TasksCompanion.insert(
|
||||
roomId: roomId,
|
||||
name: 'Abspuelen',
|
||||
intervalType: IntervalType.daily,
|
||||
effortLevel: EffortLevel.low,
|
||||
nextDueDate: DateTime(2026, 3, 15),
|
||||
),
|
||||
);
|
||||
expect(id, greaterThan(0));
|
||||
});
|
||||
|
||||
test('watchTasksInRoom emits tasks sorted by nextDueDate ascending', () async {
|
||||
// Insert tasks with different due dates (out of order)
|
||||
await db.tasksDao.insertTask(
|
||||
TasksCompanion.insert(
|
||||
roomId: roomId,
|
||||
name: 'Later Task',
|
||||
intervalType: IntervalType.weekly,
|
||||
effortLevel: EffortLevel.medium,
|
||||
nextDueDate: DateTime(2026, 3, 20),
|
||||
),
|
||||
);
|
||||
await db.tasksDao.insertTask(
|
||||
TasksCompanion.insert(
|
||||
roomId: roomId,
|
||||
name: 'Early Task',
|
||||
intervalType: IntervalType.daily,
|
||||
effortLevel: EffortLevel.low,
|
||||
nextDueDate: DateTime(2026, 3, 10),
|
||||
),
|
||||
);
|
||||
await db.tasksDao.insertTask(
|
||||
TasksCompanion.insert(
|
||||
roomId: roomId,
|
||||
name: 'Middle Task',
|
||||
intervalType: IntervalType.biweekly,
|
||||
effortLevel: EffortLevel.high,
|
||||
nextDueDate: DateTime(2026, 3, 15),
|
||||
),
|
||||
);
|
||||
|
||||
final tasks = await db.tasksDao.watchTasksInRoom(roomId).first;
|
||||
|
||||
expect(tasks.length, 3);
|
||||
expect(tasks[0].name, 'Early Task');
|
||||
expect(tasks[1].name, 'Middle Task');
|
||||
expect(tasks[2].name, 'Later Task');
|
||||
});
|
||||
|
||||
test('updateTask changes name, description, interval, effort', () async {
|
||||
final id = await db.tasksDao.insertTask(
|
||||
TasksCompanion.insert(
|
||||
roomId: roomId,
|
||||
name: 'Abspuelen',
|
||||
intervalType: IntervalType.daily,
|
||||
effortLevel: EffortLevel.low,
|
||||
nextDueDate: DateTime(2026, 3, 15),
|
||||
),
|
||||
);
|
||||
|
||||
// Get the task, modify it, and update
|
||||
final tasks = await db.tasksDao.watchTasksInRoom(roomId).first;
|
||||
final task = tasks.first;
|
||||
final updated = task.copyWith(
|
||||
name: 'Geschirr spuelen',
|
||||
description: Value('Mit Spuelmittel'),
|
||||
intervalType: IntervalType.weekly,
|
||||
effortLevel: EffortLevel.medium,
|
||||
);
|
||||
await db.tasksDao.updateTask(updated);
|
||||
|
||||
final result = await db.tasksDao.watchTasksInRoom(roomId).first;
|
||||
expect(result.first.name, 'Geschirr spuelen');
|
||||
expect(result.first.description, 'Mit Spuelmittel');
|
||||
expect(result.first.intervalType, IntervalType.weekly);
|
||||
expect(result.first.effortLevel, EffortLevel.medium);
|
||||
});
|
||||
|
||||
test('deleteTask removes the task', () async {
|
||||
final id = await db.tasksDao.insertTask(
|
||||
TasksCompanion.insert(
|
||||
roomId: roomId,
|
||||
name: 'Abspuelen',
|
||||
intervalType: IntervalType.daily,
|
||||
effortLevel: EffortLevel.low,
|
||||
nextDueDate: DateTime(2026, 3, 15),
|
||||
),
|
||||
);
|
||||
|
||||
await db.tasksDao.deleteTask(id);
|
||||
|
||||
final tasks = await db.tasksDao.watchTasksInRoom(roomId).first;
|
||||
expect(tasks, isEmpty);
|
||||
});
|
||||
|
||||
test('completeTask records completion and updates nextDueDate', () async {
|
||||
final id = await db.tasksDao.insertTask(
|
||||
TasksCompanion.insert(
|
||||
roomId: roomId,
|
||||
name: 'Abspuelen',
|
||||
intervalType: IntervalType.daily,
|
||||
effortLevel: EffortLevel.low,
|
||||
nextDueDate: DateTime(2026, 3, 15),
|
||||
),
|
||||
);
|
||||
|
||||
await db.tasksDao.completeTask(id, now: DateTime(2026, 3, 15));
|
||||
|
||||
// Check that the next due date was updated (daily: +1 day)
|
||||
final tasks = await db.tasksDao.watchTasksInRoom(roomId).first;
|
||||
expect(tasks.first.nextDueDate, DateTime(2026, 3, 16));
|
||||
});
|
||||
|
||||
test('completeTask with overdue task catches up to present', () async {
|
||||
// Task was due 2 weeks ago with weekly interval
|
||||
final id = await db.tasksDao.insertTask(
|
||||
TasksCompanion.insert(
|
||||
roomId: roomId,
|
||||
name: 'Staubsaugen',
|
||||
intervalType: IntervalType.weekly,
|
||||
effortLevel: EffortLevel.medium,
|
||||
nextDueDate: DateTime(2026, 3, 1),
|
||||
),
|
||||
);
|
||||
|
||||
// Complete on March 15
|
||||
await db.tasksDao.completeTask(id, now: DateTime(2026, 3, 15));
|
||||
|
||||
// Should catch up: Mar 1 -> Mar 8 (still past) -> Mar 15 (equals today, done)
|
||||
final tasks = await db.tasksDao.watchTasksInRoom(roomId).first;
|
||||
final nextDue = tasks.first.nextDueDate;
|
||||
// Next due should be on or after Mar 15
|
||||
expect(
|
||||
nextDue.isAfter(DateTime(2026, 3, 14)) ||
|
||||
nextDue.isAtSameMomentAs(DateTime(2026, 3, 15)),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('tasks with nextDueDate before today are detected as overdue', () async {
|
||||
// Insert an overdue task
|
||||
await db.tasksDao.insertTask(
|
||||
TasksCompanion.insert(
|
||||
roomId: roomId,
|
||||
name: 'Overdue Task',
|
||||
intervalType: IntervalType.weekly,
|
||||
effortLevel: EffortLevel.medium,
|
||||
nextDueDate: DateTime(2026, 3, 10),
|
||||
),
|
||||
);
|
||||
|
||||
// Insert a task due today (not overdue)
|
||||
await db.tasksDao.insertTask(
|
||||
TasksCompanion.insert(
|
||||
roomId: roomId,
|
||||
name: 'Today Task',
|
||||
intervalType: IntervalType.daily,
|
||||
effortLevel: EffortLevel.low,
|
||||
nextDueDate: DateTime(2026, 3, 15),
|
||||
),
|
||||
);
|
||||
|
||||
// Insert a future task (not overdue)
|
||||
await db.tasksDao.insertTask(
|
||||
TasksCompanion.insert(
|
||||
roomId: roomId,
|
||||
name: 'Future Task',
|
||||
intervalType: IntervalType.monthly,
|
||||
effortLevel: EffortLevel.low,
|
||||
nextDueDate: DateTime(2026, 3, 20),
|
||||
),
|
||||
);
|
||||
|
||||
final overdueCount = await db.tasksDao.getOverdueTaskCount(
|
||||
roomId,
|
||||
today: DateTime(2026, 3, 15),
|
||||
);
|
||||
// Only the task due Mar 10 is overdue (before Mar 15)
|
||||
expect(overdueCount, 1);
|
||||
});
|
||||
});
|
||||
}
|
||||
156
test/features/tasks/domain/scheduling_test.dart
Normal file
156
test/features/tasks/domain/scheduling_test.dart
Normal file
@@ -0,0 +1,156 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:household_keeper/features/tasks/domain/frequency.dart';
|
||||
import 'package:household_keeper/features/tasks/domain/relative_date.dart';
|
||||
import 'package:household_keeper/features/tasks/domain/scheduling.dart';
|
||||
|
||||
void main() {
|
||||
group('calculateNextDueDate', () {
|
||||
test('daily adds 1 day', () {
|
||||
final result = calculateNextDueDate(
|
||||
currentDueDate: DateTime(2026, 3, 15),
|
||||
intervalType: IntervalType.daily,
|
||||
intervalDays: 1,
|
||||
);
|
||||
expect(result, DateTime(2026, 3, 16));
|
||||
});
|
||||
|
||||
test('everyNDays(3) adds 3 days', () {
|
||||
final result = calculateNextDueDate(
|
||||
currentDueDate: DateTime(2026, 3, 15),
|
||||
intervalType: IntervalType.everyNDays,
|
||||
intervalDays: 3,
|
||||
);
|
||||
expect(result, DateTime(2026, 3, 18));
|
||||
});
|
||||
|
||||
test('weekly adds 7 days', () {
|
||||
final result = calculateNextDueDate(
|
||||
currentDueDate: DateTime(2026, 3, 15),
|
||||
intervalType: IntervalType.weekly,
|
||||
intervalDays: 1,
|
||||
);
|
||||
expect(result, DateTime(2026, 3, 22));
|
||||
});
|
||||
|
||||
test('biweekly adds 14 days', () {
|
||||
final result = calculateNextDueDate(
|
||||
currentDueDate: DateTime(2026, 3, 15),
|
||||
intervalType: IntervalType.biweekly,
|
||||
intervalDays: 1,
|
||||
);
|
||||
expect(result, DateTime(2026, 3, 29));
|
||||
});
|
||||
|
||||
test('monthly from Jan 15 gives Feb 15', () {
|
||||
final result = calculateNextDueDate(
|
||||
currentDueDate: DateTime(2026, 1, 15),
|
||||
intervalType: IntervalType.monthly,
|
||||
intervalDays: 1,
|
||||
);
|
||||
expect(result, DateTime(2026, 2, 15));
|
||||
});
|
||||
|
||||
test('monthly from Jan 31 gives Feb 28 (clamping)', () {
|
||||
final result = calculateNextDueDate(
|
||||
currentDueDate: DateTime(2026, 1, 31),
|
||||
intervalType: IntervalType.monthly,
|
||||
intervalDays: 1,
|
||||
);
|
||||
expect(result, DateTime(2026, 2, 28));
|
||||
});
|
||||
|
||||
test('monthly from Feb 28 (anchor 31) gives Mar 31 (anchor memory)', () {
|
||||
final result = calculateNextDueDate(
|
||||
currentDueDate: DateTime(2026, 2, 28),
|
||||
intervalType: IntervalType.monthly,
|
||||
intervalDays: 1,
|
||||
anchorDay: 31,
|
||||
);
|
||||
expect(result, DateTime(2026, 3, 31));
|
||||
});
|
||||
|
||||
test('quarterly from Jan 15 gives Apr 15', () {
|
||||
final result = calculateNextDueDate(
|
||||
currentDueDate: DateTime(2026, 1, 15),
|
||||
intervalType: IntervalType.quarterly,
|
||||
intervalDays: 1,
|
||||
);
|
||||
expect(result, DateTime(2026, 4, 15));
|
||||
});
|
||||
|
||||
test('yearly from 2026-03-15 gives 2027-03-15', () {
|
||||
final result = calculateNextDueDate(
|
||||
currentDueDate: DateTime(2026, 3, 15),
|
||||
intervalType: IntervalType.yearly,
|
||||
intervalDays: 1,
|
||||
);
|
||||
expect(result, DateTime(2027, 3, 15));
|
||||
});
|
||||
|
||||
test('everyNMonths(2) adds 2 months', () {
|
||||
final result = calculateNextDueDate(
|
||||
currentDueDate: DateTime(2026, 1, 15),
|
||||
intervalType: IntervalType.everyNMonths,
|
||||
intervalDays: 2,
|
||||
);
|
||||
expect(result, DateTime(2026, 3, 15));
|
||||
});
|
||||
});
|
||||
|
||||
group('catchUpToPresent', () {
|
||||
test('skips past occurrences to reach today or future', () {
|
||||
// Task was due Jan 1, weekly interval. Today is Mar 15.
|
||||
// Should advance past all missed weeks to the next week on/after Mar 15.
|
||||
final result = catchUpToPresent(
|
||||
nextDue: DateTime(2026, 1, 1),
|
||||
today: DateTime(2026, 3, 15),
|
||||
intervalType: IntervalType.weekly,
|
||||
intervalDays: 1,
|
||||
);
|
||||
// Jan 1 + 11 weeks = Mar 19 (first Thursday on/after Mar 15)
|
||||
expect(result.isAfter(DateTime(2026, 3, 14)), isTrue);
|
||||
expect(result.isBefore(DateTime(2026, 3, 22)), isTrue);
|
||||
});
|
||||
|
||||
test('returns date unchanged if already in future', () {
|
||||
final futureDate = DateTime(2026, 4, 1);
|
||||
final result = catchUpToPresent(
|
||||
nextDue: futureDate,
|
||||
today: DateTime(2026, 3, 15),
|
||||
intervalType: IntervalType.weekly,
|
||||
intervalDays: 1,
|
||||
);
|
||||
expect(result, futureDate);
|
||||
});
|
||||
});
|
||||
|
||||
group('formatRelativeDate', () {
|
||||
final today = DateTime(2026, 3, 15);
|
||||
|
||||
test('today returns "Heute"', () {
|
||||
expect(formatRelativeDate(DateTime(2026, 3, 15), today), 'Heute');
|
||||
});
|
||||
|
||||
test('tomorrow returns "Morgen"', () {
|
||||
expect(formatRelativeDate(DateTime(2026, 3, 16), today), 'Morgen');
|
||||
});
|
||||
|
||||
test('3 days from now returns "in 3 Tagen"', () {
|
||||
expect(formatRelativeDate(DateTime(2026, 3, 18), today), 'in 3 Tagen');
|
||||
});
|
||||
|
||||
test('1 day overdue returns "Uberfaellig seit 1 Tag"', () {
|
||||
expect(
|
||||
formatRelativeDate(DateTime(2026, 3, 14), today),
|
||||
'Uberfaellig seit 1 Tag',
|
||||
);
|
||||
});
|
||||
|
||||
test('5 days overdue returns "Uberfaellig seit 5 Tagen"', () {
|
||||
expect(
|
||||
formatRelativeDate(DateTime(2026, 3, 10), today),
|
||||
'Uberfaellig seit 5 Tagen',
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user