import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:halkyon_ui/halkyon_ui.dart'; import 'package:material_ui/material_ui.dart'; import '../support/test_app.dart'; /// Every Halkyon app's frame around its tabs: a rail with the app's icon /// from medium windows on, a flush bar below with a fade above it, a tick on every choice. void main() { /// A 1 × 1 PNG, the app's icon in these tests. final icon = MemoryImage( Uint8List.fromList(const [ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, // 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0B, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9C, 0x63, 0x60, 0x00, 0x02, 0x00, 0x00, 0x05, 0x00, 0x01, 0x7A, 0x5E, 0xAB, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82, ]), ); /// The frame in a window [width] wide, on tab [selected]; the tabs chosen /// go into [chosen]. Future show( WidgetTester tester, { required double width, required List chosen, int selected = 1, }) async { tester.view.physicalSize = Size(width, 800); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); await tester.pumpWidget( testApp( NavigationFrame( appName: 'Halkyon Test', icon: icon, selectedIndex: selected, onSelected: chosen.add, destinations: const [ (label: 'Home', icon: Icon(Icons.home), selectedIcon: null), (label: 'Maps', icon: Icon(Icons.map), selectedIcon: null), (label: 'Profile', icon: Icon(Icons.person), selectedIcon: null), ], child: const Scaffold(body: Text('The tab')), ), ), ); } /// The ticks the platform was asked for while [act] ran. Future> ticks( WidgetTester tester, Future Function() act, ) async { final haptics = []; tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( SystemChannels.platform, (call) async { if (call.method == 'HapticFeedback.vibrate') { haptics.add(call.arguments); } return null; }, ); addTearDown( () => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( SystemChannels.platform, null, ), ); await act(); return haptics; } testWidgets('below medium windows, a bar stands at the foot', ( tester, ) async { await show(tester, width: 400, chosen: []); expect(find.byType(FlushNavigationBar), findsOneWidget); expect(find.byType(NavigationRail), findsNothing); expect(find.text('The tab'), findsOneWidget); }); testWidgets('below medium windows, the bar is flush with the foot: as wide ' "as the window, 64 high above the system's bar, in the theme's colour", ( tester, ) async { tester.view.padding = const FakeViewPadding(bottom: 24); tester.view.viewPadding = const FakeViewPadding(bottom: 24); await show(tester, width: 400, chosen: []); final bar = find.byType(NavigationBar); expect( tester.getRect(bar), const Rect.fromLTRB(0, 800 - 64 - 24, 400, 800), ); expect(tester.widget(bar).backgroundColor, isNull); expect(tester.widget(bar).elevation, isNull); }); testWidgets('right above the bar, a fade into the background of the screen ' 'in view, which taps pass through to the screen', (tester) async { tester.view.padding = const FakeViewPadding(bottom: 24); tester.view.viewPadding = const FakeViewPadding(bottom: 24); tester.view.physicalSize = const Size(400, 800); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); var tapped = 0; const background = Color(0xFFFFE4C4); await tester.pumpWidget( testApp( NavigationFrame( appName: 'Halkyon Test', icon: icon, selectedIndex: 0, onSelected: (_) {}, destinations: const [ (label: 'Home', icon: Icon(Icons.home), selectedIcon: null), (label: 'Maps', icon: Icon(Icons.map), selectedIcon: null), ], child: TopGradient( background: background, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => tapped++, child: const SizedBox.expand(), ), ), ), ), ); await tester.pumpAndSettle(); final fade = find.byKey(FlushNavigationBar.fadeKey); final bar = tester.getRect(find.byType(NavigationBar)); expect( tester.getRect(fade), Rect.fromLTRB(0, bar.top - FlushNavigationBar.fadeHeight, 400, bar.top), ); final gradient = (tester.widget(fade).decoration as BoxDecoration) .gradient! as LinearGradient; expect(gradient.colors, [background.withValues(alpha: 0), background]); await tester.tapAt(tester.getCenter(fade)); expect(tapped, 1); }); testWidgets('before a screen claims its background, the fade ends in the ' "theme's surface, light and dark", (tester) async { tester.view.physicalSize = const Size(400, 800); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); for (final brightness in Brightness.values) { await tester.pumpWidget( testApp( brightness: brightness, NavigationFrame( appName: 'Halkyon Test', icon: icon, selectedIndex: 0, onSelected: (_) {}, destinations: const [ (label: 'Home', icon: Icon(Icons.home), selectedIcon: null), (label: 'Maps', icon: Icon(Icons.map), selectedIcon: null), ], child: const Scaffold(body: Text('The tab')), ), ), ); await tester.pumpAndSettle(); final fade = find.byKey(FlushNavigationBar.fadeKey); final surface = Theme.of(tester.element(fade)).colorScheme.surface; final gradient = (tester.widget(fade).decoration as BoxDecoration) .gradient! as LinearGradient; expect(gradient.colors.last, surface, reason: '$brightness'); } }); testWidgets("a tab's content leaves room for the bar, its fade and the " "system's bar; its floating button rises above the fade, the Scaffold " "keeping it clear of the system's bar itself", (tester) async { tester.view.padding = const FakeViewPadding(bottom: 24); tester.view.viewPadding = const FakeViewPadding(bottom: 24); tester.view.physicalSize = const Size(400, 800); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); late double cover; await tester.pumpWidget( testApp( NavigationFrame( appName: 'Halkyon Test', icon: icon, selectedIndex: 0, onSelected: (_) {}, destinations: const [ (label: 'Home', icon: Icon(Icons.home), selectedIcon: null), (label: 'Maps', icon: Icon(Icons.map), selectedIcon: null), ], child: Builder( builder: (context) => Scaffold( body: Builder( builder: (context) { cover = FlushNavigationBar.coverOf(context); return const SizedBox.expand(key: Key('tab')); }, ), floatingActionButton: Padding( padding: EdgeInsets.only( bottom: FlushNavigationBar.buttonLiftOf(context), ), child: FloatingActionButton( onPressed: () {}, child: const Icon(Icons.add), ), ), ), ), ), ), ); await tester.pumpAndSettle(); expect(cover, FlushNavigationBar.fadeHeight + 64 + 24); expect( tester.getBottomLeft(find.byKey(const Key('tab'))).dy, 800, reason: 'the content runs on beneath the fade and the bar', ); expect( tester.getRect(find.byType(FloatingActionButton)).bottom, tester.getRect(find.byKey(FlushNavigationBar.fadeKey)).top - 16, ); }); testWidgets("outside the frame, content leaves room for the system's bar " 'alone', (tester) async { tester.view.padding = const FakeViewPadding(bottom: 24); tester.view.viewPadding = const FakeViewPadding(bottom: 24); tester.view.physicalSize = const Size(400, 800); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); late double cover; late double lift; await tester.pumpWidget( testApp( Scaffold( body: Builder( builder: (context) { cover = FlushNavigationBar.coverOf(context); lift = FlushNavigationBar.buttonLiftOf(context); return const SizedBox.expand(); }, ), ), ), ); expect(cover, 24); expect(lift, 0); }); testWidgets("from medium windows on, a rail with the app's icon on top, " 'which leads to the first tab', (tester) async { final chosen = []; await show(tester, width: 800, chosen: chosen); expect(find.byType(NavigationRail), findsOneWidget); expect(find.byType(NavigationBar), findsNothing); expect(find.bySemanticsLabel('Halkyon Test'), findsOneWidget); await tester.tap(find.byTooltip('Home')); await tester.pump(); expect(chosen, [0]); }); testWidgets('every choice on the bar ticks, lightly', (tester) async { final chosen = []; await show(tester, width: 400, chosen: chosen); final haptics = await ticks(tester, () async { await tester.tap(find.text('Profile')); await tester.pump(); }); expect(haptics, ['HapticFeedbackType.selectionClick']); expect(chosen, [2]); }); testWidgets('every choice on the rail ticks, lightly', (tester) async { final chosen = []; await show(tester, width: 800, chosen: chosen); final haptics = await ticks(tester, () async { await tester.tap(find.text('Maps')); await tester.pump(); }); expect(haptics, ['HapticFeedbackType.selectionClick']); expect(chosen, [1]); }); testWidgets('the window crossing from compact to medium and back swaps ' 'the bar for the rail', (tester) async { await show(tester, width: 400, chosen: []); for (final width in const [800.0, 400.0]) { tester.view.physicalSize = Size(width, 800); await tester.pumpAndSettle(); } expect(find.byType(FlushNavigationBar), findsOneWidget); expect(find.text('The tab'), findsOneWidget); }); testWidgets('the bar steps aside where a screen has a bar of its own, ' "leaving the system's bar its room", (tester) async { tester.view.physicalSize = const Size(400, 800); tester.view.devicePixelRatio = 1; tester.view.padding = const FakeViewPadding(bottom: 24); tester.view.viewPadding = const FakeViewPadding(bottom: 24); addTearDown(tester.view.reset); await tester.pumpWidget( testApp( NavigationFrame( appName: 'Halkyon Test', icon: icon, selectedIndex: 0, onSelected: (_) {}, hidesNavigationBar: true, destinations: const [ (label: 'Home', icon: Icon(Icons.home), selectedIcon: null), (label: 'Maps', icon: Icon(Icons.map), selectedIcon: null), ], child: const Scaffold(body: Text('The tab')), ), ), ); expect(find.byType(FlushNavigationBar), findsNothing); expect(tester.getBottomLeft(find.byType(Scaffold).last).dy, 800 - 24); }); testWidgets('a status stands above the bar below medium windows, and at ' "the foot of the screen's panel from there on", (tester) async { for (final width in const [400.0, 800.0]) { tester.view.physicalSize = Size(width, 800); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); await tester.pumpWidget( testApp( NavigationFrame( appName: 'Halkyon Test', icon: icon, selectedIndex: 0, onSelected: (_) {}, status: const Text('Recording'), destinations: const [ (label: 'Home', icon: Icon(Icons.home), selectedIcon: null), (label: 'Maps', icon: Icon(Icons.map), selectedIcon: null), ], child: const Scaffold(body: Text('The tab')), ), ), ); await tester.pumpAndSettle(); final status = tester.getBottomLeft(find.text('Recording')).dy; final tab = tester.getTopLeft(find.text('The tab')).dy; expect(status, greaterThan(tab), reason: '$width'); if (width < 600) { expect( status, lessThan(tester.getTopLeft(find.byType(NavigationBar)).dy), ); } } }); testWidgets("the keyboard leaves the frame's tab its size: the tab's own " 'screen decides whether it makes way, as one with a field floating ' "over it doesn't", (tester) async { tester.view.physicalSize = const Size(400, 800); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); Widget frame({required bool screenResizes}) => testApp( NavigationFrame( appName: 'Halkyon Test', icon: icon, selectedIndex: 0, onSelected: (_) {}, destinations: const [ (label: 'Home', icon: Icon(Icons.home), selectedIcon: null), (label: 'Maps', icon: Icon(Icons.map), selectedIcon: null), ], child: Scaffold( resizeToAvoidBottomInset: screenResizes, body: const SizedBox.expand(key: Key('tab')), ), ), ); await tester.pumpWidget(frame(screenResizes: false)); final before = tester.getSize(find.byKey(const Key('tab'))); tester.view.viewInsets = const FakeViewPadding(bottom: 300); addTearDown(tester.view.resetViewInsets); await tester.pump(); expect(tester.getSize(find.byKey(const Key('tab'))), before); await tester.pumpWidget(frame(screenResizes: true)); expect( tester.getSize(find.byKey(const Key('tab'))).height, lessThan(before.height), reason: 'a screen that resizes still makes way', ); }); }