halkyon/packages/halkyon_ui/test/theme/theme_test.dart

174 lines
5.4 KiB
Dart
Raw Normal View History

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<ThemeExtras>());
});
}
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 = <WidgetState>{};
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<Material>(
find.descendant(
of: find.byType(IconButton),
matching: find.byType(Material),
),
);
expect(material.color, Brand.primary);
expect(
tester
.widget<IconTheme>(
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);
});
});
}