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'; /// The bar's look below medium windows (the developer's choice of 7 Nov /// 2026): a wave in the app's colour spreading from the destination tapped, /// the destination chosen in that colour, no fill, rounded top corners; and /// the section switching them while testing, in debug builds alone. 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, ]), ); const accent = Color(0xFFEC3E7C); tearDown(() => debugNavigationBarStyle.value = shippingNavigationBarStyle); /// The frame on tab [selected], its choices passed to [onSelected]; /// animations off where [disableAnimations]. Widget frame({ required int selected, ValueChanged? onSelected, bool disableAnimations = false, }) => testApp( Builder( builder: (context) => MediaQuery( data: MediaQuery.of( context, ).copyWith(disableAnimations: disableAnimations), child: NavigationFrame( appName: 'Halkyon Test', icon: icon, accent: accent, selectedIndex: selected, onSelected: onSelected ?? (_) {}, 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')), ), ), ), ); /// A frame whose tab follows the taps on its bar. Widget following({bool disableAnimations = false}) { var selected = 0; return StatefulBuilder( builder: (context, setState) => frame( selected: selected, disableAnimations: disableAnimations, onSelected: (index) => setState(() => selected = index), ), ); } Future phone(WidgetTester tester) async { tester.view.physicalSize = const Size(400, 800); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); } final wave = find.byKey(FlushNavigationBar.waveKey); testWidgets('the shipping look is the wave without the fill, 1.2 s, with ' 'corners of 20, which the debug section starts from', (tester) async { expect(shippingNavigationBarStyle, ( variant: NavigationBarVariant.waveWithoutFill, waveDuration: const Duration(milliseconds: 1200), cornerRadius: 20.0, )); expect(debugNavigationBarStyle.value, shippingNavigationBarStyle); }); testWidgets('a tap on another destination sends a wave over the bar, gone ' 'once its time is up', (tester) async { await phone(tester); await tester.pumpWidget(following()); expect(wave, findsNothing); await tester.tap(find.text('Profile')); await tester.pump(); await tester.pump(const Duration(milliseconds: 300)); expect(wave, findsOneWidget); final bar = tester.getRect(find.byType(NavigationBar)); expect( tester.getRect(wave), bar, reason: 'it covers the bar, clipped to it', ); await tester.pump(const Duration(milliseconds: 1000)); expect(wave, findsNothing); }); testWidgets('the wave takes as long as the style says', (tester) async { await phone(tester); debugNavigationBarStyle.value = ( variant: NavigationBarVariant.waveWithoutFill, waveDuration: const Duration(seconds: 2), cornerRadius: 20.0, ); await tester.pumpWidget(following()); await tester.tap(find.text('Maps')); await tester.pump(); await tester.pump(const Duration(milliseconds: 1500)); expect(wave, findsOneWidget); await tester.pump(const Duration(milliseconds: 600)); expect(wave, findsNothing); }); testWidgets('no wave for the destination already chosen, nor with ' 'animations off', (tester) async { await phone(tester); await tester.pumpWidget(following()); await tester.tap(find.text('Home')); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); expect(wave, findsNothing, reason: 'the tab already chosen'); await tester.pumpWidget(following(disableAnimations: true)); await tester.tap(find.text('Maps')); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); expect(wave, findsNothing, reason: 'animations off'); }); testWidgets('a tab changed without a tap (a link, back) sends no wave: ' 'the wave answers the finger', (tester) async { await phone(tester); await tester.pumpWidget(frame(selected: 0)); await tester.pumpWidget(frame(selected: 2)); await tester.pump(const Duration(milliseconds: 100)); expect(wave, findsNothing); }); testWidgets('Material alone sends no wave', (tester) async { await phone(tester); debugNavigationBarStyle.value = ( variant: NavigationBarVariant.material, waveDuration: const Duration(milliseconds: 1200), cornerRadius: 20.0, ); await tester.pumpWidget(following()); await tester.tap(find.text('Maps')); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); expect(wave, findsNothing); }); testWidgets('the destination chosen is in the app colour, its indicator ' 'tinted with it; without the fill no ripple and no growing indicator; ' 'the last variant without the indicator', (tester) async { await phone(tester); NavigationBar bar() => tester.widget(find.byType(NavigationBar)); Color? iconColor(IconData data) => IconTheme.of( tester.element(find.byIcon(data)), ).color; debugNavigationBarStyle.value = ( variant: NavigationBarVariant.wave, waveDuration: const Duration(milliseconds: 1200), cornerRadius: 20.0, ); await tester.pumpWidget(frame(selected: 1)); await tester.pumpAndSettle(); expect(iconColor(Icons.map), accent); expect(iconColor(Icons.home), isNot(accent)); expect( tester.widget(find.text('Maps')).style?.color ?? DefaultTextStyle.of(tester.element(find.text('Maps'))).style.color, accent, ); expect(bar().indicatorColor, accent.withValues(alpha: 0.22)); expect(bar().overlayColor, isNull, reason: 'with the fill'); debugNavigationBarStyle.value = shippingNavigationBarStyle; await tester.pumpAndSettle(); expect(iconColor(Icons.map), accent); expect(bar().indicatorColor, accent.withValues(alpha: 0.22)); expect( bar().overlayColor?.resolve({WidgetState.pressed}), Colors.transparent, ); expect(bar().animationDuration, Duration.zero); debugNavigationBarStyle.value = ( variant: NavigationBarVariant.waveWithoutIndicator, waveDuration: const Duration(milliseconds: 1200), cornerRadius: 20.0, ); await tester.pumpAndSettle(); expect(iconColor(Icons.map), accent); expect(bar().indicatorColor, Colors.transparent); debugNavigationBarStyle.value = ( variant: NavigationBarVariant.material, waveDuration: const Duration(milliseconds: 1200), cornerRadius: 20.0, ); await tester.pumpAndSettle(); expect(iconColor(Icons.map), isNot(accent)); expect(bar().indicatorColor, isNull); }); testWidgets("the bar's top corners are rounded as the style says, the " 'screen showing through them', (tester) async { await phone(tester); BorderRadiusGeometry corners() => tester .widget(find.byKey(FlushNavigationBar.barKey)) .borderRadius; await tester.pumpWidget(frame(selected: 0)); expect(corners(), const BorderRadius.vertical(top: Radius.circular(20))); debugNavigationBarStyle.value = ( variant: NavigationBarVariant.waveWithoutFill, waveDuration: const Duration(milliseconds: 1200), cornerRadius: 0.0, ); await tester.pump(); expect(corners(), BorderRadius.zero); }); testWidgets('the debug section switches the look live, and shows nothing ' 'outside debug builds', (tester) async { await phone(tester); await tester.pumpWidget( testApp( const Scaffold( body: SingleChildScrollView(child: NavigationBarDebugSection()), ), ), ); expect(find.text('Navigation bar (debug)'), findsOneWidget); await tester.tap(find.text('F')); await tester.pump(); await tester.tap(find.text('2 s')); await tester.pump(); await tester.tap(find.text('28')); await tester.pump(); expect(debugNavigationBarStyle.value, ( variant: NavigationBarVariant.waveWithoutIndicator, waveDuration: const Duration(seconds: 2), cornerRadius: 28.0, )); await tester.pumpWidget( testApp( const Scaffold(body: NavigationBarDebugSection(debug: false)), ), ); expect(find.text('Navigation bar (debug)'), findsNothing); }); testWidgets('outside debug builds the frame keeps the shipping look ' 'whatever the debug section holds', (tester) async { expect( effectiveNavigationBarStyle(debug: false), shippingNavigationBarStyle, ); debugNavigationBarStyle.value = ( variant: NavigationBarVariant.material, waveDuration: const Duration(seconds: 2), cornerRadius: 0.0, ); expect( effectiveNavigationBarStyle(debug: false), shippingNavigationBarStyle, ); // Tests run as a debug build. expect(effectiveNavigationBarStyle(), debugNavigationBarStyle.value); }); }