249 lines
8.5 KiB
Dart
249 lines
8.5 KiB
Dart
|
|
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<void> 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<NavigationBar>(find.byType(NavigationBar)).indicatorColor;
|
|||
|
|
Color barSurface(WidgetTester tester) => tester
|
|||
|
|
.widget<ColoredBox>(
|
|||
|
|
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<Text>(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',
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|