import 'dart:math'; import 'package:flutter_test/flutter_test.dart'; import 'package:halkyon_ui/halkyon_ui.dart'; import 'package:material_ui/material_ui.dart'; /// WCAG's contrast ratio of two colours, from 1 (none) to 21. double _contrast(Color a, Color b) { final (lighter, darker) = ( max(a.computeLuminance(), b.computeLuminance()), min(a.computeLuminance(), b.computeLuminance()), ); return (lighter + 0.05) / (darker + 0.05); } /// An app's own colours, beside the shared ones. final class const _AppColors(final Color special) extends ThemeExtension<_AppColors> { @override _AppColors copyWith({Color? special}) => _AppColors(special ?? this.special); @override _AppColors lerp(_AppColors? other, double t) => other == null ? this : _AppColors(Color.lerp(special, other.special, t)!); } void main() { group('the picker palette', () { test('has the 20 colours content can be given', () { expect(PaletteColor.values, hasLength(20)); }); for (final color in PaletteColor.values) { test( '${color.name}: its text colour is readable on its fill (WCAG AA)', () { expect( _contrast(color.foreground, color.background), greaterThanOrEqualTo(4.5), ); }, ); } test( 'finds a colour by its stored name, and nothing for an unknown one', () { expect(PaletteColor.byName('cobalt'), PaletteColor.cobalt); expect(PaletteColor.byName('ultraviolet'), isNull); expect(PaletteColor.byName(null), isNull); }, ); }); group('the theme', () { for (final brightness in Brightness.values) { test('$brightness: Material 3, with the status colours', () { final theme = appTheme(brightness); expect(theme.useMaterial3, isTrue); expect(theme.colorScheme.brightness, brightness); expect(theme.extras, isA()); }); } test("takes an app's own extensions beside the shared ones", () { const colors = _AppColors(Color(0xFF123456)); final theme = appTheme(Brightness.light, extensions: [colors]); expect(theme.extension<_AppColors>(), colors); expect(theme.extras, ThemeExtras.light); }); test('light: cobalt is the primary colour, copper the accent', () { final scheme = appTheme(Brightness.light).colorScheme; expect(scheme.primary, Brand.primary); expect(scheme.tertiary, Brand.accent); }); test("dark: the primary colour's text stays readable", () { final scheme = appTheme(Brightness.dark).colorScheme; expect( _contrast(scheme.onPrimary, scheme.primary), greaterThanOrEqualTo(4.5), ); expect( _contrast(scheme.onSurface, scheme.surface), greaterThanOrEqualTo(4.5), ); }); group('dark: filled buttons and floating buttons in cobalt itself, white ' "on it (the developer's review, 9 Nov 2026)", () { final theme = appTheme(Brightness.dark); const none = {}; test('a filled button', () { final style = theme.filledButtonTheme.style!; expect(style.backgroundColor!.resolve(none), Brand.primary); expect(style.foregroundColor!.resolve(none), Colors.white); expect( style.backgroundColor!.resolve({WidgetState.disabled}), isNot(Brand.primary), reason: "disabled, Material's own", ); }); test('a floating button', () { expect(theme.floatingActionButtonTheme.backgroundColor, Brand.primary); expect(theme.floatingActionButtonTheme.foregroundColor, Colors.white); }); test('readable: white on cobalt', () { expect(_contrast(Colors.white, Brand.primary), greaterThan(7)); }); test("links, tabs and switches keep the primary colour's lighter " 'tone', () { expect(theme.colorScheme.primary, isNot(Brand.primary)); }); testWidgets('a filled icon button, styled by filledIconButtonStyle', ( tester, ) async { await tester.pumpWidget( MaterialApp( theme: theme, home: Builder( builder: (context) => IconButton.filled( style: filledIconButtonStyle(context), onPressed: () {}, icon: const Icon(Icons.arrow_upward), ), ), ), ); final material = tester.widget( find.descendant( of: find.byType(IconButton), matching: find.byType(Material), ), ); expect(material.color, Brand.primary); expect( tester .widget( find .ancestor( of: find.byIcon(Icons.arrow_upward), matching: find.byType(IconTheme), ) .first, ) .data .color, Colors.white, ); }); }); test('light: the buttons as Material 3 has them', () { final theme = appTheme(Brightness.light); expect(theme.filledButtonTheme.style, isNull); expect(theme.floatingActionButtonTheme.backgroundColor, isNull); }); test("the navigation bar is Material 3 Expressive's shorter one", () { expect(appTheme(Brightness.light).navigationBarTheme.height, 64); }); }); }