2026-10-18 21:46:55 +02:00
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
|
|
import 'package:halkyon_ui/halkyon_ui.dart';
|
|
|
|
|
|
import 'package:material_ui/material_ui.dart';
|
|
|
|
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
|
|
/// Opens a sheet titled "New studyset" with [body] and a Create button, in
|
|
|
|
|
|
/// a window [width] wide, with a status bar [statusBar] high on top.
|
|
|
|
|
|
Future<void> open(
|
|
|
|
|
|
WidgetTester tester, {
|
|
|
|
|
|
required double width,
|
|
|
|
|
|
Widget body = const Text('Body'),
|
|
|
|
|
|
double statusBar = 0,
|
|
|
|
|
|
bool busy = false,
|
|
|
|
|
|
}) async {
|
|
|
|
|
|
tester.view.physicalSize = Size(width, 900);
|
|
|
|
|
|
tester.view.devicePixelRatio = 1;
|
|
|
|
|
|
tester.view.padding = FakeViewPadding(top: statusBar);
|
|
|
|
|
|
tester.view.viewPadding = FakeViewPadding(top: statusBar);
|
|
|
|
|
|
addTearDown(tester.view.reset);
|
|
|
|
|
|
await tester.pumpWidget(
|
|
|
|
|
|
MaterialApp(
|
|
|
|
|
|
localizationsDelegates: HalkyonUiLocalizations.localizationsDelegates,
|
|
|
|
|
|
supportedLocales: HalkyonUiLocalizations.supportedLocales,
|
|
|
|
|
|
home: Scaffold(
|
|
|
|
|
|
body: Builder(
|
|
|
|
|
|
builder: (context) => TextButton(
|
|
|
|
|
|
onPressed: () => showAdaptiveSheet<void>(
|
|
|
|
|
|
context,
|
|
|
|
|
|
swipeable: !busy,
|
|
|
|
|
|
builder: (context) => SheetScaffold(
|
|
|
|
|
|
title: 'New studyset',
|
|
|
|
|
|
busy: busy,
|
|
|
|
|
|
body: body,
|
|
|
|
|
|
footer: FilledButton(
|
|
|
|
|
|
onPressed: () {},
|
|
|
|
|
|
child: const Text('Create'),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
child: const Text('Open'),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
await tester.tap(find.text('Open'));
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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>
2026-11-01 16:53:38 +01:00
|
|
|
|
testWidgets('is a bottom sheet below expanded windows on a phone or a '
|
|
|
|
|
|
'tablet', (tester) async {
|
2026-10-18 21:46:55 +02:00
|
|
|
|
await open(tester, width: 839);
|
|
|
|
|
|
|
|
|
|
|
|
expect(find.byType(BottomSheet), findsOneWidget);
|
|
|
|
|
|
expect(find.byType(Dialog), findsNothing);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
testWidgets('is a dialog from expanded windows on', (tester) async {
|
|
|
|
|
|
await open(tester, width: 840);
|
|
|
|
|
|
|
|
|
|
|
|
expect(find.byType(Dialog), findsOneWidget);
|
|
|
|
|
|
expect(find.byType(BottomSheet), findsNothing);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-01 18:20:58 +01:00
|
|
|
|
testWidgets('sheetShowsAsDialog says which a sheet of its own will be: '
|
|
|
|
|
|
'a dialog from expanded windows on, and on the desktop at every width', (
|
|
|
|
|
|
tester,
|
|
|
|
|
|
) async {
|
|
|
|
|
|
Future<bool> at(double width) async {
|
|
|
|
|
|
tester.view.physicalSize = Size(width, 900);
|
|
|
|
|
|
tester.view.devicePixelRatio = 1;
|
|
|
|
|
|
late bool shows;
|
|
|
|
|
|
await tester.pumpWidget(
|
|
|
|
|
|
MaterialApp(
|
|
|
|
|
|
home: Builder(
|
|
|
|
|
|
builder: (context) {
|
|
|
|
|
|
shows = sheetShowsAsDialog(context);
|
|
|
|
|
|
return const SizedBox();
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
return shows;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
addTearDown(tester.view.reset);
|
|
|
|
|
|
expect(await at(400), isFalse);
|
|
|
|
|
|
expect(await at(840), isTrue);
|
|
|
|
|
|
debugDefaultTargetPlatformOverride = TargetPlatform.linux;
|
|
|
|
|
|
addTearDown(() => debugDefaultTargetPlatformOverride = null);
|
|
|
|
|
|
expect(await at(400), isTrue);
|
|
|
|
|
|
debugDefaultTargetPlatformOverride = null;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
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>
2026-11-01 16:53:38 +01:00
|
|
|
|
for (final platform in [
|
|
|
|
|
|
TargetPlatform.linux,
|
|
|
|
|
|
TargetPlatform.macOS,
|
|
|
|
|
|
TargetPlatform.windows,
|
|
|
|
|
|
]) {
|
|
|
|
|
|
testWidgets('is a dialog on the desktop however narrow the window '
|
|
|
|
|
|
'(${platform.name})', (tester) async {
|
|
|
|
|
|
debugDefaultTargetPlatformOverride = platform;
|
|
|
|
|
|
addTearDown(() => debugDefaultTargetPlatformOverride = null);
|
|
|
|
|
|
await open(tester, width: 400);
|
|
|
|
|
|
|
|
|
|
|
|
expect(find.byType(Dialog), findsOneWidget);
|
|
|
|
|
|
expect(find.byType(BottomSheet), findsNothing);
|
|
|
|
|
|
debugDefaultTargetPlatformOverride = null;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-18 21:46:55 +02:00
|
|
|
|
for (final width in [400.0, 1200.0]) {
|
|
|
|
|
|
testWidgets('keeps its close button and footer in view above a long body '
|
|
|
|
|
|
'(${width.round()} wide)', (tester) async {
|
|
|
|
|
|
await open(
|
|
|
|
|
|
tester,
|
|
|
|
|
|
width: width,
|
|
|
|
|
|
body: Column(
|
|
|
|
|
|
children: [for (var i = 0; i < 80; i++) Text('Line $i')],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
expect(find.byTooltip('Close').hitTestable(), findsOneWidget);
|
|
|
|
|
|
expect(find.text('Create').hitTestable(), findsOneWidget);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
testWidgets('stays below the status bar with the keyboard open', (
|
|
|
|
|
|
tester,
|
|
|
|
|
|
) async {
|
|
|
|
|
|
await open(
|
|
|
|
|
|
tester,
|
|
|
|
|
|
width: 400,
|
|
|
|
|
|
statusBar: 48,
|
|
|
|
|
|
body: Column(
|
|
|
|
|
|
children: [for (var i = 0; i < 80; i++) Text('Line $i')],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
tester.view.viewInsets = const FakeViewPadding(bottom: 400);
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
|
|
expect(tester.getTopLeft(find.byType(BottomSheet)).dy, 48);
|
|
|
|
|
|
expect(find.byTooltip('Close').hitTestable(), findsOneWidget);
|
|
|
|
|
|
expect(find.text('Create').hitTestable(), findsOneWidget);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
testWidgets('closes with its close button', (tester) async {
|
|
|
|
|
|
await open(tester, width: 400);
|
|
|
|
|
|
|
|
|
|
|
|
await tester.tap(find.byTooltip('Close'));
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
|
|
expect(find.text('New studyset'), findsNothing);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
testWidgets('while busy, its close button is off and back keeps it open', (
|
|
|
|
|
|
tester,
|
|
|
|
|
|
) async {
|
|
|
|
|
|
await open(tester, width: 400, busy: true);
|
|
|
|
|
|
|
|
|
|
|
|
expect(
|
|
|
|
|
|
tester
|
|
|
|
|
|
.widget<IconButton>(
|
|
|
|
|
|
find.ancestor(
|
|
|
|
|
|
of: find.byTooltip('Close'),
|
|
|
|
|
|
matching: find.byType(IconButton),
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
.onPressed,
|
|
|
|
|
|
isNull,
|
|
|
|
|
|
);
|
|
|
|
|
|
await tester.binding.handlePopRoute();
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
expect(find.text('New studyset'), findsOneWidget);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
group('a body longer than the sheet', () {
|
|
|
|
|
|
final tall = Column(
|
|
|
|
|
|
children: [for (var i = 0; i < 40; i++) const Text('Line')],
|
|
|
|
|
|
);
|
|
|
|
|
|
Finder below() => find.byKey(SheetScaffold.moreBelowKey);
|
|
|
|
|
|
Finder above() => find.byKey(SheetScaffold.moreAboveKey);
|
|
|
|
|
|
|
|
|
|
|
|
testWidgets('shows a line above the buttons while more lies below, and '
|
|
|
|
|
|
'one under the title while more lies above', (tester) async {
|
|
|
|
|
|
// As a dialog: a modal bottom sheet takes a test's drag itself.
|
|
|
|
|
|
await open(tester, width: 1000, body: tall);
|
|
|
|
|
|
|
|
|
|
|
|
expect(below(), findsOneWidget);
|
|
|
|
|
|
expect(above(), findsNothing);
|
|
|
|
|
|
|
|
|
|
|
|
await tester.drag(
|
|
|
|
|
|
find.byType(SingleChildScrollView),
|
|
|
|
|
|
const Offset(0, -300),
|
|
|
|
|
|
);
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
expect(below(), findsOneWidget);
|
|
|
|
|
|
expect(above(), findsOneWidget);
|
|
|
|
|
|
expect(
|
|
|
|
|
|
tester
|
|
|
|
|
|
.state<ScrollableState>(
|
|
|
|
|
|
find.descendant(
|
|
|
|
|
|
of: find.byType(SingleChildScrollView),
|
|
|
|
|
|
matching: find.byType(Scrollable),
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
.position
|
|
|
|
|
|
.pixels,
|
|
|
|
|
|
greaterThan(0),
|
|
|
|
|
|
reason: 'the line coming keeps where the body was scrolled to',
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
await tester.drag(
|
|
|
|
|
|
find.byType(SingleChildScrollView),
|
|
|
|
|
|
const Offset(0, -5000),
|
|
|
|
|
|
);
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
expect(below(), findsNothing);
|
|
|
|
|
|
expect(above(), findsOneWidget);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
testWidgets('has a scrollbar, always shown on the desktop', (tester) async {
|
|
|
|
|
|
debugDefaultTargetPlatformOverride = TargetPlatform.linux;
|
|
|
|
|
|
addTearDown(() => debugDefaultTargetPlatformOverride = null);
|
|
|
|
|
|
await open(tester, width: 1000, body: tall);
|
|
|
|
|
|
|
|
|
|
|
|
final scrollbar = tester.widget<Scrollbar>(find.byType(Scrollbar));
|
|
|
|
|
|
expect(scrollbar.thumbVisibility, isTrue);
|
|
|
|
|
|
debugDefaultTargetPlatformOverride = null;
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
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>
2026-11-01 16:53:38 +01:00
|
|
|
|
testWidgets("a heading of its own takes the title's place, and the close "
|
|
|
|
|
|
'button may give way to its action', (tester) async {
|
|
|
|
|
|
tester.view.physicalSize = const Size(400, 900);
|
|
|
|
|
|
tester.view.devicePixelRatio = 1;
|
|
|
|
|
|
addTearDown(tester.view.reset);
|
|
|
|
|
|
await tester.pumpWidget(
|
|
|
|
|
|
MaterialApp(
|
|
|
|
|
|
localizationsDelegates: HalkyonUiLocalizations.localizationsDelegates,
|
|
|
|
|
|
supportedLocales: HalkyonUiLocalizations.supportedLocales,
|
|
|
|
|
|
home: Scaffold(
|
|
|
|
|
|
body: Builder(
|
|
|
|
|
|
builder: (context) => TextButton(
|
|
|
|
|
|
onPressed: () => showAdaptiveSheet<void>(
|
|
|
|
|
|
context,
|
|
|
|
|
|
builder: (context) => const SheetScaffold(
|
|
|
|
|
|
title: 'Task',
|
|
|
|
|
|
heading: Text('Inbox › Shopping'),
|
|
|
|
|
|
closeButton: false,
|
|
|
|
|
|
action: Text('More'),
|
|
|
|
|
|
body: Text('Body'),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
child: const Text('Open'),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
await tester.tap(find.text('Open'));
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
|
|
expect(find.text('Inbox › Shopping'), findsOneWidget);
|
|
|
|
|
|
expect(find.text('Task'), findsNothing);
|
|
|
|
|
|
expect(find.byTooltip('Close'), findsNothing);
|
|
|
|
|
|
expect(find.text('More'), findsOneWidget);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-10-18 21:46:55 +02:00
|
|
|
|
testWidgets('a body that fits shows no lines', (tester) async {
|
|
|
|
|
|
await open(tester, width: 400);
|
|
|
|
|
|
|
|
|
|
|
|
expect(find.byKey(SheetScaffold.moreBelowKey), findsNothing);
|
|
|
|
|
|
expect(find.byKey(SheetScaffold.moreAboveKey), findsNothing);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
testWidgets("the body keeps clear of the scroll view's edges, where it is "
|
|
|
|
|
|
'cut and the scrollbar runs', (tester) async {
|
|
|
|
|
|
for (final width in [400.0, 1000.0]) {
|
|
|
|
|
|
await open(
|
|
|
|
|
|
tester,
|
|
|
|
|
|
width: width,
|
|
|
|
|
|
body: const TextField(
|
|
|
|
|
|
decoration: InputDecoration(border: OutlineInputBorder()),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
final scrolling = tester.getRect(find.byType(SingleChildScrollView));
|
|
|
|
|
|
final field = tester.getRect(find.byType(TextField));
|
|
|
|
|
|
expect(field.left - scrolling.left, greaterThanOrEqualTo(16));
|
|
|
|
|
|
expect(scrolling.right - field.right, greaterThanOrEqualTo(16));
|
|
|
|
|
|
// The title and the button stay in line with the body.
|
|
|
|
|
|
expect(tester.getTopLeft(find.text('New studyset')).dx, field.left);
|
|
|
|
|
|
expect(tester.getTopRight(find.byType(FilledButton)).dx, field.right);
|
|
|
|
|
|
await tester.tap(find.byTooltip('Close'));
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|