288 lines
9.9 KiB
Dart
288 lines
9.9 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 bar's look below medium windows (the developer's choice of 7 Nov
|
|||
|
|
/// 2026): a wave in the app's colour spreading from the destination tapped,
|
|||
|
|
/// the destination chosen in that colour, no fill, rounded top corners; and
|
|||
|
|
/// the section switching them while testing, in debug builds alone.
|
|||
|
|
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);
|
|||
|
|
|
|||
|
|
tearDown(() => debugNavigationBarStyle.value = shippingNavigationBarStyle);
|
|||
|
|
|
|||
|
|
/// 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('the shipping look is the wave without the fill, 1.2 s, with '
|
|||
|
|
'corners of 20, which the debug section starts from', (tester) async {
|
|||
|
|
expect(shippingNavigationBarStyle, (
|
|||
|
|
variant: NavigationBarVariant.waveWithoutFill,
|
|||
|
|
waveDuration: const Duration(milliseconds: 1200),
|
|||
|
|
cornerRadius: 20.0,
|
|||
|
|
));
|
|||
|
|
expect(debugNavigationBarStyle.value, shippingNavigationBarStyle);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
testWidgets('a tap on another destination sends a wave over the bar, gone '
|
|||
|
|
'once its time is up', (tester) async {
|
|||
|
|
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: 1000));
|
|||
|
|
expect(wave, findsNothing);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
testWidgets('the wave takes as long as the style says', (tester) async {
|
|||
|
|
await phone(tester);
|
|||
|
|
debugNavigationBarStyle.value = (
|
|||
|
|
variant: NavigationBarVariant.waveWithoutFill,
|
|||
|
|
waveDuration: const Duration(seconds: 2),
|
|||
|
|
cornerRadius: 20.0,
|
|||
|
|
);
|
|||
|
|
await tester.pumpWidget(following());
|
|||
|
|
await tester.tap(find.text('Maps'));
|
|||
|
|
await tester.pump();
|
|||
|
|
await tester.pump(const Duration(milliseconds: 1500));
|
|||
|
|
expect(wave, findsOneWidget);
|
|||
|
|
await tester.pump(const Duration(milliseconds: 600));
|
|||
|
|
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('Material alone sends no wave', (tester) async {
|
|||
|
|
await phone(tester);
|
|||
|
|
debugNavigationBarStyle.value = (
|
|||
|
|
variant: NavigationBarVariant.material,
|
|||
|
|
waveDuration: const Duration(milliseconds: 1200),
|
|||
|
|
cornerRadius: 20.0,
|
|||
|
|
);
|
|||
|
|
await tester.pumpWidget(following());
|
|||
|
|
await tester.tap(find.text('Maps'));
|
|||
|
|
await tester.pump();
|
|||
|
|
await tester.pump(const Duration(milliseconds: 100));
|
|||
|
|
expect(wave, findsNothing);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
testWidgets('the destination chosen is in the app colour, its indicator '
|
|||
|
|
'tinted with it; without the fill no ripple and no growing indicator; '
|
|||
|
|
'the last variant without the indicator', (tester) async {
|
|||
|
|
await phone(tester);
|
|||
|
|
NavigationBar bar() => tester.widget(find.byType(NavigationBar));
|
|||
|
|
Color? iconColor(IconData data) => IconTheme.of(
|
|||
|
|
tester.element(find.byIcon(data)),
|
|||
|
|
).color;
|
|||
|
|
|
|||
|
|
debugNavigationBarStyle.value = (
|
|||
|
|
variant: NavigationBarVariant.wave,
|
|||
|
|
waveDuration: const Duration(milliseconds: 1200),
|
|||
|
|
cornerRadius: 20.0,
|
|||
|
|
);
|
|||
|
|
await tester.pumpWidget(frame(selected: 1));
|
|||
|
|
await tester.pumpAndSettle();
|
|||
|
|
expect(iconColor(Icons.map), accent);
|
|||
|
|
expect(iconColor(Icons.home), isNot(accent));
|
|||
|
|
expect(
|
|||
|
|
tester.widget<Text>(find.text('Maps')).style?.color ??
|
|||
|
|
DefaultTextStyle.of(tester.element(find.text('Maps'))).style.color,
|
|||
|
|
accent,
|
|||
|
|
);
|
|||
|
|
expect(bar().indicatorColor, accent.withValues(alpha: 0.22));
|
|||
|
|
expect(bar().overlayColor, isNull, reason: 'with the fill');
|
|||
|
|
|
|||
|
|
debugNavigationBarStyle.value = shippingNavigationBarStyle;
|
|||
|
|
await tester.pumpAndSettle();
|
|||
|
|
expect(iconColor(Icons.map), accent);
|
|||
|
|
expect(bar().indicatorColor, accent.withValues(alpha: 0.22));
|
|||
|
|
expect(
|
|||
|
|
bar().overlayColor?.resolve({WidgetState.pressed}),
|
|||
|
|
Colors.transparent,
|
|||
|
|
);
|
|||
|
|
expect(bar().animationDuration, Duration.zero);
|
|||
|
|
|
|||
|
|
debugNavigationBarStyle.value = (
|
|||
|
|
variant: NavigationBarVariant.waveWithoutIndicator,
|
|||
|
|
waveDuration: const Duration(milliseconds: 1200),
|
|||
|
|
cornerRadius: 20.0,
|
|||
|
|
);
|
|||
|
|
await tester.pumpAndSettle();
|
|||
|
|
expect(iconColor(Icons.map), accent);
|
|||
|
|
expect(bar().indicatorColor, Colors.transparent);
|
|||
|
|
|
|||
|
|
debugNavigationBarStyle.value = (
|
|||
|
|
variant: NavigationBarVariant.material,
|
|||
|
|
waveDuration: const Duration(milliseconds: 1200),
|
|||
|
|
cornerRadius: 20.0,
|
|||
|
|
);
|
|||
|
|
await tester.pumpAndSettle();
|
|||
|
|
expect(iconColor(Icons.map), isNot(accent));
|
|||
|
|
expect(bar().indicatorColor, isNull);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
testWidgets("the bar's top corners are rounded as the style says, the "
|
|||
|
|
'screen showing through them', (tester) async {
|
|||
|
|
await phone(tester);
|
|||
|
|
BorderRadiusGeometry corners() => tester
|
|||
|
|
.widget<ClipRRect>(find.byKey(FlushNavigationBar.barKey))
|
|||
|
|
.borderRadius;
|
|||
|
|
|
|||
|
|
await tester.pumpWidget(frame(selected: 0));
|
|||
|
|
expect(corners(), const BorderRadius.vertical(top: Radius.circular(20)));
|
|||
|
|
|
|||
|
|
debugNavigationBarStyle.value = (
|
|||
|
|
variant: NavigationBarVariant.waveWithoutFill,
|
|||
|
|
waveDuration: const Duration(milliseconds: 1200),
|
|||
|
|
cornerRadius: 0.0,
|
|||
|
|
);
|
|||
|
|
await tester.pump();
|
|||
|
|
expect(corners(), BorderRadius.zero);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
testWidgets('the debug section switches the look live, and shows nothing '
|
|||
|
|
'outside debug builds', (tester) async {
|
|||
|
|
await phone(tester);
|
|||
|
|
await tester.pumpWidget(
|
|||
|
|
testApp(
|
|||
|
|
const Scaffold(
|
|||
|
|
body: SingleChildScrollView(child: NavigationBarDebugSection()),
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
);
|
|||
|
|
expect(find.text('Navigation bar (debug)'), findsOneWidget);
|
|||
|
|
|
|||
|
|
await tester.tap(find.text('F'));
|
|||
|
|
await tester.pump();
|
|||
|
|
await tester.tap(find.text('2 s'));
|
|||
|
|
await tester.pump();
|
|||
|
|
await tester.tap(find.text('28'));
|
|||
|
|
await tester.pump();
|
|||
|
|
expect(debugNavigationBarStyle.value, (
|
|||
|
|
variant: NavigationBarVariant.waveWithoutIndicator,
|
|||
|
|
waveDuration: const Duration(seconds: 2),
|
|||
|
|
cornerRadius: 28.0,
|
|||
|
|
));
|
|||
|
|
|
|||
|
|
await tester.pumpWidget(
|
|||
|
|
testApp(
|
|||
|
|
const Scaffold(body: NavigationBarDebugSection(debug: false)),
|
|||
|
|
),
|
|||
|
|
);
|
|||
|
|
expect(find.text('Navigation bar (debug)'), findsNothing);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
testWidgets('outside debug builds the frame keeps the shipping look '
|
|||
|
|
'whatever the debug section holds', (tester) async {
|
|||
|
|
expect(
|
|||
|
|
effectiveNavigationBarStyle(debug: false),
|
|||
|
|
shippingNavigationBarStyle,
|
|||
|
|
);
|
|||
|
|
debugNavigationBarStyle.value = (
|
|||
|
|
variant: NavigationBarVariant.material,
|
|||
|
|
waveDuration: const Duration(seconds: 2),
|
|||
|
|
cornerRadius: 0.0,
|
|||
|
|
);
|
|||
|
|
expect(
|
|||
|
|
effectiveNavigationBarStyle(debug: false),
|
|||
|
|
shippingNavigationBarStyle,
|
|||
|
|
);
|
|||
|
|
// Tests run as a debug build.
|
|||
|
|
expect(effectiveNavigationBarStyle(), debugNavigationBarStyle.value);
|
|||
|
|
});
|
|||
|
|
}
|