feat(todo): 3-day grid's blocks dragged to another day and time, or onto All day (ADR 0033, D17)

A block held and dragged moves its task to another of the three days
shown and another time, its top snapping to a quarter hour, the time it
would take shown under the pointer, its duration, recurrence and
reminders kept; dropped on a day's All day it loses its time and
duration. Either can be undone for a moment. A later time of a repeating
task doesn't move. DayDropArea's move is now moveTask, shared by both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-01 20:58:12 +01:00
commit 0373f723e7
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -810,6 +810,140 @@ void main() {
expect(date.duration, const Duration(minutes: 30));
});
group('a block held and dragged', () {
/// Holds [from], moves to [to] in steps and lets go.
Future<void> drag(WidgetTester tester, Offset from, Offset to) async {
final gesture = await tester.startGesture(from);
await tester.pump(
kLongPressTimeout + const Duration(milliseconds: 50),
);
for (var step = 1; step <= 10; step++) {
await gesture.moveTo(Offset.lerp(from, to, step / 10)!);
await tester.pump();
}
await gesture.up();
await tester.pumpAndSettle();
}
/// Tomorrow's 9:00 dentist, an hour long, weekly, in the 3-day grid;
/// its ID.
Future<String> dentist(WidgetTester tester) async {
final today = Day.of(DateTime.now());
await start(tester, usedBefore: true);
await seed(
tester,
TaskContent(
title: 'Dentist',
projectId: inbox(),
date: TaskDate(
today.plus(1),
time: 9 * 60,
duration: const Duration(hours: 1),
repeat: const Repeat(RepeatUnit.week),
),
),
);
await threeDay(tester);
final stored = await tester.runAsync(() => tasks().all());
return stored!.single.id;
}
Future<TaskDate> dateOf(WidgetTester tester) async {
final stored = await tester.runAsync(() => tasks().all());
return stored!.single.content!.date!;
}
testWidgets('onto another day and hour moves its task there, its '
'duration and recurrence kept, the place under the pointer '
'snapping to a quarter hour; which can be undone', (tester) async {
final id = await dentist(tester);
final today = Day.of(DateTime.now());
final block = find.byKey(ThreeDayGrid.blockKey(id, today.plus(1)));
await tester.ensureVisible(block);
await tester.pumpAndSettle();
// Held 20 minutes below its top, dropped where 14:27 is under the
// pointer: its top at 14:07, which snaps to 14:00.
final from = tester.getTopLeft(block) + const Offset(10, 20);
final column = tester.getTopLeft(
find.byKey(ThreeDayGrid.columnKey(today)),
);
await drag(tester, from, column + const Offset(20, 14 * 60 + 27));
expect(
find.byKey(quickAddTitleKey),
findsNothing,
reason: 'the hold a drag, not a new task',
);
final moved = await dateOf(tester);
expect(moved.day, today);
expect(moved.time, 14 * 60);
expect(moved.duration, const Duration(hours: 1));
expect(moved.repeat, const Repeat(RepeatUnit.week));
expect(find.byKey(ThreeDayGrid.blockKey(id, today)), findsOneWidget);
await tester.tap(find.text('Undo'));
await tester.pumpAndSettle();
final back = await dateOf(tester);
expect(back.day, today.plus(1));
expect(back.time, 9 * 60);
});
testWidgets('onto a day’s All day takes its time away', (tester) async {
final id = await dentist(tester);
final today = Day.of(DateTime.now());
final block = find.byKey(ThreeDayGrid.blockKey(id, today.plus(1)));
await tester.ensureVisible(block);
await tester.pumpAndSettle();
final from = tester.getTopLeft(block) + const Offset(10, 20);
final allDay = tester.getCenter(
find.byKey(ThreeDayGrid.allDayKey(today.plus(2))),
);
await drag(tester, from, allDay);
final moved = await dateOf(tester);
expect(moved.day, today.plus(2));
expect(moved.time, isNull);
expect(moved.duration, isNull);
expect(moved.repeat, const Repeat(RepeatUnit.week));
});
testWidgets('a later time of a repeating task stays where it is', (
tester,
) async {
final id = await dentist(tester);
await tester.tap(find.byTooltip('Display'));
await tester.pumpAndSettle();
await tester.tap(
find.widgetWithText(SwitchListTile, 'Future occurrences'),
);
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(FilledButton, 'Done'));
await tester.pumpAndSettle();
// Today's grid shows tomorrow and the day after; the next time is
// a week on.
await tester.tap(find.byTooltip('Later days'));
await tester.pumpAndSettle();
await tester.tap(find.byTooltip('Later days'));
await tester.pumpAndSettle();
final today = Day.of(DateTime.now());
final later = find.byKey(ThreeDayGrid.blockKey(id, today.plus(8)));
await tester.ensureVisible(later);
await tester.pumpAndSettle();
final column = tester.getTopLeft(
find.byKey(ThreeDayGrid.columnKey(today.plus(6))),
);
await drag(
tester,
tester.getTopLeft(later) + const Offset(10, 20),
column + const Offset(20, 14 * 60),
);
final date = await dateOf(tester);
expect(date.day, today.plus(1));
expect(date.time, 9 * 60);
});
});
testWidgets('its filters keep the agenda to what they ask for; Reset '
'all puts it back', (tester) async {
await start(tester, usedBefore: true);