halkyon/packages/halkyon_ui/test/widget/adaptive_sheet_test.dart
Tizian 「ティツィアーン」 4898d0a04a
feat(learn): the assistant's conversation a dialog on the desktop and in expanded windows (Todo's ADR 0033, D11)
- halkyon_ui's sheetShowsAsDialog says whether showAdaptiveSheet shows
  a dialog here, for a sheet of its own that can't go through it.
- Learn's conversation, a half-height sheet dragged up, asks it: where
  sheets are dialogs it is a dialog up to 720 wide and as tall as the
  window, with no handle to drag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-01 18:20:58 +01:00

301 lines
9.7 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
});
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;
});
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();
}
});
}