Files
deckcraft/crates/ui-egui/src/split_button_tests.rs
T
skylar taylor-barrick 14f40c408c Migrate DeckCraft to egui 0.37 (#1350)
* egui037-20261022: migrate DeckCraft to egui 0.37

Preserve current-main document and PDF behavior alongside the reviewed input, capture, font, ribbon and notice-packaging migration.

Validation: cargo xtask ci passed all eight stages, with 1,877 workspace tests and three doctests passed, six tests ignored, strict native lint and 23 WASM crates checked. Native GUI, platform and performance qualification remains separate.

* E21: restore Latin coverage and enforce required font inputs

* Fix migration review findings in font validation and UI controls

* M-egui037: document UI lifecycle and regression contracts
2026-10-25 10:48:52 -04:00

137 lines
6.2 KiB
Rust

//! Split controls on real ribbon callers: a dropdown-only control must not expose a dead named
//! half, while Group and Rotate keep a real main action beside their options arrow.
#![allow(clippy::unwrap_used, clippy::expect_used)]
use deckcraft_engine::Session;
use deckcraft_model::ShapeId;
use egui::accesskit::Role;
use egui_kittest::Harness;
use egui_kittest::kittest::Queryable;
use serde_json::json;
use crate::{Services, SlideApp};
/// Two rectangles selected on a blank slide, so all the arrange controls are enabled.
fn harness(tab: &str) -> (Harness<'static, SlideApp>, [ShapeId; 2]) {
let mut s = Session::with_new();
s.execute("slide.new", &json!({"layout": "blank"})).unwrap();
let mut ids = [ShapeId(0); 2];
for (i, x) in [100.0, 300.0].iter().enumerate() {
let r = s.execute("shape.insert", &json!({"preset": "rect", "rect": [x, 100.0, 80.0, 60.0]})).unwrap();
ids[i] = ShapeId(r["id"].as_u64().unwrap() as u32);
}
s.execute("selection.select", &json!({"ids": [ids[0].0, ids[1].0]})).unwrap();
let mut app = SlideApp::new(s, Services::default());
app.ui.tab = tab.into();
let mut h = Harness::builder().with_size(egui::vec2(1800.0, 900.0)).build_ui_state(
|ui, app: &mut SlideApp| {
app.logic(ui.ctx());
app.ui(ui);
},
app,
);
h.run_steps(3);
(h, ids)
}
/// Click the accessible button or chevron with the given name and run three harness steps.
fn click(h: &mut Harness<'static, SlideApp>, name: &str) {
h.get_by_role_and_label(Role::Button, name).click();
h.run_steps(3);
}
/// Click each named half, see the same menu, toggle it closed from that same half, then repeat on
/// the other half. The old callers wired only one half, so one full click produced no menu.
fn both_halves_open_same_menu(tab: &str, name: &str, item: &str) {
let (mut h, _) = harness(tab);
for half in [name.to_string(), format!("{name} options")] {
assert!(h.query_by_label(item).is_none(), "{tab}: the {half} menu starts closed");
click(&mut h, &half);
assert!(h.query_by_label(item).is_some(), "{tab}: clicking {half} opens the shared menu");
click(&mut h, &half);
assert!(h.query_by_label(item).is_none(), "{tab}: clicking {half} again closes the same menu");
}
}
/// Both the Home Layout label and chevron open and toggle the same layout menu.
#[test]
fn home_layout_opens_the_layout_menu_from_its_label_and_arrow() {
both_halves_open_same_menu("Home", "Layout", "Title Slide");
}
/// Keyboard entry must recognize either named half as the menu's origin. In particular, Up
/// enters at the last item rather than letting spatial focus navigation escape into the ribbon.
#[test]
fn home_section_menu_enters_wraps_and_closes_from_either_keyboard_trigger() {
let (mut h, _) = harness("Home");
for trigger in ["Section", "Section options"] {
for (entry, first, wrapped) in
[(egui::Key::ArrowUp, "Remove All Sections", "Add Section"), (egui::Key::ArrowDown, "Add Section", "Remove All Sections")]
{
h.get_by_role_and_label(Role::Button, trigger).focus();
h.run_steps(1);
assert!(h.get_by_role_and_label(Role::Button, trigger).is_focused(), "{trigger}: the named half has focus");
h.key_press(egui::Key::Enter);
h.run_steps(3);
assert!(h.query_by_role_and_label(Role::Button, "Add Section").is_some(), "{trigger}: Enter opens the menu");
h.key_press(entry);
h.run_steps(2);
assert!(h.get_by_role_and_label(Role::Button, first).is_focused(), "{trigger}: {entry:?} enters the menu");
// The opposite direction wraps at the edge, then the entry direction wraps back.
h.key_press(if entry == egui::Key::ArrowUp { egui::Key::ArrowDown } else { egui::Key::ArrowUp });
h.run_steps(2);
assert!(h.get_by_role_and_label(Role::Button, wrapped).is_focused(), "{trigger}: focus wraps inside the menu");
h.key_press(entry);
h.run_steps(2);
assert!(h.get_by_role_and_label(Role::Button, first).is_focused(), "{trigger}: focus wraps back");
h.key_press(egui::Key::Escape);
h.run_steps(3);
assert!(h.query_by_role_and_label(Role::Button, "Add Section").is_none(), "{trigger}: Escape closes the menu");
}
}
}
/// Main and chevron halves of Shape Effects open the same effects menu on Home and Shape
/// Format.
#[test]
fn shape_effects_opens_the_effects_menu_from_its_label_and_arrow_on_both_tabs() {
both_halves_open_same_menu("Home", "Shape Effects", "Format Shape…");
both_halves_open_same_menu("Shape Format", "Shape Effects", "Format Shape…");
}
/// Text Fill's label and chevron both open its colour menu.
#[test]
fn shape_format_text_fill_opens_its_colour_menu_from_its_label_and_arrow() {
both_halves_open_same_menu("Shape Format", "Text Fill", "More Colors… #");
}
/// The Group main area executes grouping while only its chevron opens the group menu.
#[test]
fn shape_format_group_main_groups_and_its_arrow_opens_the_group_menu() {
let (mut h, _) = harness("Shape Format");
click(&mut h, "Group");
{
let d = h.state().session.active().unwrap();
assert_eq!(d.selection.shapes.len(), 1, "the main half grouped the two selected shapes");
let grouped = d.shape(d.selection.shapes[0]).unwrap();
assert!(grouped.is_group(), "the result is a group, not a menu choice");
}
click(&mut h, "Group options");
assert!(h.query_by_label("Ungroup").is_some(), "the arrow opens the Group actions");
}
/// The Rotate main area rotates while its chevron opens the rotate menu.
#[test]
fn shape_format_rotate_main_rotates_and_its_arrow_opens_the_rotate_menu() {
let (mut h, ids) = harness("Shape Format");
h.state_mut().session.execute("selection.select", &json!({"ids": [ids[0].0]})).unwrap();
h.run_steps(2);
click(&mut h, "Rotate");
let rot = h.state().session.active().unwrap().shape(ids[0]).unwrap().xfrm.unwrap().rot;
assert!((rot - 90.0).abs() < 1e-9, "the main half rotates right: {rot}");
click(&mut h, "Rotate options");
assert!(h.query_by_label("Rotate Left 90°").is_some(), "the arrow opens the Rotate actions");
}