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 8 Nov /// 2026, after trying the variants on a phone): a wave in the screen's /// colour spreading from the destination tapped, the destination chosen in /// that colour, without Material's fill, rounded top corners. 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); /// 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('a tap on another destination sends a wave over the bar, gone ' 'after its 1.2 s', (tester) async { expect(FlushNavigationBar.waveDuration, const Duration(milliseconds: 1200)); 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: 800)); expect(wave, findsOneWidget, reason: 'not yet 1.2 s'); await tester.pump(const Duration(milliseconds: 200)); 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('the destination chosen is in the app colour, legible, its ' "indicator tinted with it and there at once, without Material's " 'ripple', (tester) async { await phone(tester); NavigationBar bar() => tester.widget(find.byType(NavigationBar)); Color? iconColor(IconData data) => IconTheme.of( tester.element(find.byIcon(data)), ).color; // The app colour as legible on the bar and on its indicator. Color tone() { final surface = Theme.of( tester.element(find.byType(NavigationBar)), ).colorScheme.surfaceContainer; return readableTone( accent, brightness: Brightness.light, on: [ surface, Color.alphaBlend(accent.withValues(alpha: 0.22), surface), ], ); } await tester.pumpWidget(frame(selected: 1)); await tester.pumpAndSettle(); expect(iconColor(Icons.map), tone()); expect(iconColor(Icons.home), isNot(tone())); expect( tester.widget(find.text('Maps')).style?.color ?? DefaultTextStyle.of(tester.element(find.text('Maps'))).style.color, tone(), ); expect(bar().indicatorColor, accent.withValues(alpha: 0.22)); expect( bar().overlayColor?.resolve({WidgetState.pressed}), Colors.transparent, ); expect(bar().animationDuration, Duration.zero); }); testWidgets("the bar's top corners are rounded, 20 dp, the screen showing " 'through them', (tester) async { await phone(tester); await tester.pumpWidget(frame(selected: 0)); expect(FlushNavigationBar.cornerRadius, 20); expect( tester .widget(find.byKey(FlushNavigationBar.barKey)) .borderRadius, const BorderRadius.vertical(top: Radius.circular(20)), ); }); }