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 navigation bar takes the colour of the screen in view (the /// developer's review, 8 Nov 2026): a project of its own colour colours the /// bar while it shows, the app's colour before any screen claims one, every /// tone legible on the bar in both themes. 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, ]), ); final accent = PaletteColor.strawberry.background; tearDown(() => debugNavigationBarStyle.value = shippingNavigationBarStyle); /// The frame around [child] on a phone, in [brightness]. Future show( WidgetTester tester, Widget child, { Brightness brightness = Brightness.light, bool disableAnimations = false, }) async { tester.view.physicalSize = const Size(400, 800); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); await tester.pumpWidget( testApp( brightness: brightness, Builder( builder: (context) => MediaQuery( data: MediaQuery.of( context, ).copyWith(disableAnimations: disableAnimations), child: NavigationFrame( appName: 'Halkyon Test', icon: icon, accent: accent, selectedIndex: 0, onSelected: (_) {}, destinations: const [ (label: 'Home', icon: Icon(Icons.home), selectedIcon: null), (label: 'Maps', icon: Icon(Icons.map), selectedIcon: null), ], child: child, ), ), ), ), ); } /// A screen claiming [color] while it shows. Widget screen(Color color) => TopGradient( color: color, background: Colors.white, child: const SizedBox.expand(), ); Color? chosenIcon(WidgetTester tester) => IconTheme.of(tester.element(find.byIcon(Icons.home))).color; Color? indicator(WidgetTester tester) => tester.widget(find.byType(NavigationBar)).indicatorColor; Color barSurface(WidgetTester tester) => tester .widget( find .descendant( of: find.byKey(FlushNavigationBar.barKey), matching: find.byType(ColoredBox), ) .first, ) .color; Color legible(WidgetTester tester, Color color) { final theme = Theme.of(tester.element(find.byType(NavigationBar))); final surface = barSurface(tester); return readableTone( color, brightness: theme.brightness, on: [surface, Color.alphaBlend(color.withValues(alpha: 0.22), surface)], ); } testWidgets("before a screen claims a colour, the bar takes the app's", ( tester, ) async { await show(tester, const SizedBox.expand()); await tester.pumpAndSettle(); expect(indicator(tester), accent.withValues(alpha: 0.22)); expect(chosenIcon(tester), legible(tester, accent)); }); testWidgets('the bar takes the colour the screen in view claims, and ' 'changes with it over 300 ms', (tester) async { final project = PaletteColor.ocean.background; await show(tester, screen(project)); await tester.pumpAndSettle(); expect(indicator(tester), project.withValues(alpha: 0.22)); expect(chosenIcon(tester), legible(tester, project)); // Another project opens on the same tab. final other = PaletteColor.olive.background; await show(tester, screen(other)); await tester.pump(); await tester.pump(const Duration(milliseconds: 150)); expect( indicator(tester), isNot( anyOf(project.withValues(alpha: 0.22), other.withValues(alpha: 0.22)), ), reason: 'half way', ); await tester.pump(const Duration(milliseconds: 200)); expect(indicator(tester), other.withValues(alpha: 0.22)); }); testWidgets('with animations off, the colour changes at once', ( tester, ) async { await show(tester, screen(PaletteColor.ocean.background)); await tester.pumpAndSettle(); final other = PaletteColor.olive.background; await show(tester, screen(other), disableAnimations: true); await tester.pump(); await tester.pump(); expect(indicator(tester), other.withValues(alpha: 0.22)); }); group('every palette colour is legible on the bar', () { final byLuminance = [...PaletteColor.values] ..sort( (a, b) => a.background.computeLuminance().compareTo( b.background.computeLuminance(), ), ); final lightest = byLuminance.last; final darkest = byLuminance.first; for (final brightness in Brightness.values) { for (final tinted in [false, true]) { testWidgets('in the $brightness theme, the bar ' '${tinted ? 'tinted' : 'neutral'}: the lightest ' '(${lightest.name}), the darkest (${darkest.name}) and every ' 'other', (tester) async { debugNavigationBarStyle.value = ( variant: NavigationBarVariant.waveWithoutFill, waveDuration: const Duration(milliseconds: 1200), cornerRadius: 20.0, tintedSurface: tinted, ); for (final color in [lightest, darkest, ...PaletteColor.values]) { await show( tester, screen(color.background), brightness: brightness, ); await tester.pumpAndSettle(); final tone = chosenIcon(tester)!; final surface = barSurface(tester); final onIndicator = Color.alphaBlend( color.background.withValues(alpha: 0.22), surface, ); expect( TopGradient.contrast(tone, surface), greaterThanOrEqualTo(4.5), reason: '${color.name} on the bar', ); expect( TopGradient.contrast(tone, onIndicator), greaterThanOrEqualTo(4.5), reason: '${color.name} on its indicator', ); final label = find.text('Home'); expect( tester.widget(label).style?.color ?? DefaultTextStyle.of(tester.element(label)).style.color, tone, reason: '${color.name}: the label as the icon', ); } }); } } }); testWidgets("a colour legible already keeps the palette's own text tone: " 'its outline in light mode, its fill in dark', (tester) async { const light = Color(0xFFF3F3F3); const dark = Color(0xFF1A1B24); expect( readableTone( PaletteColor.cobalt.background, brightness: Brightness.light, on: [light], ), PaletteColor.cobalt.outline, ); expect( readableTone( PaletteColor.purple.background, brightness: Brightness.dark, on: [dark], ), PaletteColor.purple.background, ); }); testWidgets("the bar's surface stays neutral unless tinted, which mixes in " '6 % of the colour in light mode and 8 % in dark', (tester) async { final project = PaletteColor.ocean.background; for (final brightness in Brightness.values) { debugNavigationBarStyle.value = shippingNavigationBarStyle; await show(tester, screen(project), brightness: brightness); await tester.pumpAndSettle(); final neutral = Theme.of( tester.element(find.byType(NavigationBar)), ).colorScheme.surfaceContainer; expect(barSurface(tester), neutral, reason: '$brightness'); debugNavigationBarStyle.value = ( variant: NavigationBarVariant.waveWithoutFill, waveDuration: const Duration(milliseconds: 1200), cornerRadius: 20.0, tintedSurface: true, ); await tester.pumpAndSettle(); final share = brightness == Brightness.dark ? 0.08 : 0.06; expect( barSurface(tester), Color.alphaBlend(project.withValues(alpha: share), neutral), reason: '$brightness', ); } }); }