D2a: ui-egui integration tests use craft-ui's docking ids, labels and geometry

- Focus and traversal tests use DockArea::hit_id for tabs and dividers; the
  focus outline is checked as craft-ui draws it (inset 2 pt, accent colour)
  while the tab has keyboard focus from traversal.
- Floating resize grips are checked under craft-ui's accessibility label.
- Stack resizes compare rendered heights: craft-ui shares a stack by the
  entries' heights as proportions, so the saved heights are checked as ratios.
- Drops into a split use the side zone depth (SIDE_EDGE_ZONE_DEPTH).
- Panels that need their whole area (scroll audit, automation audit) are
  docked alone: maximizing a stacked panel maximizes its whole stack.
- ui_tabs: dragging by the panel menu button is gone (craft-ui drags by tab,
  drag bar or border); the test docks by the tab. A stacked header's -/+
  button collapses and expands, a click on a closed header opens it; a
  click on an open header no longer collapses it.
- ui_tabs (#302): a panel dropped on a stacked panel joins the stack as its
  own entry below it; craft-ui stack entries hold one panel each.
- ui_panel_focus: a reopened Composition goes back to its own index
  ([Composition, Layer]), craft-ui's restore order.
- ui_menus probes the Effect menu with Remove All, since Effect Controls
  also names a tab.
This commit is contained in:
aiongg
2026-10-25 05:41:47 +00:00
parent e89aff1e99
commit 3eae1747ae
10 changed files with 97 additions and 63 deletions
+8 -4
View File
@@ -128,13 +128,14 @@ fn pointer_drop_merges_floats_and_redocks_without_project_changes() {
drag(&mut h, from, pos2(700.0, 400.0), Modifiers::COMMAND);
assert_eq!(floating(h.state()).len(), 1);
assert!(h.state().ui.dock.is_floating(PanelKind::Info));
assert_eq!(h.get_by_label("Resize floating panel").accesskit_node().role(), egui::accesskit::Role::Button);
let grips: Vec<_> = h.query_all_by_label("Resize panel window").collect();
assert!(!grips.is_empty() && grips.iter().all(|grip| grip.accesskit_node().role() == egui::accesskit::Role::Button));
// Its corner resizes it and its title bar moves it.
let original = floating(h.state())[0].rect;
let grip = rect(&h, "panel.float.0.resize").center();
drag(&mut h, grip, grip + vec2(36.0, 24.0), Modifiers::NONE);
drag(&mut h, grip, grip - vec2(36.0, 24.0), Modifiers::NONE);
let resized = floating(h.state())[0].rect;
assert!(resized[2] > original[2] + 25.0 && resized[3] > original[3] + 15.0, "{original:?} -> {resized:?}");
assert!(resized[2] < original[2] - 25.0 && resized[3] < original[3] - 15.0, "{original:?} -> {resized:?}");
let bar = rect(&h, "panel.float.0.move");
drag(&mut h, bar.center(), bar.center() - vec2(24.0, 18.0), Modifiers::NONE);
let moved = floating(h.state())[0].rect;
@@ -305,10 +306,13 @@ fn stacked_panels_resize_by_pointer_without_changing_closed_entries() {
dock::docked(dock::stack(&[(PanelKind::Preview, true, Some(100.0)), (PanelKind::Properties, true, None), (PanelKind::Align, false, Some(77.0))]));
h.run_steps(3);
let project = h.state().session.project.clone();
let before = rect(&h, "panel.Preview").height();
let from = rect(&h, "dock.gutter.s0").center();
drag(&mut h, from, from + vec2(0.0, 16.0), Modifiers::NONE);
let after = rect(&h, "panel.Preview").height();
assert!((after - before - 16.0).abs() < 2.0, "{before} -> {after}");
let Some(Node::Stack { entries }) = &h.state().ui.dock.layout.root else { panic!("stack lost") };
assert!(entries[0].height.unwrap_or_default() > 110.0, "{entries:?}");
assert!(entries[0].height.unwrap_or_default() > 100.0, "{entries:?}");
assert_eq!(entries[2], StackEntry { panel: PanelKind::Align, open: false, height: Some(77.0) });
assert!(std::sync::Arc::ptr_eq(&project, &h.state().session.project));
}
@@ -96,6 +96,8 @@ fn covered(w: Rect, elements: &[Rect]) -> bool {
/// (panel, widget rect, label) of every interactive widget no automation element covers, for
/// one panel shown maximized.
fn gaps_of(h: &mut Harness<'_, EffectcraftApp>, p: PanelKind) -> Vec<String> {
// Docked alone: maximizing a stacked panel maximizes its whole stack.
h.state_mut().ui.dock = effectcraft_ui_egui::dock::docked(effectcraft_ui_egui::dock::tabs(&[p], 0));
h.state_mut().show_panel(p);
h.state_mut().ui.maximized = Some(p);
h.run_steps(4);
+3 -2
View File
@@ -1,10 +1,11 @@
//! Shared pane resizing in the full application, including application shortcut ownership.
use craft_ui::docking::{Divider, DockArea, Hit};
use effectcraft_engine::Session;
use effectcraft_ui_egui::EffectcraftApp;
use effectcraft_ui_egui::dock::{self, Node, PanelKind, SplitSize};
use effectcraft_ui_egui::theme::ThemeKind;
use egui::{Event, Id, Key, Modifiers, PointerButton, Rect, pos2, vec2};
use egui::{Event, Key, Modifiers, PointerButton, Rect, pos2, vec2};
use egui_kittest::Harness;
use serde_json::json;
@@ -50,7 +51,7 @@ fn focused_dock_resize_owns_arrows_and_keeps_pointer_dragging() {
h.run_steps(3);
let before_project = h.state().session.project.clone();
let before_times = h.state().session.state.times.clone();
let id = Id::new(("dock-gutter", String::new()));
let id = DockArea::<PanelKind>::hit_id(effectcraft_ui_egui::dock_ui::area_id(), &Hit::Divider(Divider::Split(Vec::new())));
h.ctx.memory_mut(|memory| memory.request_focus(id));
h.run_steps(2);
assert_eq!(h.ctx.memory(|memory| memory.focused()), Some(id));
+2
View File
@@ -288,6 +288,8 @@ fn composition_tabs_and_scrolling_preview() {
let mut s = Session::default();
s.execute("file.openDemoProject", json!({})).unwrap();
let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_eframe(|_| EffectcraftApp::new(s));
// A short Preview panel scrolls its controls (stacked panels share the column by height).
h.state_mut().apply_dock(craft_ui::docking::Action::ResizeStack { panel: PanelKind::Properties, height: Some(650.0) }).unwrap();
h.run_steps(4);
rect(&h, "panel.tab.Timeline.close");
rect(&h, "preview.scroll");
+3 -2
View File
@@ -318,7 +318,8 @@ fn hold_browse_opens_a_submenu_and_the_release_inside_picks_an_entry() {
h.state_mut().session.state.selected_layers = vec![first];
let effect = rect(&h, "menu.Effect");
press(&mut h, effect.center());
assert!(shown(&h, "Effect Controls"), "the Effect menu opens on the press");
// (Effect ▸ Remove All: "Effect Controls" also names the panel's tab.)
assert!(shown(&h, "Remove All"), "the Effect menu opens on the press");
// The categories come from the effect registry; the first alphabetically is "3D Channel".
let cat = h.query_by_label_contains("3D Channel").expect("the category row is shown").rect();
move_to(&mut h, cat.center());
@@ -326,7 +327,7 @@ fn hold_browse_opens_a_submenu_and_the_release_inside_picks_an_entry() {
let entry = h.query_by_label_contains("Cryptomatte").expect("the submenu is open").rect();
move_to(&mut h, entry.center());
release(&mut h, entry.center());
assert!(!shown(&h, "Effect Controls"), "the release closed the menus");
assert!(!shown(&h, "Remove All"), "the release closed the menus");
assert!(h.state().session.state.last_effect.is_some(), "and the release chose the entry: it applied its effect");
}
+3 -3
View File
@@ -67,7 +67,7 @@ fn opening_a_comp_brings_up_a_closed_composition_panel() {
assert_eq!(h.state().session.active_comp_id(), Some(ItemId(other)));
let dock = &h.state().ui.dock;
assert!(dock.is_visible(PanelKind::Composition), "the Composition panel is back and in front");
assert_eq!(group_of(dock, PanelKind::Composition), Some(vec![PanelKind::Layer, PanelKind::Composition]), "back in the centre");
assert_eq!(group_of(dock, PanelKind::Composition), Some(vec![PanelKind::Composition, PanelKind::Layer]), "back in the centre");
assert!(h.state().auto.elements.iter().any(|e| e.id == "panel.Composition"), "the Composition panel is drawn");
// Open Composition (the Project panel's context menu) with the panel closed again.
@@ -75,12 +75,12 @@ fn opening_a_comp_brings_up_a_closed_composition_panel() {
invoke(&mut h, "comp.open", json!({"comp": main}));
assert_eq!(h.state().session.active_comp_id(), Some(ItemId(main)));
assert!(h.state().ui.dock.is_visible(PanelKind::Composition));
assert_eq!(group_of(&h.state().ui.dock, PanelKind::Composition), Some(vec![PanelKind::Layer, PanelKind::Composition]));
assert_eq!(group_of(&h.state().ui.dock, PanelKind::Composition), Some(vec![PanelKind::Composition, PanelKind::Layer]));
// Window ▸ Composition reopens it in the centre too (not beside Effects & Presets).
invoke(&mut h, "window.closePanel", json!({"panel": "Composition"}));
invoke(&mut h, "window.panel", json!({"panel": "Composition"}));
assert_eq!(group_of(&h.state().ui.dock, PanelKind::Composition), Some(vec![PanelKind::Layer, PanelKind::Composition]));
assert_eq!(group_of(&h.state().ui.dock, PanelKind::Composition), Some(vec![PanelKind::Composition, PanelKind::Layer]));
assert!(h.state().ui.dock.is_visible(PanelKind::Composition));
}
+4 -2
View File
@@ -182,8 +182,9 @@ fn panels_taller_than_their_space_have_a_scroll_bar_to_drag() {
for _ in 0..6 {
app.session.execute("renderQueue.add", json!({})).unwrap();
}
// The panel alone in the window (maximizing a stacked panel maximizes its whole stack).
app.ui.dock = effectcraft_ui_egui::dock::docked(effectcraft_ui_egui::dock::tabs(&[panel], 0));
app.show_panel(panel);
app.ui.maximized = Some(panel);
h.run_steps(3);
let (_, max) = scrolled(&h, id);
assert!(max > 0.0, "{id}: the rows overflow");
@@ -193,7 +194,8 @@ fn panels_taller_than_their_space_have_a_scroll_bar_to_drag() {
click_at(&mut h, pos2(bar.center().x, bar.min.y + 2.0));
assert_eq!(scrolled(&h, id).0, 0.0, "{id}: at the top");
drag(&mut h, pos2(bar.center().x, bar.min.y + 6.0), pos2(bar.center().x, bar.max.y + 40.0));
assert_eq!(scrolled(&h, id).0, max, "{id}: dragged to the end");
let end = scrolled(&h, id).0;
assert!((end - max).abs() < 0.01, "{id}: dragged to the end: {end} of {max}");
}
}
+51 -38
View File
@@ -73,10 +73,11 @@ fn project_double_click_opens_a_comp_as_its_own_timeline_tab() {
assert_eq!(open_comps(&h), vec![main, pre]);
assert_eq!(h.state().session.state.active_comp, Some(ItemId(pre)));
assert_eq!(h.state().session.project.item(ItemId(pre)).map(|i| i.name.clone()), Some("Pre".into()));
// One Timeline tab per comp, the newest last; the shown one answers to the panel's id.
// One comp tab per comp in the Timeline's tab strip, the newest last.
let (tm, tp) = (rect(&h, &format!("panel.tab.Timeline.{main}")), rect(&h, &format!("panel.tab.Timeline.{pre}")));
assert!(tp.min.x > tm.max.x, "{tm:?} {tp:?}");
assert_eq!(rect(&h, "panel.tab.Timeline"), tp);
assert!(tp.min.x >= tm.max.x - 0.5, "{tm:?} {tp:?}");
let strip = rect(&h, "panel.tab.Timeline");
assert!(strip.y_range().contains(tp.center().y) && tp.min.x > strip.max.x, "{strip:?} {tp:?}");
// Clicking Main's tab shows Main again; both stay open.
click_n(&mut h, tm.center(), 1);
assert_eq!(h.state().session.state.active_comp, Some(ItemId(main)));
@@ -171,18 +172,17 @@ fn double_clicking_a_precomp_bar_opens_its_comp() {
assert_eq!(h.state().session.active_comp_id(), Some(ItemId(pre)));
}
/// Issue #46: a panel dragged by its panel-menu (hamburger) button onto the left edge of another
/// group docks there as its own group; dragging the gap between panels resizes them.
/// Issue #46: a panel dragged onto the left edge of another group docks there as its own group;
/// dragging the gap between panels resizes them.
#[test]
fn panels_dock_beside_others_and_gutters_resize_them() {
let (mut h, _, _) = harness();
let dock = |h: &Harness<'_, EffectcraftApp>| h.state().ui.dock.clone();
// Show Effect Controls in its group so its menu button is on screen, then drag by it.
let tab = rect(&h, "panel.tab.EffectControls");
click_n(&mut h, tab.center(), 1);
let grip = rect(&h, "panel.menu.EffectControls").center();
let tl = rect(&h, "panel.Timeline");
drag(&mut h, grip, pos2(tl.min.x + tl.width() * 0.08, tl.center().y));
// Inside the Timeline body's left drop zone.
let depth = craft_ui::docking::SIDE_EDGE_ZONE_DEPTH.min(tl.width().min(tl.height()) / 4.0);
drag(&mut h, tab.center(), pos2(tl.min.x + depth / 2.0, tl.center().y));
assert_eq!(group_of(&dock(&h), PanelKind::EffectControls), Some(vec![PanelKind::EffectControls]));
assert!(!group_of(&dock(&h), PanelKind::Timeline).unwrap().contains(&PanelKind::EffectControls));
let (fx, tl) = (rect(&h, "panel.EffectControls"), rect(&h, "panel.Timeline"));
@@ -221,24 +221,26 @@ fn stacked_panel_gaps_resize_the_panels() {
}
let (preview, props) = (rect(&h, "panel.Preview"), rect(&h, "panel.Properties"));
let gap = rect(&h, "dock.gutter.bs0");
assert!(gap.center().y > preview.max.y && gap.center().y < props.min.y, "the gap below Preview: {gap:?}");
assert!(gap.center().y >= preview.max.y - 1.0 && gap.center().y <= props.min.y + 1.0, "the gap below Preview: {gap:?}");
drag(&mut h, gap.center(), gap.center() + egui::vec2(0.0, 150.0));
let (p2, q2) = (rect(&h, "panel.Preview"), rect(&h, "panel.Properties"));
assert!((p2.height() - preview.height() - 150.0).abs() < 2.0, "Preview {} → {}", preview.height(), p2.height());
assert!((props.height() - q2.height() - 150.0).abs() < 2.0, "Properties {} → {}", props.height(), q2.height());
assert!((q2.max.y - props.max.y).abs() < 1.0, "the column stays filled");
let saved = height(&h.state().ui.dock, PanelKind::Preview).flatten().expect("Preview's height is in the layout");
assert!((saved - p2.height()).abs() < 2.0, "{saved} vs {}", p2.height());
assert_eq!(height(&h.state().ui.dock, PanelKind::Properties), Some(None), "Properties still takes the rest");
// The saved heights share the column as the panels do.
let saved = |p| height(&h.state().ui.dock, p).flatten().expect("the height is in the layout");
let (sp, sq) = (saved(PanelKind::Preview), saved(PanelKind::Properties));
assert!((sp / sq - p2.height() / q2.height()).abs() < 0.02, "{sp}/{sq} vs {}/{}", p2.height(), q2.height());
// Collapsing and expanding Preview keeps its height.
let tab = rect(&h, "panel.tab.Preview");
click_n(&mut h, tab.center(), 1);
let toggle = rect(&h, "panel.header.Preview");
click_n(&mut h, toggle.center(), 1);
assert!(!h.state().ui.dock.is_visible(PanelKind::Preview));
click_n(&mut h, tab.center(), 1);
click_n(&mut h, toggle.center(), 1);
assert!((rect(&h, "panel.Preview").height() - p2.height()).abs() < 1.0);
// Not below the minimum.
// Dragged up to Preview's top, it stops at its minimum.
let gap = rect(&h, "dock.gutter.bs0");
drag(&mut h, gap.center(), gap.center() - egui::vec2(0.0, 400.0));
let top = rect(&h, "panel.Preview").min.y;
drag(&mut h, gap.center(), pos2(gap.center().x, top));
assert!((rect(&h, "panel.Preview").height() - 40.0).abs() < 1.0, "{}", rect(&h, "panel.Preview").height());
}
@@ -333,41 +335,52 @@ fn dragging_the_viewer_timeline_divider_far_up_does_not_crash() {
h.run_steps(2);
}
/// Issue #301: clicking anywhere on a stacked panel's header bar expands/collapses it, not only
/// its name; the panel menu button keeps its own click.
/// Issue #301: a stacked panel's header bar opens it with a click anywhere on it, and its −/+
/// button at the bar's right end collapses and expands it.
#[test]
fn a_stacked_panels_whole_header_bar_toggles_it() {
let (mut h, _, _) = harness();
let open = |h: &Harness<'_, EffectcraftApp>| h.state().ui.dock.is_visible(PanelKind::Preview);
assert!(open(&h));
let bar = rect(&h, "panel.header.Preview");
let tab = rect(&h, "panel.tab.Preview");
assert!(bar.min.x >= tab.max.x - 1.0 && bar.width() > 40.0, "{bar:?} {tab:?}");
click_n(&mut h, pos2(bar.max.x - 6.0, bar.center().y), 1);
assert!(!open(&h), "a click at the bar's right end collapses Preview");
let bar = rect(&h, "panel.header.Preview");
click_n(&mut h, bar.center(), 1);
let toggle = rect(&h, "panel.header.Preview");
let bar = rect(&h, "panel.tab.Preview");
assert!(toggle.max.x <= bar.max.x + 1.0 && toggle.min.x > bar.center().x, "{toggle:?} {bar:?}");
click_n(&mut h, toggle.center(), 1);
assert!(!open(&h), "the button collapses Preview");
let toggle = rect(&h, "panel.header.Preview");
click_n(&mut h, toggle.center(), 1);
assert!(open(&h), "and expands it again");
let menu = rect(&h, "panel.menu.Preview");
click_n(&mut h, menu.center(), 1);
assert!(open(&h), "the panel menu button doesn't toggle the panel");
let toggle = rect(&h, "panel.header.Preview");
click_n(&mut h, toggle.center(), 1);
assert!(!open(&h));
click_n(&mut h, pos2(bar.min.x + bar.width() * 0.25, bar.center().y), 1);
assert!(open(&h), "a click on the bar away from the button opens it");
}
/// #302: a panel dropped onto a stacked panel becomes a tab in that panel's frame (it was
/// inserted as a stacked panel of its own below it); its tabs switch the frame's panel.
/// #302: a panel dropped onto a stacked panel joins the stack as a stacked panel of its own
/// below it (each stacked entry holds one panel), and opens and closes on its own.
#[test]
fn a_tab_dropped_on_a_stacked_panel_joins_its_frame() {
let (mut h, _, _) = harness();
let dock = |h: &Harness<'_, EffectcraftApp>| h.state().ui.dock.clone();
let stacked = |h: &Harness<'_, EffectcraftApp>| match &h.state().ui.dock.layout.root {
Some(Node::Split { second, .. }) => match second.as_ref() {
Node::Stack { entries } => entries.iter().map(|e| e.panel).collect::<Vec<_>>(),
other => panic!("{other:?}"),
},
other => panic!("{other:?}"),
};
let fx = rect(&h, "panel.tab.EffectControls");
let props = rect(&h, "panel.Properties");
drag(&mut h, fx.center(), props.center());
assert_eq!(group_of(&dock(&h), PanelKind::Properties), Some(vec![PanelKind::Properties, PanelKind::EffectControls]));
assert!(dock(&h).is_visible(PanelKind::EffectControls) && !dock(&h).is_visible(PanelKind::Properties));
// The frame's other tab shows Properties again, the frame stays open.
let tab = rect(&h, "panel.tab.Properties");
click_n(&mut h, tab.center(), 1);
assert!(dock(&h).is_visible(PanelKind::Properties) && !dock(&h).is_visible(PanelKind::EffectControls));
let order = stacked(&h);
let at = order.iter().position(|p| *p == PanelKind::Properties).unwrap();
assert_eq!(order.get(at + 1), Some(&PanelKind::EffectControls), "{order:?}");
assert!(dock(&h).is_visible(PanelKind::EffectControls) && dock(&h).is_visible(PanelKind::Properties));
// Its header button closes it alone.
let toggle = rect(&h, "panel.header.EffectControls");
click_n(&mut h, toggle.center(), 1);
assert!(!dock(&h).is_visible(PanelKind::EffectControls) && dock(&h).is_visible(PanelKind::Properties));
// The layout (saved workspaces) round-trips.
let s = serde_json::to_string(&dock(&h)).unwrap();
assert_eq!(serde_json::from_str::<Workspace>(&s).unwrap(), dock(&h));
+2
View File
@@ -147,6 +147,8 @@ fn type_tool_click_type_edit_and_commit() {
let (_, _, a, c) = edited(&h);
assert_eq!((a, c), (0, 10));
h.state_mut().show_panel(PanelKind::Character);
// Room for the whole Character panel in the stack.
h.state_mut().apply_dock(craft_ui::docking::Action::SetStackOpen { panel: PanelKind::Properties, open: false }).unwrap();
h.run_steps(3);
let bold = rect(&h, "character.fauxBold");
click(&mut h, bold.center(), Modifiers::NONE);
+19 -12
View File
@@ -11,6 +11,11 @@ use egui::{Event, Id, Key, Modifiers, vec2};
use egui_kittest::Harness;
use serde_json::json;
/// The egui id of panel `p`'s dock tab.
fn tab_id(p: PanelKind) -> Id {
craft_ui::docking::DockArea::<PanelKind>::hit_id(effectcraft_ui_egui::dock_ui::area_id(), &craft_ui::docking::Hit::Tab(p))
}
#[test]
fn named_and_current_workspaces_survive_frontend_restart() {
let store = Arc::new(MemoryConfig::default());
@@ -176,9 +181,9 @@ fn tab_traversal_activates_an_inactive_dock_tab_without_editing_the_project() {
h.run_steps(3);
assert!(h.state().auto.find("panel.tab.EffectControls").is_some(), "the inactive tab must be visible in this fixture");
let before = h.state().ui.dock.clone();
let target = Id::new(("tab", "aaa".to_string(), 1usize));
let target = tab_id(PanelKind::EffectControls);
let mut tabs = 0;
for _ in 0..80 {
for _ in 0..200 {
key(&mut h, Key::Tab, Modifiers::NONE);
tabs += 1;
if h.ctx.memory(|m| m.focused()) == Some(target) {
@@ -188,6 +193,15 @@ fn tab_traversal_activates_an_inactive_dock_tab_without_editing_the_project() {
assert_eq!(h.ctx.memory(|m| m.focused()), Some(target));
println!("Tab presses from no focus to Effect Controls: {tabs}");
assert_eq!(h.state().ui.dock, before, "traversal alone must not activate a tab");
// craft-ui outlines the keyboard-focused tab, inset 2 pt, in the dock's accent (EffectCraft's
// focus colour).
let response = h.ctx.read_response(target).unwrap();
assert!(
h.output().shapes.iter().any(|shape| {
matches!(&shape.shape, egui::Shape::Rect(r) if r.rect == response.rect.shrink(2.0) && r.stroke.color == h.state().tokens.focus && r.stroke.width > 0.0)
}),
"the dock tab must visibly show keyboard focus"
);
key(&mut h, Key::Space, Modifiers::NONE);
let mut activated = before;
activated.apply(craft_ui::docking::Action::Activate { panel: PanelKind::EffectControls }, dock::permissions).unwrap();
@@ -195,17 +209,10 @@ fn tab_traversal_activates_an_inactive_dock_tab_without_editing_the_project() {
assert_eq!(h.state().ui.focused, PanelKind::EffectControls);
assert!(!h.state().playback.playing);
assert!(Arc::ptr_eq(&h.state().session.project, &project));
let response = h.ctx.read_response(target).unwrap();
assert!(
h.output().shapes.iter().any(|shape| {
matches!(&shape.shape, egui::Shape::Rect(r) if r.rect == response.rect.shrink(1.0) && r.stroke.color == h.state().tokens.text && r.stroke.width > 0.0)
}),
"the active dock tab must visibly retain keyboard focus"
);
let inspected = effectcraft_ui_egui::control::inspect(h.state(), &h.ctx);
assert_eq!(inspected["keyboardFocus"]["automationId"], "panel.tab.EffectControls");
key(&mut h, Key::Tab, Modifiers::SHIFT);
assert_eq!(h.ctx.memory(|m| m.focused()), Some(Id::new(("tab", "aaa".to_string(), 0usize))));
assert_eq!(h.ctx.memory(|m| m.focused()), Some(tab_id(PanelKind::Project)));
key(&mut h, Key::Enter, Modifiers::NONE);
assert_eq!(h.state().ui.focused, PanelKind::Project);
assert!(!h.state().playback.playing);
@@ -284,8 +291,8 @@ fn control_root_divider_drag_after_toolbar_and_dock_tab_keyboard_activation() {
d.ok("ui.click", json!({"id":"tools.Selection"}), true);
// The toolbar can grow as upstream adds controls. Traverse to the actual response
// identity rather than assuming a fixed number of focus stops.
let target = Id::new(("tab", "aaa".to_string(), 1usize));
for tabs in 1..=100 {
let target = tab_id(PanelKind::EffectControls);
for tabs in 1..=200 {
d.ok("ui.key", json!({"key":"Tab"}), true);
assert!(d.h.state().ui.mini_flowchart.is_none(), "dock-tab traversal must not open a foreground popup");
if d.h.ctx.memory(|memory| memory.focused()) == Some(target) {