mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-08 22:28:09 +00:00
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:
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user