halkyon/packages/halkyon_ui/test/widget/navigation_bar_wave_test.dart
Tizian 「ティツィアーン」 56f40d4d65
refactor(ui,learn,todo,train,meditate): the navigation bar's one look, the debug section gone
The developer decided on 8 Nov 2026, after trying the variants on the
phone: the wave in the colour of the screen in view, without Material's
ripple, the indicator kept and there at once, a 1.2 s wave, top corners
of 20 dp, the bar's own neutral surface. Learn's bar stays cobalt, the
colour its tabs wash in, rather than its icon's purple.

So the bar has one look: FlushNavigationBar.waveDuration and
cornerRadius are constants, and the debug section goes from the four
Settings screens with what it switched (NavigationBarDebugSection,
debugNavigationBarStyle, effectiveNavigationBarStyle, the style record
and the variants A, C and F, the speeds, the corners and the tinted
surface). Their tests go too; those stating the look kept stay. Learn's
sign-out helper no longer has to scroll past the section.

ADR 0030's amendment of D60, the halkyon_ui README and Learn's
AGENTS.md record the decision and the alternatives set aside.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-08 11:48:39 +01:00

178 lines
6.2 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<int>? 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<void> 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<Text>(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<ClipRRect>(find.byKey(FlushNavigationBar.barKey))
.borderRadius,
const BorderRadius.vertical(top: Radius.circular(20)),
);
});
}