halkyon/packages/halkyon_ui/test/widget/navigation_frame_test.dart
Tizian 「ティツィアーン」 bc4953aaf8
fix(learn,ui): the open assistant field holds still: no flicker, no sliding away, no resizing under the keyboard (ADR 0021, D69)
Inside a studyset the field flickered as it opened and closed: the
floating location switched between a raised centre and the centre,
which Scaffold animates by scaling the button out and in. It keeps one
location now, raised above the licence line by padding. While open it
no longer slides away as the studyset or the Library scrolls
(ScrollHidingFab.pinned, new), and the keyboard covers Home, the
Library, a studyset and a session rather than shrinking them; the field
lifts itself by as much of the keyboard as reaches above it.
NavigationFrame no longer resizes for the keyboard itself, leaving it
to each tab's own scaffold, which still does by default (Todo's,
Train's and Meditate's tests pass unchanged).

Whats-New: Inside a studyset, the question field opens without flickering, stays while you scroll, and the screen no longer jumps when the keyboard comes.
Whats-New-De: In einem Lernset öffnet sich das Fragefeld ohne Flackern, bleibt beim Scrollen stehen, und der Bildschirm springt nicht mehr, wenn die Tastatur kommt.

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

237 lines
7.9 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';
/// 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);
});
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),
);
}
}
});
testWidgets("the keyboard leaves the frame's tab its size: the tab's own "
'screen decides whether it makes way, as one with a field floating '
"over it doesn't", (tester) async {
tester.view.physicalSize = const Size(400, 800);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
Widget frame({required bool screenResizes}) => testApp(
NavigationFrame(
appName: 'Halkyon Test',
icon: icon,
selectedIndex: 0,
onSelected: (_) {},
destinations: const [
(label: 'Home', icon: Icon(Icons.home), selectedIcon: null),
(label: 'Maps', icon: Icon(Icons.map), selectedIcon: null),
],
child: Scaffold(
resizeToAvoidBottomInset: screenResizes,
body: const SizedBox.expand(key: Key('tab')),
),
),
);
await tester.pumpWidget(frame(screenResizes: false));
final before = tester.getSize(find.byKey(const Key('tab')));
tester.view.viewInsets = const FakeViewPadding(bottom: 300);
addTearDown(tester.view.resetViewInsets);
await tester.pump();
expect(tester.getSize(find.byKey(const Key('tab'))), before);
await tester.pumpWidget(frame(screenResizes: true));
expect(
tester.getSize(find.byKey(const Key('tab'))).height,
lessThan(before.height),
reason: 'a screen that resizes still makes way',
);
});
}