chain-studio/test/sidebar_test.dart
flemming-it c436e12601
Some checks are pending
Security / Security check (push) Waiting to run
feat(nav): discoverable sidebar — instant tooltips, shortcuts made visible, pinnable rail
The icon-only rail forced first-time users to guess (usertest:
Senior, a11y, UX personas). Three changes:
- Nav tooltips appear instantly and carry the page shortcut
  (Cmd+1..9, Ctrl on non-mac — Ctrl activators added); expanded
  labels show the same hint. Explicit button semantics for
  screen readers on every destination.
- A visible 'Search & commands' row above the footer opens the
  existing Cmd+K palette, which nothing in the UI advertised.
- Settings -> Appearance gains 'Keep the navigation expanded':
  pins the rail with permanent labels (persisted preference).
Footer strip and pillar toggle made overflow-safe for the
animating rail; responsive test scrolls the by-design scrollable
destinations list.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
2026-07-17 23:36:33 +02:00

84 lines
2.9 KiB
Dart

// Locks the sidebar invariant: destination icons sit at the same Y
// whether the rail is collapsed or expanded. The header rows above the
// destinations list have FIXED pixel heights (see _SidebarState), so
// expanding the rail (a width-only change) must not shift any
// destination vertically. This was a recurring regression source; this
// test pins it.
//
// Expansion is driven via StudioApp(startSidebarExpanded: true), NOT a
// hover gesture — hover mid-transition trips RenderFlex overflow in
// widget tests (see widget_test.dart). With the flag the rail's
// animation controller starts at value 1.0 (fully expanded), so no
// animation needs to settle.
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:chain_studio/data/hub.dart';
import 'package:chain_studio/main.dart';
const _destinations = <String>[
'welcome',
'store',
'doctor',
'flows',
'audit',
'approvals',
'runs',
'federation',
];
Map<String, double> _destinationYs(WidgetTester tester) {
final ys = <String, double>{};
for (final id in _destinations) {
final finder = find.byKey(ValueKey('sidebar-item-$id'));
expect(finder, findsOneWidget, reason: 'destination "$id" must render');
ys[id] = tester.getTopLeft(finder).dy;
}
return ys;
}
void main() {
testWidgets(
'sidebar destination Y positions are stable across rail expansion',
(tester) async {
// Desktop-sized surface: the rail carries 8 destinations plus
// the search row and footer — the 600-px default viewport
// scrolls the last destination out of the ListView.
tester.view.physicalSize = const Size(1280, 900);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.reset);
// Collapsed (default).
await tester.pumpWidget(
const StudioApp(
initialThemeMode: ThemeModeValue.system,
initialLocale: Locale('en'),
),
);
// pump (not pumpAndSettle): the app has long-lived timers/animations
// that never "settle"; one frame is enough to lay the sidebar out.
await tester.pump(const Duration(milliseconds: 100));
final collapsed = _destinationYs(tester);
// Expanded: controller starts at 1.0 via the flag, hover disabled.
await tester.pumpWidget(
const StudioApp(
initialThemeMode: ThemeModeValue.system,
initialLocale: Locale('en'),
startSidebarExpanded: true,
),
);
await tester.pump(const Duration(milliseconds: 100));
final expanded = _destinationYs(tester);
for (final id in _destinations) {
expect(
expanded[id],
closeTo(collapsed[id]!, 0.5),
reason: 'destination "$id" Y shifted on expansion '
'(${collapsed[id]} -> ${expanded[id]}) — the rows above the '
'destinations list must keep fixed heights',
);
}
},
);
}