- 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>
271 lines
8.7 KiB
Dart
271 lines
8.7 KiB
Dart
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();
|
||
}
|
||
|
||
testWidgets('is a bottom sheet below expanded windows on a phone or a '
|
||
'tablet', (tester) async {
|
||
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);
|
||
});
|
||
|
||
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;
|
||
});
|
||
}
|
||
|
||
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;
|
||
});
|
||
});
|
||
|
||
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);
|
||
});
|
||
|
||
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();
|
||
}
|
||
});
|
||
}
|