From 3eae1747aebf0c56a7ecf7502dcb18ec1471d894 Mon Sep 17 00:00:00 2001 From: aiongg Date: Sun, 25 Oct 2026 05:41:47 +0000 Subject: [PATCH] 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. --- crates/ui-egui/tests/it/shared_docking_ui.rs | 12 ++- crates/ui-egui/tests/it/ui_agent_audit.rs | 2 + crates/ui-egui/tests/it/ui_dock_resize.rs | 5 +- crates/ui-egui/tests/it/ui_fx_editors.rs | 2 + crates/ui-egui/tests/it/ui_menus.rs | 5 +- crates/ui-egui/tests/it/ui_panel_focus.rs | 6 +- crates/ui-egui/tests/it/ui_scrolling.rs | 6 +- crates/ui-egui/tests/it/ui_tabs.rs | 89 +++++++++++-------- crates/ui-egui/tests/it/ui_text_edit.rs | 2 + crates/ui-egui/tests/it/ui_workspace_focus.rs | 31 ++++--- 10 files changed, 97 insertions(+), 63 deletions(-) diff --git a/crates/ui-egui/tests/it/shared_docking_ui.rs b/crates/ui-egui/tests/it/shared_docking_ui.rs index 552939ad..c71fadf3 100644 --- a/crates/ui-egui/tests/it/shared_docking_ui.rs +++ b/crates/ui-egui/tests/it/shared_docking_ui.rs @@ -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)); } diff --git a/crates/ui-egui/tests/it/ui_agent_audit.rs b/crates/ui-egui/tests/it/ui_agent_audit.rs index a14f7783..17b40b9b 100644 --- a/crates/ui-egui/tests/it/ui_agent_audit.rs +++ b/crates/ui-egui/tests/it/ui_agent_audit.rs @@ -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 { + // 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); diff --git a/crates/ui-egui/tests/it/ui_dock_resize.rs b/crates/ui-egui/tests/it/ui_dock_resize.rs index e58fc1af..863fe697 100644 --- a/crates/ui-egui/tests/it/ui_dock_resize.rs +++ b/crates/ui-egui/tests/it/ui_dock_resize.rs @@ -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::::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)); diff --git a/crates/ui-egui/tests/it/ui_fx_editors.rs b/crates/ui-egui/tests/it/ui_fx_editors.rs index b9530fdf..436bbc2a 100644 --- a/crates/ui-egui/tests/it/ui_fx_editors.rs +++ b/crates/ui-egui/tests/it/ui_fx_editors.rs @@ -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"); diff --git a/crates/ui-egui/tests/it/ui_menus.rs b/crates/ui-egui/tests/it/ui_menus.rs index a59599df..82c81f38 100644 --- a/crates/ui-egui/tests/it/ui_menus.rs +++ b/crates/ui-egui/tests/it/ui_menus.rs @@ -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"); } diff --git a/crates/ui-egui/tests/it/ui_panel_focus.rs b/crates/ui-egui/tests/it/ui_panel_focus.rs index 6a185058..0d436f70 100644 --- a/crates/ui-egui/tests/it/ui_panel_focus.rs +++ b/crates/ui-egui/tests/it/ui_panel_focus.rs @@ -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)); } diff --git a/crates/ui-egui/tests/it/ui_scrolling.rs b/crates/ui-egui/tests/it/ui_scrolling.rs index 66534ebd..b97adb84 100644 --- a/crates/ui-egui/tests/it/ui_scrolling.rs +++ b/crates/ui-egui/tests/it/ui_scrolling.rs @@ -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}"); } } diff --git a/crates/ui-egui/tests/it/ui_tabs.rs b/crates/ui-egui/tests/it/ui_tabs.rs index 2b0edb3f..5f3f6d4a 100644 --- a/crates/ui-egui/tests/it/ui_tabs.rs +++ b/crates/ui-egui/tests/it/ui_tabs.rs @@ -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::>(), + 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::(&s).unwrap(), dock(&h)); diff --git a/crates/ui-egui/tests/it/ui_text_edit.rs b/crates/ui-egui/tests/it/ui_text_edit.rs index ed5baca2..4b7a13ac 100644 --- a/crates/ui-egui/tests/it/ui_text_edit.rs +++ b/crates/ui-egui/tests/it/ui_text_edit.rs @@ -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); diff --git a/crates/ui-egui/tests/it/ui_workspace_focus.rs b/crates/ui-egui/tests/it/ui_workspace_focus.rs index 8874c987..7069b9aa 100644 --- a/crates/ui-egui/tests/it/ui_workspace_focus.rs +++ b/crates/ui-egui/tests/it/ui_workspace_focus.rs @@ -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::::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) {