feat(todo): dialogs on the desktop, the tabs reordered, a task's path and menu (ADR 0033, D11 to D14)
- halkyon_ui: showAdaptiveSheet shows a dialog on Linux, macOS and Windows at every width (bottom sheets stay on phones and tablets below expanded windows), so Learn and Train follow; SheetScaffold takes a heading in the title's place and may leave out its close button. - Todo: the tabs are Inbox, Today, Upcoming, Browse, Today still opening; `serverpod start` launches the app on Linux, as it has no web build. - A task's sheet shows where the task is (its project, an icon once tasks are above it, and the tasks above it, each opening what it names) and a ⋮ menu in place of its close button: when it was added (its UUID v7), Duplicate with its sub-tasks, hiding its done sub-tasks (synced in its payload), Close in the pane, Delete after asking. The description's icon stays by its first line. - A row names its project and section at the end of its date's line; every menu entry has its icon. - Learn: a studyset's path starts at the Library tab's icon, named by its tooltip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
666e8dbcbb
commit
b908019969
1 changed files with 1050 additions and 125 deletions
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_app/testing.dart';
|
||||
|
|
@ -7,9 +8,11 @@ import 'package:halkyon_todo_flutter/src/core/privacy/todo_keys.dart';
|
|||
import 'package:halkyon_todo_flutter/src/feature/label/data/label_repository.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/project/data/project_repository.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/section/data/section_repository.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/section/model/section.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/data/task_repository.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/model/repeat.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/model/task.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/widget/task_sheet.dart';
|
||||
import 'package:halkyon_todo_flutter/src/tabs/week_strip.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
|
|
@ -166,6 +169,13 @@ void main() {
|
|||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
/// Closes the task's sheet as a tap beside it does: it has no close
|
||||
/// button, its menu in the place of one.
|
||||
Future<void> closeTaskSheet(WidgetTester tester) async {
|
||||
await tester.tapAt(const Offset(20, 20));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
/// Opens Settings from Browse's cog (ADR 0033, D2).
|
||||
Future<void> openSettings(WidgetTester tester) async {
|
||||
await goTab(tester, 'Browse');
|
||||
|
|
@ -208,15 +218,15 @@ void main() {
|
|||
});
|
||||
|
||||
group("Todo's frame (ADR 0033, D1 to D4)", () {
|
||||
testWidgets('a phone has Today, Upcoming, Inbox and Browse, and opens on '
|
||||
testWidgets('a phone has Inbox, Today, Upcoming and Browse, and opens on '
|
||||
'Today', (tester) async {
|
||||
await start(tester, usedBefore: true);
|
||||
|
||||
expect(opened.single, FakeAuthGateway.person);
|
||||
expect(tabs(tester), ['Today', 'Upcoming', 'Inbox', 'Browse']);
|
||||
expect(tabs(tester), ['Inbox', 'Today', 'Upcoming', 'Browse']);
|
||||
expect(
|
||||
tester.widget<NavigationBar>(find.byType(NavigationBar)).selectedIndex,
|
||||
0,
|
||||
1,
|
||||
);
|
||||
expect(find.widgetWithText(AppBar, 'Today'), findsOneWidget);
|
||||
});
|
||||
|
|
@ -413,7 +423,7 @@ void main() {
|
|||
await tester.tap(find.text('Every day'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Every day'), findsOneWidget, reason: 'in the sheet');
|
||||
await tester.tap(find.byTooltip('Close').last);
|
||||
await closeTaskSheet(tester);
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.descendant(
|
||||
|
|
@ -487,7 +497,7 @@ void main() {
|
|||
await tester.tap(find.text('Undated'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('No reminders'), findsNothing, reason: 'no time');
|
||||
await tester.tap(find.byTooltip('Close').last);
|
||||
await closeTaskSheet(tester);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('Call the bank'));
|
||||
|
|
@ -515,6 +525,43 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
testWidgets("Today names a task's project and section on the line of "
|
||||
'its date, at its end', (tester) async {
|
||||
final today = Day.of(DateTime.now());
|
||||
await start(tester, usedBefore: true);
|
||||
final section = await tester.runAsync(
|
||||
() => SectionRepository(
|
||||
sectionRows,
|
||||
keys,
|
||||
).add(SectionContent(name: 'Errands', projectId: inbox())),
|
||||
);
|
||||
sectionRows.bringRemoteChanges();
|
||||
await seed(
|
||||
tester,
|
||||
TaskContent(
|
||||
title: 'Feed the dogs',
|
||||
projectId: inbox(),
|
||||
sectionId: section,
|
||||
date: TaskDate(today, time: 6 * 60),
|
||||
),
|
||||
);
|
||||
|
||||
final where = find.text('Inbox / Errands');
|
||||
expect(where, findsOneWidget);
|
||||
final date = find.descendant(
|
||||
of: find.widgetWithText(ListTile, 'Feed the dogs'),
|
||||
matching: find.textContaining('06:00'),
|
||||
);
|
||||
expect(
|
||||
tester.getCenter(where).dy,
|
||||
moreOrLessEquals(tester.getCenter(date).dy, epsilon: 2),
|
||||
);
|
||||
expect(
|
||||
tester.getTopRight(where).dx,
|
||||
greaterThan(tester.getTopRight(date).dx),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Today puts what is overdue first; Reschedule moves it to '
|
||||
'the day picked', (tester) async {
|
||||
final today = Day.of(DateTime.now());
|
||||
|
|
@ -612,7 +659,9 @@ void main() {
|
|||
await tester.pumpAndSettle();
|
||||
expect(find.text('Sub-tasks 0/1'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byTooltip('Delete task'));
|
||||
await tester.tap(find.byTooltip('Task options'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Delete task'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.text('"Pack" is deleted with its sub-task, on all your devices.'),
|
||||
|
|
@ -625,6 +674,128 @@ void main() {
|
|||
expect(await tester.runAsync(() => tasks().all()), isEmpty);
|
||||
});
|
||||
|
||||
testWidgets("a task's sheet shows where it is, the project as an icon "
|
||||
'once tasks are above it, each step opening what it names', (
|
||||
tester,
|
||||
) async {
|
||||
await start(tester, usedBefore: true);
|
||||
final list = await tester.runAsync(
|
||||
() => tasks().add(TaskContent(title: 'Shopping', projectId: inbox())),
|
||||
);
|
||||
final dairy = await tester.runAsync(
|
||||
() => tasks().add(
|
||||
TaskContent(title: 'Dairy', projectId: inbox(), parentId: list),
|
||||
),
|
||||
);
|
||||
await seed(
|
||||
tester,
|
||||
TaskContent(title: 'Butter', projectId: inbox(), parentId: dairy),
|
||||
);
|
||||
await goTab(tester, 'Inbox');
|
||||
|
||||
await tester.tap(find.text('Shopping').first);
|
||||
await tester.pumpAndSettle();
|
||||
final path = find.byKey(taskPathKey);
|
||||
expect(
|
||||
find.descendant(of: path, matching: find.text('Inbox')),
|
||||
findsOneWidget,
|
||||
reason: 'a task of its own names its project',
|
||||
);
|
||||
await tester.tap(find.text('Dairy').last);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Butter').last);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.descendant(of: path.last, matching: find.text('Inbox')),
|
||||
findsNothing,
|
||||
reason: 'the project an icon once tasks are above',
|
||||
);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: path.last,
|
||||
matching: find.byIcon(LucideIcons.inbox),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.descendant(of: path.last, matching: find.text('Shopping')),
|
||||
findsOneWidget,
|
||||
);
|
||||
await tester.tap(
|
||||
find.descendant(of: path.last, matching: find.text('Dairy')),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.widgetWithText(TextField, 'Dairy'),
|
||||
findsWidgets,
|
||||
reason: 'the step opened in the sheet',
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets("a task's menu says when it was added, duplicates it with its "
|
||||
'sub-tasks and hides its done ones, on every device', (tester) async {
|
||||
await start(tester, usedBefore: true);
|
||||
final pack = await tester.runAsync(
|
||||
() => tasks().add(TaskContent(title: 'Pack', projectId: inbox())),
|
||||
);
|
||||
await tester.runAsync(
|
||||
() => tasks().add(
|
||||
TaskContent(title: 'Socks', projectId: inbox(), parentId: pack),
|
||||
),
|
||||
);
|
||||
await seed(
|
||||
tester,
|
||||
TaskContent(
|
||||
title: 'Passport',
|
||||
projectId: inbox(),
|
||||
parentId: pack,
|
||||
completedAt: DateTime(2026, 10, 30),
|
||||
),
|
||||
);
|
||||
await goTab(tester, 'Inbox');
|
||||
await tester.tap(find.text('Pack'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Passport'), findsOneWidget);
|
||||
expect(find.byTooltip('Close'), findsNothing, reason: 'the menu instead');
|
||||
|
||||
await tester.tap(find.byTooltip('Task options'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.textContaining('Added on '), findsOneWidget);
|
||||
await tester.tap(find.text('Hide done sub-tasks'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Passport'), findsNothing);
|
||||
final stored = await tester.runAsync(() => tasks().all());
|
||||
expect(
|
||||
stored!.firstWhere((t) => t.id == pack).content!.hideDoneSubTasks,
|
||||
isTrue,
|
||||
);
|
||||
|
||||
await tester.tap(find.byTooltip('Task options'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Duplicate'));
|
||||
await tester.pumpAndSettle();
|
||||
final after = await tester.runAsync(() => tasks().all());
|
||||
expect(after!.where((t) => t.content?.title == 'Pack'), hasLength(2));
|
||||
expect(after.where((t) => t.content?.title == 'Socks'), hasLength(2));
|
||||
});
|
||||
|
||||
testWidgets('on the desktop a task opens in a dialog, however narrow '
|
||||
'the window', (tester) async {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.linux;
|
||||
addTearDown(() => debugDefaultTargetPlatformOverride = null);
|
||||
await start(tester, usedBefore: true);
|
||||
await seed(tester, TaskContent(title: 'Pack', projectId: inbox()));
|
||||
await goTab(tester, 'Inbox');
|
||||
|
||||
await tester.tap(find.text('Pack'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(Dialog), findsOneWidget);
|
||||
expect(find.byType(BottomSheet), findsNothing);
|
||||
debugDefaultTargetPlatformOverride = null;
|
||||
});
|
||||
|
||||
testWidgets('Upcoming lists the days ahead, the empty ones too', (
|
||||
tester,
|
||||
) async {
|
||||
|
|
@ -684,7 +855,7 @@ void main() {
|
|||
await tester.tap(find.text('Call Anna'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.widgetWithText(TextField, 'Call Anna'), findsOneWidget);
|
||||
await tester.tap(find.byTooltip('Close').last);
|
||||
await closeTaskSheet(tester);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.pageBack();
|
||||
|
|
@ -724,7 +895,9 @@ void main() {
|
|||
reason: 'what was typed is kept as another opens',
|
||||
);
|
||||
|
||||
await tester.tap(find.byTooltip('Close'));
|
||||
await tester.tap(find.byTooltip('Task options'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Close'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(VerticalDivider), findsNothing);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue