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