halkyon/packages/halkyon_ui/test/widget/navigation_sidebar_test.dart

245 lines
7.9 KiB
Dart
Raw Normal View History

import 'dart:typed_data';
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 rail opening into a sidebar with labels (decided by the developer,
/// 9 and 10 Nov 2026): phones keep the bar, medium windows a rail alone,
/// and from expanded windows on a toggle at the rail's foot opens it into a
/// sidebar 256 wide, kept open or closed as the app keeps it, its labels
/// coming in one after another, all at once without animations.
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,
]),
);
/// The frame in a window [width] wide, the sidebar [open], its toggles
/// going into [toggled] (none offered where null), the tabs chosen into
/// [chosen].
Future<void> show(
WidgetTester tester, {
required double width,
bool open = false,
List<bool>? toggled,
List<int>? chosen,
Widget? sections,
bool still = false,
bool settle = true,
}) async {
tester.view.physicalSize = Size(width, 800);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
final frame = NavigationFrame(
appName: 'Halkyon Test',
icon: icon,
accent: const Color(0xFFEC3E7C),
selectedIndex: 0,
onSelected: (index) => chosen?.add(index),
sidebarOpen: open,
onSidebarChanged: toggled?.add,
sidebarSections: sections,
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')),
);
await tester.pumpWidget(
testApp(
still
? Builder(
builder: (context) => MediaQuery(
data: MediaQuery.of(
context,
).copyWith(disableAnimations: true),
child: frame,
),
)
: frame,
),
);
if (settle) {
await tester.pumpAndSettle();
} else {
await tester.pump();
}
}
testWidgets('medium windows keep the rail alone: no toggle, open or not', (
tester,
) async {
await show(tester, width: 700, open: true, toggled: []);
expect(find.byType(NavigationRail), findsOneWidget);
expect(find.byKey(NavigationFrame.sidebarKey), findsNothing);
expect(find.byTooltip('Open sidebar'), findsNothing);
expect(find.byTooltip('Close sidebar'), findsNothing);
});
testWidgets('from expanded windows on, a toggle at the foot of the rail '
'asks for the sidebar', (tester) async {
final toggled = <bool>[];
await show(tester, width: 1000, toggled: toggled);
final toggle = find.byTooltip('Open sidebar');
expect(toggle, findsOneWidget);
expect(
tester.getBottomLeft(toggle).dy,
greaterThan(tester.getBottomLeft(find.text('Profile')).dy + 200),
reason: 'at the foot, apart from the tabs',
);
await tester.tap(toggle);
await tester.pump();
expect(toggled, [true]);
});
testWidgets('an app that keeps no choice offers no toggle', (tester) async {
await show(tester, width: 1000);
expect(find.byTooltip('Open sidebar'), findsNothing);
});
testWidgets('open, a sidebar 256 wide: the app named beside its icon, '
'each tab its label beside its icon, what the app lists below them, '
'and a toggle closing it', (tester) async {
final toggled = <bool>[];
final chosen = <int>[];
await show(
tester,
width: 1000,
open: true,
toggled: toggled,
chosen: chosen,
sections: const Text('Favourites'),
);
final sidebar = find.byKey(NavigationFrame.sidebarKey);
expect(tester.getSize(sidebar).width, 256);
expect(find.byType(NavigationRail), findsNothing);
expect(find.text('Halkyon Test'), findsOneWidget);
final mapsIcon = tester.getRect(find.byIcon(Icons.map));
final mapsLabel = tester.getRect(find.text('Maps'));
expect(mapsLabel.left, greaterThan(mapsIcon.right));
expect(mapsLabel.center.dy, closeTo(mapsIcon.center.dy, 1));
expect(
find.descendant(of: sidebar, matching: find.text('Favourites')),
findsOneWidget,
);
await tester.tap(find.text('Maps'));
await tester.pump();
expect(chosen, [1]);
await tester.tap(find.byTooltip('Close sidebar'));
await tester.pump();
expect(toggled, [false]);
});
testWidgets('its icons stand where the rail had them, so opening it moves '
'nothing under the pointer', (tester) async {
await show(tester, width: 1000, toggled: []);
final closed = tester.getCenter(find.byIcon(Icons.map)).dx;
await show(tester, width: 1000, open: true, toggled: []);
expect(tester.getCenter(find.byIcon(Icons.map)).dx, closeTo(closed, 1));
});
testWidgets('kept open in a medium window, it is the rail there', (
tester,
) async {
await show(tester, width: 1000, open: true, toggled: []);
tester.view.physicalSize = const Size(700, 800);
await tester.pumpAndSettle();
expect(find.byType(NavigationRail), findsOneWidget);
expect(find.byKey(NavigationFrame.sidebarKey), findsNothing);
});
testWidgets('opening it, the labels come in one after another, top to '
'bottom', (tester) async {
await show(tester, width: 1000, toggled: []);
await show(tester, width: 1000, open: true, toggled: [], settle: false);
await tester.pump(const Duration(milliseconds: 100));
double opacityOf(String label) => tester
.widget<FadeTransition>(
find
.ancestor(
of: find.text(label),
matching: find.byType(FadeTransition),
)
.first,
)
.opacity
.value;
expect(opacityOf('Home'), greaterThan(opacityOf('Profile')));
await tester.pumpAndSettle();
expect(opacityOf('Profile'), 1);
});
testWidgets('without animations, it opens whole at once', (tester) async {
await show(tester, width: 1000, toggled: [], still: true);
await show(
tester,
width: 1000,
open: true,
toggled: [],
still: true,
settle: false,
);
expect(tester.getSize(find.byKey(NavigationFrame.sidebarKey)).width, 256);
expect(
tester
.widget<FadeTransition>(
find
.ancestor(
of: find.text('Profile'),
matching: find.byType(FadeTransition),
)
.first,
)
.opacity
.value,
1,
);
});
testWidgets('its toggle speaks German too', (tester) async {
tester.view.physicalSize = const Size(1000, 800);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
await tester.pumpWidget(
testApp(
NavigationFrame(
appName: 'Halkyon Test',
icon: icon,
accent: const Color(0xFFEC3E7C),
selectedIndex: 0,
onSelected: (_) {},
onSidebarChanged: (_) {},
destinations: const [
(label: 'Start', icon: Icon(Icons.home), selectedIcon: null),
],
child: const Scaffold(body: Text('The tab')),
),
locale: const Locale('de'),
),
);
await tester.pumpAndSettle();
expect(find.byTooltip('Seitenleiste öffnen'), findsOneWidget);
});
}