2026-10-30 23:52:39 +01:00
|
|
|
|
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';
|
|
|
|
|
|
|
|
|
|
|
|
/// Every Halkyon app's frame around its tabs: a rail with the app's icon
|
|
|
|
|
|
/// from medium windows on, a floating bar below, a tick on every choice.
|
|
|
|
|
|
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, on tab [selected]; the tabs chosen
|
|
|
|
|
|
/// go into [chosen].
|
|
|
|
|
|
Future<void> show(
|
|
|
|
|
|
WidgetTester tester, {
|
|
|
|
|
|
required double width,
|
|
|
|
|
|
required List<int> chosen,
|
|
|
|
|
|
int selected = 1,
|
|
|
|
|
|
}) async {
|
|
|
|
|
|
tester.view.physicalSize = Size(width, 800);
|
|
|
|
|
|
tester.view.devicePixelRatio = 1;
|
|
|
|
|
|
addTearDown(tester.view.reset);
|
|
|
|
|
|
await tester.pumpWidget(
|
|
|
|
|
|
testApp(
|
|
|
|
|
|
NavigationFrame(
|
|
|
|
|
|
appName: 'Halkyon Test',
|
|
|
|
|
|
icon: icon,
|
|
|
|
|
|
selectedIndex: selected,
|
|
|
|
|
|
onSelected: chosen.add,
|
|
|
|
|
|
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')),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// The ticks the platform was asked for while [act] ran.
|
|
|
|
|
|
Future<List<Object?>> ticks(
|
|
|
|
|
|
WidgetTester tester,
|
|
|
|
|
|
Future<void> Function() act,
|
|
|
|
|
|
) async {
|
|
|
|
|
|
final haptics = <Object?>[];
|
|
|
|
|
|
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
|
|
|
|
SystemChannels.platform,
|
|
|
|
|
|
(call) async {
|
|
|
|
|
|
if (call.method == 'HapticFeedback.vibrate') {
|
|
|
|
|
|
haptics.add(call.arguments);
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
addTearDown(
|
|
|
|
|
|
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
|
|
|
|
SystemChannels.platform,
|
|
|
|
|
|
null,
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
await act();
|
|
|
|
|
|
return haptics;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
testWidgets('below medium windows, a bar floats above the foot', (
|
|
|
|
|
|
tester,
|
|
|
|
|
|
) async {
|
|
|
|
|
|
await show(tester, width: 400, chosen: []);
|
|
|
|
|
|
|
|
|
|
|
|
expect(find.byType(FloatingNavigationBar), findsOneWidget);
|
|
|
|
|
|
expect(find.byType(NavigationRail), findsNothing);
|
|
|
|
|
|
expect(find.text('The tab'), findsOneWidget);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
testWidgets("from medium windows on, a rail with the app's icon on top, "
|
|
|
|
|
|
'which leads to the first tab', (tester) async {
|
|
|
|
|
|
final chosen = <int>[];
|
|
|
|
|
|
await show(tester, width: 800, chosen: chosen);
|
|
|
|
|
|
|
|
|
|
|
|
expect(find.byType(NavigationRail), findsOneWidget);
|
|
|
|
|
|
expect(find.byType(NavigationBar), findsNothing);
|
|
|
|
|
|
expect(find.bySemanticsLabel('Halkyon Test'), findsOneWidget);
|
|
|
|
|
|
|
|
|
|
|
|
await tester.tap(find.byTooltip('Home'));
|
|
|
|
|
|
await tester.pump();
|
|
|
|
|
|
expect(chosen, [0]);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
testWidgets('every choice on the bar ticks, lightly', (tester) async {
|
|
|
|
|
|
final chosen = <int>[];
|
|
|
|
|
|
await show(tester, width: 400, chosen: chosen);
|
|
|
|
|
|
final haptics = await ticks(tester, () async {
|
|
|
|
|
|
await tester.tap(find.text('Profile'));
|
|
|
|
|
|
await tester.pump();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
expect(haptics, ['HapticFeedbackType.selectionClick']);
|
|
|
|
|
|
expect(chosen, [2]);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
testWidgets('every choice on the rail ticks, lightly', (tester) async {
|
|
|
|
|
|
final chosen = <int>[];
|
|
|
|
|
|
await show(tester, width: 800, chosen: chosen);
|
|
|
|
|
|
final haptics = await ticks(tester, () async {
|
|
|
|
|
|
await tester.tap(find.text('Maps'));
|
|
|
|
|
|
await tester.pump();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
expect(haptics, ['HapticFeedbackType.selectionClick']);
|
|
|
|
|
|
expect(chosen, [1]);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
testWidgets('the window crossing from compact to medium and back swaps '
|
|
|
|
|
|
'the bar for the rail', (tester) async {
|
|
|
|
|
|
await show(tester, width: 400, chosen: []);
|
|
|
|
|
|
for (final width in const [800.0, 400.0]) {
|
|
|
|
|
|
tester.view.physicalSize = Size(width, 800);
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
expect(find.byType(FloatingNavigationBar), findsOneWidget);
|
|
|
|
|
|
expect(find.text('The tab'), findsOneWidget);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
testWidgets('the bar steps aside where a screen has a bar of its own, '
|
|
|
|
|
|
"leaving the system's bar its room", (tester) async {
|
|
|
|
|
|
tester.view.physicalSize = const Size(400, 800);
|
|
|
|
|
|
tester.view.devicePixelRatio = 1;
|
|
|
|
|
|
tester.view.padding = const FakeViewPadding(bottom: 24);
|
|
|
|
|
|
addTearDown(tester.view.reset);
|
|
|
|
|
|
await tester.pumpWidget(
|
|
|
|
|
|
testApp(
|
|
|
|
|
|
NavigationFrame(
|
|
|
|
|
|
appName: 'Halkyon Test',
|
|
|
|
|
|
icon: icon,
|
|
|
|
|
|
selectedIndex: 0,
|
|
|
|
|
|
onSelected: (_) {},
|
|
|
|
|
|
hidesNavigationBar: true,
|
|
|
|
|
|
destinations: const [
|
|
|
|
|
|
(label: 'Home', icon: Icon(Icons.home), selectedIcon: null),
|
|
|
|
|
|
(label: 'Maps', icon: Icon(Icons.map), selectedIcon: null),
|
|
|
|
|
|
],
|
|
|
|
|
|
child: const Scaffold(body: Text('The tab')),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
expect(find.byType(FloatingNavigationBar), findsNothing);
|
|
|
|
|
|
expect(tester.getBottomLeft(find.byType(Scaffold).last).dy, 800 - 24);
|
|
|
|
|
|
});
|
2026-10-31 12:12:55 +01:00
|
|
|
|
|
|
|
|
|
|
testWidgets('a status stands above the bar below medium windows, and at '
|
|
|
|
|
|
"the foot of the screen's panel from there on", (tester) async {
|
|
|
|
|
|
for (final width in const [400.0, 800.0]) {
|
|
|
|
|
|
tester.view.physicalSize = Size(width, 800);
|
|
|
|
|
|
tester.view.devicePixelRatio = 1;
|
|
|
|
|
|
addTearDown(tester.view.reset);
|
|
|
|
|
|
await tester.pumpWidget(
|
|
|
|
|
|
testApp(
|
|
|
|
|
|
NavigationFrame(
|
|
|
|
|
|
appName: 'Halkyon Test',
|
|
|
|
|
|
icon: icon,
|
|
|
|
|
|
selectedIndex: 0,
|
|
|
|
|
|
onSelected: (_) {},
|
|
|
|
|
|
status: const Text('Recording'),
|
|
|
|
|
|
destinations: const [
|
|
|
|
|
|
(label: 'Home', icon: Icon(Icons.home), selectedIcon: null),
|
|
|
|
|
|
(label: 'Maps', icon: Icon(Icons.map), selectedIcon: null),
|
|
|
|
|
|
],
|
|
|
|
|
|
child: const Scaffold(body: Text('The tab')),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
|
|
final status = tester.getBottomLeft(find.text('Recording')).dy;
|
|
|
|
|
|
final tab = tester.getTopLeft(find.text('The tab')).dy;
|
|
|
|
|
|
expect(status, greaterThan(tab), reason: '$width');
|
|
|
|
|
|
if (width < 600) {
|
|
|
|
|
|
expect(
|
|
|
|
|
|
status,
|
|
|
|
|
|
lessThan(tester.getTopLeft(find.byType(NavigationBar)).dy),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-10-30 23:52:39 +01:00
|
|
|
|
}
|