import 'dart:typed_data'; 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'; /// The rail opening into a sidebar with labels (decided by the developer, /// 9 and 10 Nov 2026): phones keep the bar, medium windows a rail alone, /// and from expanded windows on a toggle at the rail's foot opens it into a /// sidebar 256 wide, kept open or closed as the app keeps it, its labels /// coming in one after another, all at once without animations. 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, the sidebar [open], its toggles /// going into [toggled] (none offered where null), the tabs chosen into /// [chosen]. Future show( WidgetTester tester, { required double width, bool open = false, List? toggled, List? chosen, Widget? sections, bool still = false, bool settle = true, }) async { tester.view.physicalSize = Size(width, 800); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); final frame = NavigationFrame( appName: 'Halkyon Test', icon: icon, accent: const Color(0xFFEC3E7C), selectedIndex: 0, onSelected: (index) => chosen?.add(index), sidebarOpen: open, onSidebarChanged: toggled?.add, sidebarSections: sections, 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')), ); await tester.pumpWidget( testApp( still ? Builder( builder: (context) => MediaQuery( data: MediaQuery.of( context, ).copyWith(disableAnimations: true), child: frame, ), ) : frame, ), ); if (settle) { await tester.pumpAndSettle(); } else { await tester.pump(); } } testWidgets('medium windows keep the rail alone: no toggle, open or not', ( tester, ) async { await show(tester, width: 700, open: true, toggled: []); expect(find.byType(NavigationRail), findsOneWidget); expect(find.byKey(NavigationFrame.sidebarKey), findsNothing); expect(find.byTooltip('Open sidebar'), findsNothing); expect(find.byTooltip('Close sidebar'), findsNothing); }); testWidgets('from expanded windows on, a toggle at the foot of the rail ' 'asks for the sidebar', (tester) async { final toggled = []; await show(tester, width: 1000, toggled: toggled); final toggle = find.byTooltip('Open sidebar'); expect(toggle, findsOneWidget); expect( tester.getBottomLeft(toggle).dy, greaterThan(tester.getBottomLeft(find.text('Profile')).dy + 200), reason: 'at the foot, apart from the tabs', ); await tester.tap(toggle); await tester.pump(); expect(toggled, [true]); }); testWidgets('an app that keeps no choice offers no toggle', (tester) async { await show(tester, width: 1000); expect(find.byTooltip('Open sidebar'), findsNothing); }); testWidgets('open, a sidebar 256 wide: the app named beside its icon, ' 'each tab its label beside its icon, what the app lists below them, ' 'and a toggle closing it', (tester) async { final toggled = []; final chosen = []; await show( tester, width: 1000, open: true, toggled: toggled, chosen: chosen, sections: const Text('Favourites'), ); final sidebar = find.byKey(NavigationFrame.sidebarKey); expect(tester.getSize(sidebar).width, 256); expect(find.byType(NavigationRail), findsNothing); expect(find.text('Halkyon Test'), findsOneWidget); final mapsIcon = tester.getRect(find.byIcon(Icons.map)); final mapsLabel = tester.getRect(find.text('Maps')); expect(mapsLabel.left, greaterThan(mapsIcon.right)); expect(mapsLabel.center.dy, closeTo(mapsIcon.center.dy, 1)); expect( find.descendant(of: sidebar, matching: find.text('Favourites')), findsOneWidget, ); await tester.tap(find.text('Maps')); await tester.pump(); expect(chosen, [1]); await tester.tap(find.byTooltip('Close sidebar')); await tester.pump(); expect(toggled, [false]); }); testWidgets('its icons stand where the rail had them, so opening it moves ' 'nothing under the pointer', (tester) async { await show(tester, width: 1000, toggled: []); final closed = tester.getCenter(find.byIcon(Icons.map)).dx; await show(tester, width: 1000, open: true, toggled: []); expect(tester.getCenter(find.byIcon(Icons.map)).dx, closeTo(closed, 1)); }); testWidgets('kept open in a medium window, it is the rail there', ( tester, ) async { await show(tester, width: 1000, open: true, toggled: []); tester.view.physicalSize = const Size(700, 800); await tester.pumpAndSettle(); expect(find.byType(NavigationRail), findsOneWidget); expect(find.byKey(NavigationFrame.sidebarKey), findsNothing); }); testWidgets('opening it, the labels come in one after another, top to ' 'bottom', (tester) async { await show(tester, width: 1000, toggled: []); await show(tester, width: 1000, open: true, toggled: [], settle: false); await tester.pump(const Duration(milliseconds: 100)); double opacityOf(String label) => tester .widget( find .ancestor( of: find.text(label), matching: find.byType(FadeTransition), ) .first, ) .opacity .value; expect(opacityOf('Home'), greaterThan(opacityOf('Profile'))); await tester.pumpAndSettle(); expect(opacityOf('Profile'), 1); }); testWidgets('without animations, it opens whole at once', (tester) async { await show(tester, width: 1000, toggled: [], still: true); await show( tester, width: 1000, open: true, toggled: [], still: true, settle: false, ); expect(tester.getSize(find.byKey(NavigationFrame.sidebarKey)).width, 256); expect( tester .widget( find .ancestor( of: find.text('Profile'), matching: find.byType(FadeTransition), ) .first, ) .opacity .value, 1, ); }); testWidgets('its toggle speaks German too', (tester) async { tester.view.physicalSize = const Size(1000, 800); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); await tester.pumpWidget( testApp( NavigationFrame( appName: 'Halkyon Test', icon: icon, accent: const Color(0xFFEC3E7C), selectedIndex: 0, onSelected: (_) {}, onSidebarChanged: (_) {}, destinations: const [ (label: 'Start', icon: Icon(Icons.home), selectedIcon: null), ], child: const Scaffold(body: Text('The tab')), ), locale: const Locale('de'), ), ); await tester.pumpAndSettle(); expect(find.byTooltip('Seitenleiste öffnen'), findsOneWidget); }); }