UI tests: gradient and rotation controls are found in release builds

Gradient and rotate-view harnesses looked up widgets by egui Id debug
source strings (for example ".with(…)" and "rot-combo"). Those names
exist only with debug assertions, so the same tests panic in --release.
Resolve widgets from stable scope salts / AccessKit labels instead, which
work in both profiles.
This commit is contained in:
Ahmed Gomaa
2026-11-09 02:03:06 +07:00
committed by bflatastic
parent afe8294c7d
commit 93a039d524
2 changed files with 41 additions and 29 deletions
+27 -22
View File
@@ -106,13 +106,14 @@ fn app(fill: Value) -> VectorcraftApp {
struct Panel {
ctx: egui::Context,
time: f64,
scope: egui::Id,
}
impl Panel {
fn new() -> Self {
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
Self { ctx, time: 0.0 }
Self { ctx, time: 0.0, scope: egui::Id::NULL }
}
/// One frame with `events` (and `modifiers` held); returns the texts drawn and where.
@@ -144,23 +145,27 @@ impl Panel {
screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(260.0, 600.0))),
..Default::default()
};
let mut out = self.ctx.run_ui(input, |ui| draw(app, ui));
let mut out = self.ctx.run_ui(input, |ui| {
self.scope = ui.scope_id();
draw(app, ui);
});
out.textures_delta.clear();
let mut v = vec![];
out.shapes.iter().for_each(|c| texts(&c.shape, &mut v));
v
}
/// The rect of the widget keyed `key` (last frame). egui 0.37's `WidgetRect.parent_id` is
/// the parent Ui's unstable `unique_id`, not the stable `scope_id` widgets are salted with;
/// debug builds remember the `with(key)` that made each id, so match that.
fn widget(&self, key: impl std::hash::Hash + std::fmt::Debug + Copy) -> Rect {
let suffix = format!(".with({key:?})");
self.ctx
.viewport(|vp| {
vp.prev_pass.widgets.layers().flat_map(|(_, w)| w.iter()).find(|w| format!("{:?}", w.id).ends_with(&suffix)).map(|w| w.rect)
})
.expect("widget drawn")
/// Match actual IDs in the panel or its immediate horizontal rows. `parent_id` is an
/// unstable auto ID, and the human-readable `Id` debug source is absent in release builds.
fn widget(&self, key: impl egui::AsIdSalt + Copy) -> Rect {
// Match Ui::new_child: its default child salt is an IdSalt, not the raw string.
let row_scope = self.scope.with(egui::IdSalt::new("child"));
let ids = [self.scope.with(key), row_scope.with(key)];
// Outside run_ui, read_response can prefer the older this_pass geometry after
// end_pass swaps the passes. Use the completed frame, including after Hide Options.
let matches: Vec<_> = self.ctx.viewport(|vp| ids.into_iter().filter_map(|id| vp.prev_pass.widgets.get(id).map(|w| w.rect)).collect());
assert_eq!(matches.len(), 1, "expected one widget for {key:?}");
matches[0]
}
fn click(&mut self, app: &mut VectorcraftApp, at: Pos2) -> Vec<(String, Pos2)> {
@@ -252,7 +257,7 @@ fn the_proxy_toggles_fill_and_stroke_and_stays_with_options_hidden() {
let fill = p.widget("fill-proxy");
p.click(&mut app, fill.left_top() + vec2(2.0, 2.0));
assert!(app.session.fill_active, "the proxy works with the options hidden");
p.widget(("grad-stop", 1));
p.widget(("grad-stop", 1_usize));
}
#[test]
@@ -331,7 +336,7 @@ fn a_selected_diamond_shows_and_sets_its_midpoint_in_location() {
let mut p = Panel::new();
let texts = p.frame(&mut app, vec![], Modifiers::NONE);
assert!(texts.iter().any(|(t, _)| t == "100%"), "the stop's location: {texts:?}");
let diamond = p.widget(("grad-mid", 0));
let diamond = p.widget(("grad-mid", 0_usize));
let texts = p.click(&mut app, diamond.center());
assert_eq!(app.session.selected_stop(), None, "the diamond replaces the stop selection");
assert!(texts.iter().any(|(t, _)| t == "30%"), "the midpoint: {texts:?}");
@@ -343,7 +348,7 @@ fn a_selected_diamond_shows_and_sets_its_midpoint_in_location() {
p.frame(&mut app, vec![key(egui::Key::Enter, Modifiers::NONE)], Modifiers::NONE);
assert!((fill(&app).gradient.stops[0].midpoint - 0.6).abs() < 1e-6, "{:?}", fill(&app).gradient.stops);
// Clicking a stop selects it again.
let stop = p.widget(("grad-stop", 0));
let stop = p.widget(("grad-stop", 0_usize));
let texts = p.click(&mut app, stop.center());
assert_eq!(app.session.selected_stop(), Some(0));
assert!(texts.iter().any(|(t, _)| t == "0%"));
@@ -474,7 +479,7 @@ fn alt_drag_copies_a_stop_and_alt_dropping_on_a_stop_swaps_colours() {
let mut app = app(json!({"gradient": {}}));
let mut p = Panel::new();
p.frame(&mut app, vec![], Modifiers::NONE);
let (a, b) = (p.widget(("grad-stop", 0)).center(), p.widget(("grad-stop", 1)).center());
let (a, b) = (p.widget(("grad-stop", 0_usize)).center(), p.widget(("grad-stop", 1_usize)).center());
let mid = pos2((a.x + b.x) / 2.0, a.y);
let offsets = |app: &VectorcraftApp| fill(app).gradient.stops.iter().map(|s| (s.offset * 10.0).round() / 10.0).collect::<Vec<_>>();
// A plain drag moves the stop.
@@ -499,7 +504,7 @@ fn a_dropped_swatch_adds_or_recolours_a_stop() {
let mut app = app(json!({"gradient": {}}));
let mut p = Panel::new();
p.frame(&mut app, vec![], Modifiers::NONE);
let (a, b) = (p.widget(("grad-stop", 0)).center(), p.widget(("grad-stop", 1)).center());
let (a, b) = (p.widget(("grad-stop", 0_usize)).center(), p.widget(("grad-stop", 1_usize)).center());
let drop = |p: &mut Panel, app: &mut VectorcraftApp, at: Pos2| {
egui::DragAndDrop::set_payload(&p.ctx, PanelDrag::paint(Paint::solid(Color::rgb(1.0, 0.0, 0.0))));
p.frame(app, vec![Event::PointerMoved(at)], Modifiers::NONE);
@@ -509,7 +514,7 @@ fn a_dropped_swatch_adds_or_recolours_a_stop() {
drop(&mut p, &mut app, pos2((a.x + b.x) / 2.0, a.y - 20.0));
assert_eq!(hexes(&app), ["#ffffff", "#ff0000", "#000000"]);
p.frame(&mut app, vec![], Modifiers::NONE);
let last = p.widget(("grad-stop", 2)).center();
let last = p.widget(("grad-stop", 2_usize)).center();
drop(&mut p, &mut app, last);
assert_eq!(hexes(&app), ["#ffffff", "#ff0000", "#ff0000"]);
// A spot swatch tile dropped on a stop links it: editing the swatch recolours the stop.
@@ -538,7 +543,7 @@ fn a_color_dropped_on_the_ramp_keeps_its_model_in_a_cmyk_document() {
app.run("paint.setFill", json!({"gradient": {"stops": rgb}, "keepModel": true})).unwrap();
let mut p = Panel::new();
p.frame(&mut app, vec![], Modifiers::NONE);
let (a, b) = (p.widget(("grad-stop", 0)).center(), p.widget(("grad-stop", 1)).center());
let (a, b) = (p.widget(("grad-stop", 0_usize)).center(), p.widget(("grad-stop", 1_usize)).center());
let at = pos2((a.x + b.x) / 2.0, a.y - 20.0);
let red = Color::rgb(1.0, 0.0, 0.0);
egui::DragAndDrop::set_payload(&p.ctx, PanelDrag::paint(Paint::solid(red)));
@@ -554,7 +559,7 @@ fn double_clicking_a_stop_opens_the_shared_popover_beside_it() {
let mut app = app(json!({"gradient": {}}));
let mut p = Panel::new();
p.frame(&mut app, vec![], Modifiers::NONE);
let stop = p.widget(("grad-stop", 1));
let stop = p.widget(("grad-stop", 1_usize));
let b = |pressed| Event::PointerButton { pos: stop.center(), button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE };
p.frame(&mut app, vec![Event::PointerMoved(stop.center()), b(true), b(false), b(true), b(false)], Modifiers::NONE);
let d = app.ui.dialog.clone().expect("the popover opened");
@@ -755,7 +760,7 @@ fn a_stop_dragged_off_the_ramp_or_deleted_with_the_button_beside_it_goes() {
let mut app = app(json!({"gradient": {"stops": stops}}));
let mut p = Panel::new();
p.frame(&mut app, vec![], Modifiers::NONE);
let mid = p.widget(("grad-stop", 1)).center();
let mid = p.widget(("grad-stop", 1_usize)).center();
// Dragged down off the ramp, the middle stop goes.
p.drag(&mut app, mid, mid + vec2(0.0, 60.0), Modifiers::NONE);
assert_eq!(hexes(&app), ["#ffffff", "#000000"]);
@@ -773,7 +778,7 @@ fn a_stop_dragged_off_the_ramp_or_deleted_with_the_button_beside_it_goes() {
assert_eq!(hexes(&app), ["#ffffff", "#ff0000", "#000000"]);
app.run("edit.redo", json!({})).unwrap();
// Two stops stay two: dragged off, the stop comes back, and the button is off.
let first = p.widget(("grad-stop", 0)).center();
let first = p.widget(("grad-stop", 0_usize)).center();
p.drag(&mut app, first, first + vec2(0.0, 60.0), Modifiers::NONE);
assert_eq!(hexes(&app), ["#ffffff", "#000000"]);
app.run("gradient.selectStop", json!({"index": 0})).unwrap();
+14 -7
View File
@@ -227,7 +227,7 @@ impl Window {
}
/// One headless frame of the whole window, `seconds` after the last one, with `events`.
fn after(&mut self, seconds: f64, events: Vec<egui::Event>) {
fn after(&mut self, seconds: f64, events: Vec<egui::Event>) -> egui::FullOutput {
self.time += seconds;
let rect = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(800.0, 600.0));
let raw = egui::RawInput { screen_rect: Some(rect), time: Some(self.time), events, ..Default::default() };
@@ -237,6 +237,7 @@ impl Window {
app.ui(ui);
});
out.textures_delta.clear();
out
}
/// A press and release of Escape, `seconds` after the last frame.
@@ -336,12 +337,18 @@ fn escape_after_ending_isolation_or_with_the_rotate_view_tool_resets_the_view()
#[test]
fn escape_after_closing_a_popup_or_during_a_ruler_origin_drag_keeps_the_rotation() {
let mut w = Window::new();
// The menu's button has the id `ComboBox::from_id_salt("rot-combo")` gives it in the status bar's
// row, which hosts other widgets too. egui 0.37 salts the Ui's stable `scope_id`, not the
// widget rect's unstable `parent_id`; in debug builds the id's debug source names its salt.
let widgets: Vec<egui::WidgetRect> = w.ctx.viewport(|vp| vp.prev_pass.widgets.layers().flat_map(|(_, ws)| ws.iter().copied()).collect());
let combo = widgets.iter().find(|r| format!("{:?}", r.id).contains("rot-combo")).copied();
let combo = combo.expect("the status bar's angle menu");
// Find the actual combo by its accessible name, which is present in both build profiles;
// `Id`'s debug source (including "rot-combo") only exists with debug assertions enabled.
w.ctx.enable_accesskit();
let out = w.after(0.0, vec![]);
let tree = out.platform_output.accesskit_update.expect("the window's accessibility tree");
let combos: Vec<_> =
tree.nodes.iter().filter(|(_, node)| node.role() == egui::accesskit::Role::ComboBox && node.label() == Some("Rotation")).collect();
assert_eq!(combos.len(), 1, "the status bar's angle menu");
let combo = w
.ctx
.viewport(|vp| vp.prev_pass.widgets.layers().flat_map(|(_, ws)| ws.iter()).find(|r| r.id.accesskit_id() == combos[0].0).copied())
.expect("the angle menu's widget");
w.click(0.0, egui::PointerButton::Primary, combo.rect.center());
w.after(0.0, vec![]);
assert!(craft_ui::ComboBox::is_open(&w.ctx, combo.id), "the angle menu is open");