test(studio): lock sidebar destination Y-stability on rail expansion

Adds a widget test asserting each destination icon keeps its Y position
whether the rail is collapsed or expanded — a recurring regression. To
drive expansion without a hover gesture (which trips RenderFlex overflow
mid-transition), a test-only startSidebarExpanded flag threads
StudioApp -> StudioShell -> _Sidebar (controller starts at 1.0, hover
disabled); _SidebarItems get stable ValueKeys. analyze clean; new test
and the existing smoke test pass.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-06-19 21:30:02 +02:00
parent d66f46f133
commit 36a8252f67
2 changed files with 103 additions and 5 deletions

View file

@ -60,11 +60,18 @@ class StudioApp extends StatefulWidget {
/// ChainTheme.
final String? initialThemePlugin;
/// Test-only: start the sidebar expanded (no hover). Lets a widget
/// test assert destination-icon Y stability across collapsed vs
/// expanded without a hover gesture (which trips RenderFlex overflow
/// mid-transition). Always false in production.
final bool startSidebarExpanded;
const StudioApp({
super.key,
required this.initialThemeMode,
required this.initialLocale,
this.initialThemePlugin,
this.startSidebarExpanded = false,
});
/// Lookup helper so descendants can flip the theme without
@ -226,7 +233,9 @@ class StudioAppState extends State<StudioApp> {
supportedLocales: AppLocalizations.supportedLocales,
localizationsDelegates:
AppLocalizations.localizationsDelegates,
home: const StudioShell(),
home: StudioShell(
startSidebarExpanded: widget.startSidebarExpanded,
),
);
},
),
@ -237,7 +246,10 @@ class StudioAppState extends State<StudioApp> {
}
class StudioShell extends StatefulWidget {
const StudioShell({super.key});
/// Test-only: forward to the sidebar so it starts expanded.
final bool startSidebarExpanded;
const StudioShell({super.key, this.startSidebarExpanded = false});
@override
State<StudioShell> createState() => StudioShellState();
@ -523,6 +535,7 @@ class StudioShellState extends State<StudioShell> {
endpointLabel: HubService.instance.endpointLabel,
activeChannel: _activeChannel,
pendingApprovals: _pendingApprovals,
forceExpanded: widget.startSidebarExpanded,
),
Container(width: 1, color: theme.colorScheme.outlineVariant),
Expanded(
@ -649,6 +662,11 @@ class _Sidebar extends StatefulWidget {
/// without polling the page.
final int pendingApprovals;
/// Test-only: start fully expanded (controller at 1.0) and disable
/// hover, so a widget test can measure the expanded layout without a
/// hover gesture. Always false in production.
final bool forceExpanded;
const _Sidebar({
required this.selectedIndex,
required this.onSelect,
@ -657,6 +675,7 @@ class _Sidebar extends StatefulWidget {
required this.endpointLabel,
required this.activeChannel,
this.pendingApprovals = 0,
this.forceExpanded = false,
});
@override
@ -694,7 +713,7 @@ class _SidebarState extends State<_Sidebar>
// the rail feels crisp rather than sluggish.
duration: ChainMotion.slow,
reverseDuration: ChainMotion.base,
value: 0,
value: widget.forceExpanded ? 1 : 0,
);
}
@ -740,8 +759,8 @@ class _SidebarState extends State<_Sidebar>
final hasChannel =
widget.activeChannel != null && widget.activeChannel!.isNotEmpty;
return MouseRegion(
onEnter: (_) => _ctrl.forward(),
onExit: (_) => _ctrl.reverse(),
onEnter: widget.forceExpanded ? null : (_) => _ctrl.forward(),
onExit: widget.forceExpanded ? null : (_) => _ctrl.reverse(),
child: AnimatedBuilder(
animation: _ctrl,
builder: (context, _) {
@ -837,6 +856,7 @@ class _SidebarState extends State<_Sidebar>
children: [
for (var i = 0; i < widget.pages.length; i++)
_SidebarItem(
key: ValueKey('sidebar-item-${widget.pages[i].id}'),
page: widget.pages[i],
selected: i == widget.selectedIndex,
t: t,
@ -1342,6 +1362,7 @@ class _SidebarItem extends StatefulWidget {
final int? badge;
const _SidebarItem({
super.key,
required this.page,
required this.selected,
required this.t,