245 lines
7.9 KiB
Dart
245 lines
7.9 KiB
Dart
|
|
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);
|
|||
|
|
});
|
|||
|
|
}
|