diff --git a/crates/ui-egui/src/dialogs_assembly_tests.rs b/crates/ui-egui/src/dialogs_assembly_tests.rs index 9dbd7d42..a9a76b61 100644 --- a/crates/ui-egui/src/dialogs_assembly_tests.rs +++ b/crates/ui-egui/src/dialogs_assembly_tests.rs @@ -191,7 +191,10 @@ fn ok_click_survives_a_busy_preview() { let frame = |app: &mut SolveApp, busy: bool, events: Vec| { app.preview.busy = busy; let input = egui::RawInput { screen_rect: Some(screen), events, ..Default::default() }; - ctx.run_ui(input, |ui| crate::dialogs::show(app, ui.ctx())) + let mut out = ctx.run_ui(input, |ui| crate::dialogs::show(app, ui.ctx())); + // Nothing is painted: drop the texture changes (an unapplied delta panics on drop). + out.textures_delta.clear(); + out }; frame(&mut app, false, vec![]); let out = frame(&mut app, false, vec![]); diff --git a/crates/ui-egui/src/palette.rs b/crates/ui-egui/src/palette.rs index bfb0449f..681063ef 100644 --- a/crates/ui-egui/src/palette.rs +++ b/crates/ui-egui/src/palette.rs @@ -103,9 +103,11 @@ pub fn popup(app: &mut SolveApp, ctx: &egui::Context) { .open(&mut open) .show(ctx, |ui| { let r = ui.add(egui::TextEdit::singleline(&mut app.palette.text).hint_text("command name").desired_width(380.0)); + // Read Enter before taking the focus back (lost_focus reads the focus as it is now). + let entered = r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)); r.request_focus(); let list = matches(&app.palette.text); - if r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) { + if entered { picked = list.first().map(|x| x.0); } egui::ScrollArea::vertical().max_height(280.0).show(ui, |ui| { diff --git a/crates/ui-egui/src/scenario.rs b/crates/ui-egui/src/scenario.rs index b2a6609c..97945295 100644 --- a/crates/ui-egui/src/scenario.rs +++ b/crates/ui-egui/src/scenario.rs @@ -4,19 +4,21 @@ //! //! A scenario is a JSON list of steps: //! - `{"start": "Extrude"}`: a toolbar click; `{"key": "E", "shift"?, "cmd"?}`; `{"text": "20"}` -//! - `{"widget": "Revolute"}`: click a dialog field, choice or button by its text +//! - `{"widget": "Revolute", "near"?: "Type"}`: click a dialog field, choice or button by its +//! text (with `near`: the one in that label's row) //! - `{"click": AT, "double"?, "shift"?, "ctrl"?, "button"?}`, `{"move": AT}`, //! `{"drag": [AT, AT], "shift"?, "steps"?}` where AT is `[x, y]` (screen), `{"world": [x,y,z]}` //! or `{"sketch": [x,y]}` (the sketch being edited) //! - `{"call": "ui.view", "params": {…}}`: any other control request (`fail: true` expects an //! error) +//! - `{"make_image": "pic.png"}`: a picture in the scenario's folder //! - `{"choose_file": "part.step"}`: the next Open picker returns that file of the scenario's //! folder (Save pickers save there) //! - `{"shot": "name"}`: a screenshot when run in a window; headless, a render of the model //! into `$SOLVECRAFT_SCENARIO_SHOTS` when that is set; `{"debug": 1}` //! prints the UI state //! - AT may also be `{"plane": "XY"}`: the middle of an origin plane's square -//! - `{"expect": {…}}`: checks, see [`check`] +//! - `{"expect": {…}}`: checks, see [`check`]; `{"until": {…}}` waits (frames) until they pass //! //! The same files drive the live app over the control channel for screenshots. @@ -60,6 +62,15 @@ impl Default for Harness { } } +impl Drop for Harness { + /// The scenario's files go with it (kept with SOLVECRAFT_SCENARIO_KEEP set). + fn drop(&mut self) { + if std::env::var_os("SOLVECRAFT_SCENARIO_KEEP").is_none() { + let _ = std::fs::remove_dir_all(&self.dir); + } + } +} + impl Harness { pub fn new() -> Harness { let mut h = Harness { @@ -134,7 +145,21 @@ impl Harness { /// The centre of the visible widget whose label or value is `text` (the last one drawn wins: /// popups are drawn after what opened them). pub fn widget(&self, text: &str) -> Option<[f64; 2]> { - let tree = self.output.accesskit_update.as_ref()?; + self.widgets(text).into_iter().next() + } + + /// The widget showing `text` nearest the label `near` (the field in a label's row). + pub fn widget_near(&self, text: &str, near: &str) -> Option<[f64; 2]> { + let [lx, ly] = self.widgets(near).into_iter().next()?; + self.widgets(text).into_iter().filter(|[x, _]| *x > lx).min_by(|a, b| { + let d = |p: &[f64; 2]| (p[1] - ly).abs() * 4.0 + (p[0] - lx).abs(); + d(a).total_cmp(&d(b)) + }) + } + + /// Centres of the visible widgets whose label or value is `text`, last drawn first. + fn widgets(&self, text: &str) -> Vec<[f64; 2]> { + let Some(tree) = self.output.accesskit_update.as_ref() else { return Vec::new() }; tree.nodes .iter() .rev() @@ -143,8 +168,9 @@ impl Harness { n.label().is_some_and(|l| l == text || l.strip_prefix(text).is_some_and(|r| r.starts_with(' '))) || n.value() == Some(text) }) .filter_map(|(_, n)| n.bounds()) - .find(|b| b.x1 > b.x0 && b.y1 > b.y0) + .filter(|b| b.x1 > b.x0 && b.y1 > b.y0) .map(|b| [(b.x0 + b.x1) / 2.0, (b.y0 + b.y1) / 2.0]) + .collect() } /// A step's point on screen. @@ -165,73 +191,100 @@ impl Harness { /// Run one step. pub fn step(&mut self, s: &Value) -> Result<(), String> { let flag = |k: &str| s.get(k).and_then(Value::as_bool).unwrap_or(false); - let res = if let Some(id) = s.get("start").and_then(Value::as_str) { - self.call("ui.start", json!({ "command": id })) - } else if let Some(k) = s.get("key").and_then(Value::as_str) { - self.call("ui.key", json!({"key": k, "shift": flag("shift"), "cmd": flag("cmd")})) - } else if let Some(t) = s.get("text").and_then(Value::as_str) { - self.call("ui.text", json!({ "text": t })) - } else if let Some(at) = s.get("click") { - let [x, y] = self.at(at)?; - // Hover first, as a hand does. - self.call("ui.move", json!({"x": x, "y": y})); - let mut p = json!({"x": x, "y": y, "double": flag("double"), "shift": flag("shift"), "ctrl": flag("ctrl")}); - if let Some(b) = s.get("button") { - p["button"] = b.clone(); - } - self.call("ui.click", p) - } else if let Some(text) = s.get("widget").and_then(Value::as_str) { - // A dialog field, choice or button, found by its text. - let [x, y] = self.widget(text).ok_or_else(|| format!("no widget `{text}` on screen"))?; - self.call("ui.move", json!({"x": x, "y": y})); - self.call("ui.click", json!({"x": x, "y": y})) - } else if let Some(at) = s.get("move") { - let [x, y] = self.at(at)?; - self.call("ui.move", json!({"x": x, "y": y})) - } else if let Some(d) = s.get("drag") { - let [x0, y0] = self.at(d.get(0).unwrap_or(&Value::Null))?; - let [x1, y1] = self.at(d.get(1).unwrap_or(&Value::Null))?; - let steps = s.get("steps").cloned().unwrap_or(json!(8)); - self.call("ui.drag", json!({"x0": x0, "y0": y0, "x1": x1, "y1": y1, "steps": steps, "shift": flag("shift")})) - } else if let Some(m) = s.get("call").and_then(Value::as_str) { - self.call(m, s.get("params").cloned().unwrap_or(json!({}))) - } else if let Some(name) = s.get("shot").and_then(Value::as_str) { - // With SOLVECRAFT_SCENARIO_SHOTS set: a render of the model as the camera sees it. - if let Ok(dir) = std::env::var("SOLVECRAFT_SCENARIO_SHOTS") { - let path = std::path::Path::new(&dir).join(format!("{name}.png")); - self.call("ui.render", json!({"path": path.to_string_lossy(), "width": 1000, "height": 700})); - } - return Ok(()); - } else if let Some(f) = s.get("choose_file").and_then(Value::as_str) { - *self.next_file.borrow_mut() = Some(self.dir.join(f).to_string_lossy().into_owned()); - return Ok(()); - } else if let Some(m) = s.get("print").and_then(Value::as_str) { - let r = self.call(m, s.get("params").cloned().unwrap_or(json!({}))); - eprintln!("print {m}: {r}"); - return Ok(()); - } else if s.get("debug").is_some() { - let ui = self.call("ui.inspect", json!({})); - let sel = self.call("ui.selection", json!({})); - eprintln!( - "debug: dialog {} tool {} hover {}\nselection {}\nstatus {:?}", - ui["result"]["dialog"], ui["result"]["tool"], ui["result"]["hover"], sel["result"]["selection"], self.app.status - ); - if s["debug"] == json!("widgets") { - let texts: Vec = self - .output - .accesskit_update - .iter() - .flat_map(|t| t.nodes.iter()) - .filter_map(|(_, n)| n.label().or(n.value()).map(|l| format!("{:?} {l}", n.role()))) - .collect(); - eprintln!("widgets: {texts:?}"); - } - return Ok(()); - } else if let Some(e) = s.get("expect") { - return check(self, e); - } else { - return Err(format!("unknown step {s}")); - }; + let res = + if let Some(id) = s.get("start").and_then(Value::as_str) { + self.call("ui.start", json!({ "command": id })) + } else if let Some(k) = s.get("key").and_then(Value::as_str) { + self.call("ui.key", json!({"key": k, "shift": flag("shift"), "cmd": flag("cmd")})) + } else if let Some(t) = s.get("text").and_then(Value::as_str) { + self.call("ui.text", json!({ "text": t })) + } else if let Some(at) = s.get("click") { + let [x, y] = self.at(at)?; + // Hover first, as a hand does. + self.call("ui.move", json!({"x": x, "y": y})); + let mut p = json!({"x": x, "y": y, "double": flag("double"), "shift": flag("shift"), "ctrl": flag("ctrl")}); + if let Some(b) = s.get("button") { + p["button"] = b.clone(); + } + self.call("ui.click", p) + } else if let Some(text) = s.get("widget").and_then(Value::as_str) { + // A dialog field, choice or button, found by its text. + let found = match s.get("near").and_then(Value::as_str) { + Some(near) => self.widget_near(text, near), + None => self.widget(text), + }; + let [x, y] = found.ok_or_else(|| format!("no widget `{text}` on screen"))?; + self.call("ui.move", json!({"x": x, "y": y})); + self.call("ui.click", json!({"x": x, "y": y})) + } else if let Some(at) = s.get("move") { + let [x, y] = self.at(at)?; + self.call("ui.move", json!({"x": x, "y": y})) + } else if let Some(d) = s.get("drag") { + let [x0, y0] = self.at(d.get(0).unwrap_or(&Value::Null))?; + let [x1, y1] = self.at(d.get(1).unwrap_or(&Value::Null))?; + let steps = s.get("steps").cloned().unwrap_or(json!(8)); + self.call("ui.drag", json!({"x0": x0, "y0": y0, "x1": x1, "y1": y1, "steps": steps, "shift": flag("shift")})) + } else if let Some(m) = s.get("call").and_then(Value::as_str) { + self.call(m, s.get("params").cloned().unwrap_or(json!({}))) + } else if let Some(name) = s.get("shot").and_then(Value::as_str) { + // With SOLVECRAFT_SCENARIO_SHOTS set: a render of the model as the camera sees it. + if let Ok(dir) = std::env::var("SOLVECRAFT_SCENARIO_SHOTS") { + let path = std::path::Path::new(&dir).join(format!("{name}.png")); + self.call("ui.render", json!({"path": path.to_string_lossy(), "width": 1000, "height": 700})); + } + return Ok(()); + } else if let Some(f) = s.get("make_image").and_then(Value::as_str) { + // A picture to insert (a 200 x 100 checkerboard PNG in the scenario's folder). + let img = image::RgbImage::from_fn(200, 100, |x, y| { + if (x / 20 + y / 20) % 2 == 0 { image::Rgb([230, 230, 230]) } else { image::Rgb([60, 90, 160]) } + }); + img.save(self.dir.join(f)).map_err(|e| format!("make_image: {e}"))?; + return Ok(()); + } else if let Some(f) = s.get("choose_file").and_then(Value::as_str) { + *self.next_file.borrow_mut() = Some(self.dir.join(f).to_string_lossy().into_owned()); + return Ok(()); + } else if let Some(m) = s.get("print").and_then(Value::as_str) { + let r = self.call(m, s.get("params").cloned().unwrap_or(json!({}))); + eprintln!("print {m}: {r}"); + return Ok(()); + } else if s.get("debug").is_some() { + let ui = self.call("ui.inspect", json!({})); + let sel = self.call("ui.selection", json!({})); + eprintln!( + "debug: dialog {} tool {} hover {}\nselection {}\nstatus {:?}", + ui["result"]["dialog"], ui["result"]["tool"], ui["result"]["hover"], sel["result"]["selection"], self.app.status + ); + if s["debug"] == json!("widgets") { + let texts: Vec = self + .output + .accesskit_update + .iter() + .flat_map(|t| t.nodes.iter()) + .filter_map(|(_, n)| { + let at = n.bounds().map(|b| format!(" @{:.0},{:.0}", (b.x0 + b.x1) / 2.0, (b.y0 + b.y1) / 2.0)).unwrap_or_default(); + n.label().or(n.value()).map(|l| format!("{:?} {l}{at}", n.role())) + }) + .collect(); + eprintln!("widgets: {texts:?}"); + } + return Ok(()); + } else if let Some(e) = s.get("until") { + // Background work (a sample being built, a preview): frames until the checks pass. + let mut last = String::new(); + for _ in 0..600 { + match check(self, e) { + Ok(()) => return Ok(()), + Err(m) => last = m, + } + std::thread::sleep(std::time::Duration::from_millis(20)); + self.frames(2); + } + return Err(format!("timed out: {last}")); + } else if let Some(e) = s.get("expect") { + return check(self, e); + } else { + return Err(format!("unknown step {s}")); + }; let failed = res["ok"] != json!(true); if failed != flag("fail") { return Err(format!("{s}: {res}")); @@ -348,6 +401,48 @@ pub fn check(h: &mut Harness, e: &Value) -> Result<(), String> { return Err(format!("no file {}", p.display())); } } + "file_contains" => { + // {file, text}: a saved text file holds the text. + let p = h.dir.join(v["file"].as_str().unwrap_or_default()); + let body = std::fs::read_to_string(&p).unwrap_or_default(); + let t = v["text"].as_str().unwrap_or_default(); + if !body.contains(t) { + return Err(format!("{} does not contain `{t}`", p.display())); + } + } + "hidden" => { + // Bodies hidden in the view. + let h = &ui["ui"]["hidden_bodies"]; + let h = if h.is_null() { &ui["ui"]["hiddenBodies"] } else { h }; + if h != v { + return Err(format!("hidden bodies: got {h}, want {v}")); + } + } + "canvas_width" => { + // The first canvas's width (mm). + let w = h.app.session.doc.canvases.first().map(|c| c.width).unwrap_or(f64::NAN); + approx(w, v, "canvas width")?; + } + "section" => { + if h.app.session.section.is_some() != v.as_bool().unwrap_or(false) { + return Err(format!("section view: want {v}")); + } + } + "home" => { + if ui["home"] != *v { + return Err(format!("start page open: got {}, want {v}", ui["home"])); + } + } + "documents" => { + if ui["documents"] != *v { + return Err(format!("open designs: got {}, want {v}", ui["documents"])); + } + } + "name" => { + if doc["name"] != *v { + return Err(format!("design name: got {}, want {v}", doc["name"])); + } + } "query" => { // {command, params?, path: [keys or indices], value}: a command's result at path. let cmd = v["command"].as_str().ok_or("query needs `command`")?; diff --git a/crates/ui-egui/src/sketch_palette.rs b/crates/ui-egui/src/sketch_palette.rs index 9622e242..a96a9bc2 100644 --- a/crates/ui-egui/src/sketch_palette.rs +++ b/crates/ui-egui/src/sketch_palette.rs @@ -167,9 +167,9 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) { } ui.end_row(); for (k, label) in TOGGLES { - ui.label(label); + // The label is part of the checkbox: clicking it toggles too. let mut on = shown(k); - if ui.checkbox(&mut on, "").changed() { + if ui.checkbox(&mut on, label).changed() { set = Some((k.to_string(), on)); } ui.end_row(); diff --git a/crates/ui-egui/tests/scenarios.rs b/crates/ui-egui/tests/scenarios.rs index 18b90c65..3b497273 100644 --- a/crates/ui-egui/tests/scenarios.rs +++ b/crates/ui-egui/tests/scenarios.rs @@ -34,5 +34,13 @@ scenarios!( s09_step_roundtrip_hole, s10_parameters_drive_model, s11_sketch_constraints, - s12_pattern_bodies + s12_pattern_bodies, + s13_plastic_enclosure, + s14_appearance_export, + s15_start_page_sample_save_reopen, + s16_tabs_separate_undo, + s17_toolbox_and_visibility, + s18_3d_sketch_pipe, + s19_canvas_calibrate, + s20_section_with_joint_drive ); diff --git a/crates/ui-egui/tests/scenarios/s13_plastic_enclosure.json b/crates/ui-egui/tests/scenarios/s13_plastic_enclosure.json new file mode 100644 index 00000000..cded06cf --- /dev/null +++ b/crates/ui-egui/tests/scenarios/s13_plastic_enclosure.json @@ -0,0 +1,187 @@ +[ + { + "call": "ui.set", + "params": { + "tab": "PLASTIC" + } + }, + { + "start": "SketchCreate" + }, + { + "click": { + "plane": "XY" + } + }, + { + "start": "ShapeRectangleTwoPoint" + }, + { + "click": { + "sketch": [ + 0, + 0 + ] + } + }, + { + "click": { + "sketch": [ + 60, + 40 + ] + } + }, + { + "key": "Escape" + }, + { + "start": "SketchStop" + }, + { + "call": "ui.view", + "params": { + "view": "iso" + } + }, + { + "start": "Extrude" + }, + { + "text": "30" + }, + { + "key": "Enter" + }, + { + "expect": { + "bodies": 1, + "volume": 72000 + } + }, + { + "start": "FusionShellBodyCommand" + }, + { + "click": { + "world": [ + 30, + 20, + 30 + ] + } + }, + { + "text": "2" + }, + { + "key": "Enter" + }, + { + "expect": { + "bodies": 1, + "errors": 0, + "dialog": null, + "volume": [ + 15552, + 0.0001 + ] + } + }, + { + "shot": "s13_shell" + }, + { + "call": "ui.view", + "params": { + "view": "top" + } + }, + { + "start": "FusionBossCommand" + }, + { + "click": { + "world": [ + 30, + 28, + 2 + ] + } + }, + { + "key": "Enter" + }, + { + "expect": { + "errors": 0, + "dialog": null, + "volume": [ + 15764.04, + 0.0001 + ] + } + }, + { + "shot": "s13_boss" + }, + { + "start": "FusionLipCommand" + }, + { + "click": { + "world": [ + 1, + 20, + 30 + ] + } + }, + { + "key": "Enter" + }, + { + "expect": { + "errors": 0, + "dialog": null, + "volume": [ + 16334.04, + 0.0001 + ] + } + }, + { + "shot": "s13_lip" + }, + { + "call": "ui.view", + "params": { + "view": "iso" + } + }, + { + "start": "FusionSnapFitCommand" + }, + { + "click": { + "world": [ + 58, + 20, + 15 + ] + } + }, + { + "key": "Enter" + }, + { + "expect": { + "errors": 0, + "dialog": null, + "bodies": 1 + } + }, + { + "shot": "s13_snapfit" + } +] \ No newline at end of file diff --git a/crates/ui-egui/tests/scenarios/s14_appearance_export.json b/crates/ui-egui/tests/scenarios/s14_appearance_export.json new file mode 100644 index 00000000..12faab2c --- /dev/null +++ b/crates/ui-egui/tests/scenarios/s14_appearance_export.json @@ -0,0 +1,111 @@ +[ + { + "start": "SketchCreate" + }, + { + "click": { + "plane": "XY" + } + }, + { + "start": "ShapeRectangleTwoPoint" + }, + { + "click": { + "sketch": [ + 0, + 0 + ] + } + }, + { + "click": { + "sketch": [ + 30, + 20 + ] + } + }, + { + "key": "Escape" + }, + { + "start": "SketchStop" + }, + { + "call": "ui.view", + "params": { + "view": "iso" + } + }, + { + "start": "Extrude" + }, + { + "text": "10" + }, + { + "key": "Enter" + }, + { + "click": { + "world": [ + 15, + 10, + 10 + ] + } + }, + { + "key": "A" + }, + { + "expect": { + "dialog": "Appearance" + } + }, + { + "shot": "s14_appearance" + }, + { + "widget": "OK" + }, + { + "expect": { + "dialog": null, + "errors": 0 + } + }, + { + "click": [ + 127, + 16 + ] + }, + { + "widget": "Export STEP\u2026" + }, + { + "expect": { + "file": "Untitled.step", + "file_contains": { + "file": "Untitled.step", + "text": "COLOUR_RGB('',0.7686" + } + } + }, + { + "click": [ + 127, + 16 + ] + }, + { + "widget": "Export 3MF\u2026" + }, + { + "expect": { + "file": "Untitled.3mf" + } + } +] \ No newline at end of file diff --git a/crates/ui-egui/tests/scenarios/s15_start_page_sample_save_reopen.json b/crates/ui-egui/tests/scenarios/s15_start_page_sample_save_reopen.json new file mode 100644 index 00000000..1b0c52cf --- /dev/null +++ b/crates/ui-egui/tests/scenarios/s15_start_page_sample_save_reopen.json @@ -0,0 +1,24 @@ +[ + {"click": [248, 17]}, + {"expect": {"home": true}}, + {"call": "ui.home", "params": {"sample": 0}}, + {"until": {"home": false}}, + {"expect": {"errors": 0, "bodies": 1, "name": "Sample Plate", "param": {"width": 80}}}, + {"shot": "s15_sample"}, + {"start": "ChangeParameterCommand"}, + {"widget": "8 mm"}, + {"key": "A", "cmd": true}, + {"text": "10 mm"}, + {"key": "Enter"}, + {"widget": "Close window"}, + {"expect": {"param": {"thickness": 10}, "errors": 0}}, + {"key": "S", "cmd": true}, + {"expect": {"file": "Sample Plate.solvecraft"}}, + {"key": "N", "cmd": true}, + {"expect": {"bodies": 0}}, + {"choose_file": "Sample Plate.solvecraft"}, + {"key": "O", "cmd": true}, + {"debug": 1}, + {"expect": {"bodies": 1, "errors": 0, "param": {"thickness": 10}}}, + {"shot": "s15_reopened"} +] diff --git a/crates/ui-egui/tests/scenarios/s16_tabs_separate_undo.json b/crates/ui-egui/tests/scenarios/s16_tabs_separate_undo.json new file mode 100644 index 00000000..177148a7 --- /dev/null +++ b/crates/ui-egui/tests/scenarios/s16_tabs_separate_undo.json @@ -0,0 +1,39 @@ +[ + {"start": "SketchCreate"}, + {"click": {"plane": "XY"}}, + {"start": "ShapeRectangleTwoPoint"}, + {"click": {"sketch": [0, 0]}}, + {"click": {"sketch": [30, 20]}}, + {"key": "Escape"}, + {"start": "SketchStop"}, + {"start": "Extrude"}, + {"text": "10"}, + {"key": "Enter"}, + {"expect": {"bodies": 1, "volume": 6000, "documents": 1}}, + {"key": "N", "cmd": true}, + {"expect": {"bodies": 0, "documents": 2, "features": 0}}, + {"start": "SketchCreate"}, + {"click": {"plane": "XY"}}, + {"start": "CircleCenterRadius"}, + {"click": {"sketch": [0, 0]}}, + {"click": {"sketch": [10, 0]}}, + {"key": "Escape"}, + {"start": "SketchStop"}, + {"start": "Extrude"}, + {"text": "5"}, + {"key": "Enter"}, + {"expect": {"bodies": 1, "volume": [1570.8, 1e-3]}}, + {"key": "Z", "cmd": true}, + {"expect": {"bodies": 0}}, + {"call": "ui.documents", "params": {"action": "switch", "index": 0}}, + {"expect": {"bodies": 1, "volume": 6000}}, + {"key": "Z", "cmd": true}, + {"expect": {"bodies": 0}}, + {"key": "Y", "cmd": true}, + {"expect": {"bodies": 1, "volume": 6000}}, + {"call": "ui.documents", "params": {"action": "switch", "index": 1}}, + {"expect": {"bodies": 0}}, + {"key": "Y", "cmd": true}, + {"expect": {"bodies": 1, "volume": [1570.8, 1e-3]}}, + {"shot": "s16_second_tab"} +] diff --git a/crates/ui-egui/tests/scenarios/s17_toolbox_and_visibility.json b/crates/ui-egui/tests/scenarios/s17_toolbox_and_visibility.json new file mode 100644 index 00000000..f4615b2c --- /dev/null +++ b/crates/ui-egui/tests/scenarios/s17_toolbox_and_visibility.json @@ -0,0 +1,114 @@ +[ + { + "key": "S" + }, + { + "text": "Create Sketch" + }, + { + "key": "Enter" + }, + { + "click": { + "plane": "XY" + } + }, + { + "expect": { + "sketching": true + } + }, + { + "start": "ShapeRectangleTwoPoint" + }, + { + "click": { + "sketch": [ + 0, + 0 + ] + } + }, + { + "click": { + "sketch": [ + 30, + 20 + ] + } + }, + { + "key": "Escape" + }, + { + "key": "S" + }, + { + "text": "Finish Sketch" + }, + { + "key": "Enter" + }, + { + "expect": { + "sketching": false + } + }, + { + "key": "S" + }, + { + "text": "Extrude" + }, + { + "key": "Enter" + }, + { + "text": "10" + }, + { + "key": "Enter" + }, + { + "expect": { + "bodies": 1, + "volume": 6000 + } + }, + { + "call": "ui.view", + "params": { + "view": "iso" + } + }, + { + "click": { + "world": [ + 15, + 10, + 10 + ] + } + }, + { + "key": "V" + }, + { + "expect": { + "hidden": [ + "Body1" + ] + } + }, + { + "shot": "s17_hidden" + }, + { + "key": "V" + }, + { + "expect": { + "hidden": [] + } + } +] \ No newline at end of file diff --git a/crates/ui-egui/tests/scenarios/s18_3d_sketch_pipe.json b/crates/ui-egui/tests/scenarios/s18_3d_sketch_pipe.json new file mode 100644 index 00000000..42c21bc3 --- /dev/null +++ b/crates/ui-egui/tests/scenarios/s18_3d_sketch_pipe.json @@ -0,0 +1,168 @@ +[ + { + "start": "SketchCreate" + }, + { + "click": { + "plane": "XY" + } + }, + { + "start": "ShapeRectangleTwoPoint" + }, + { + "click": { + "sketch": [ + 0, + 0 + ] + } + }, + { + "click": { + "sketch": [ + 30, + 20 + ] + } + }, + { + "key": "Escape" + }, + { + "start": "SketchStop" + }, + { + "call": "ui.view", + "params": { + "view": "iso" + } + }, + { + "start": "Extrude" + }, + { + "text": "15" + }, + { + "key": "Enter" + }, + { + "start": "SketchCreate" + }, + { + "click": { + "world": [ + 15, + 0, + 7 + ] + } + }, + { + "expect": { + "sketching": true + } + }, + { + "widget": "3D Sketch" + }, + { + "call": "ui.view", + "params": { + "view": "iso" + } + }, + { + "start": "DrawPolyline" + }, + { + "click": { + "world": [ + 30, + 0, + 15 + ] + } + }, + { + "click": { + "world": [ + 30, + 20, + 15 + ] + } + }, + { + "click": { + "world": [ + 0, + 20, + 15 + ] + } + }, + { + "key": "Escape" + }, + { + "key": "Escape" + }, + { + "shot": "s18_3d_lines" + }, + { + "expect": { + "query": { + "command": "sketch.inspect", + "path": [ + "wires", + 1, + "id" + ], + "value": "w2" + } + } + }, + { + "start": "SketchStop" + }, + { + "start": "PrimitivePipe" + }, + { + "click": { + "world": [ + 30, + 10, + 15 + ] + } + }, + { + "click": { + "world": [ + 15, + 20, + 15 + ] + } + }, + { + "text": "3" + }, + { + "key": "Enter" + }, + { + "expect": { + "bodies": 2, + "errors": 0, + "dialog": null + } + }, + { + "shot": "s18_pipe" + } +] \ No newline at end of file diff --git a/crates/ui-egui/tests/scenarios/s19_canvas_calibrate.json b/crates/ui-egui/tests/scenarios/s19_canvas_calibrate.json new file mode 100644 index 00000000..24b8979a --- /dev/null +++ b/crates/ui-egui/tests/scenarios/s19_canvas_calibrate.json @@ -0,0 +1,72 @@ +[ + { + "make_image": "pic.png" + }, + { + "choose_file": "pic.png" + }, + { + "start": "FusionAddCanvasCommand" + }, + { + "click": { + "plane": "XY" + } + }, + { + "key": "Escape" + }, + { + "expect": { + "canvas_width": [ + 100, + 0.5 + ] + } + }, + { + "call": "ui.menu", + "params": { + "x": 100, + "y": 260, + "target": { + "type": "canvas", + "id": 1 + } + } + }, + { + "call": "ui.menuPick", + "params": { + "item": "Calibrate" + } + }, + { + "widget": "100.0 mm", + "near": "Distance" + }, + { + "key": "A", + "cmd": true + }, + { + "text": "50" + }, + { + "key": "Enter" + }, + { + "widget": "Calibrate" + }, + { + "expect": { + "canvas_width": [ + 50, + 0.01 + ] + } + }, + { + "shot": "s19_canvas" + } +] \ No newline at end of file diff --git a/crates/ui-egui/tests/scenarios/s20_section_with_joint_drive.json b/crates/ui-egui/tests/scenarios/s20_section_with_joint_drive.json new file mode 100644 index 00000000..4d4bdea1 --- /dev/null +++ b/crates/ui-egui/tests/scenarios/s20_section_with_joint_drive.json @@ -0,0 +1,249 @@ +[ + { + "start": "FusionCreateNewComponentCommand" + }, + { + "key": "Enter" + }, + { + "start": "SketchCreate" + }, + { + "click": { + "plane": "XY" + } + }, + { + "start": "ShapeRectangleTwoPoint" + }, + { + "click": { + "sketch": [ + 0, + 0 + ] + } + }, + { + "click": { + "sketch": [ + 20, + 20 + ] + } + }, + { + "key": "Escape" + }, + { + "start": "SketchStop" + }, + { + "call": "ui.view", + "params": { + "view": "iso" + } + }, + { + "start": "Extrude" + }, + { + "text": "10" + }, + { + "key": "Enter" + }, + { + "call": "ui.menu", + "params": { + "x": 100, + "y": 170, + "target": { + "type": "component", + "id": 0 + } + } + }, + { + "call": "ui.menuPick", + "params": { + "item": "Activate" + } + }, + { + "start": "FusionCreateNewComponentCommand" + }, + { + "key": "Enter" + }, + { + "start": "SketchCreate" + }, + { + "click": { + "plane": "XZ" + } + }, + { + "start": "ShapeRectangleTwoPoint" + }, + { + "click": { + "sketch": [ + 30, + 0 + ] + } + }, + { + "click": { + "sketch": [ + 50, + 10 + ] + } + }, + { + "key": "Escape" + }, + { + "start": "SketchStop" + }, + { + "call": "ui.view", + "params": { + "view": "iso" + } + }, + { + "start": "Extrude" + }, + { + "click": { + "world": [ + 40, + 0, + 5 + ] + } + }, + { + "text": "20" + }, + { + "key": "Enter" + }, + { + "expect": { + "bodies": 2, + "errors": 0 + } + }, + { + "start": "JointAssembleCmdNew" + }, + { + "click": { + "world": [ + 10, + 10, + 10 + ] + } + }, + { + "click": { + "world": [ + 40, + 0, + 5 + ] + } + }, + { + "widget": "Rigid" + }, + { + "widget": "Revolute" + }, + { + "widget": "OK" + }, + { + "expect": { + "dialog": null, + "errors": 0, + "query": { + "command": "joint.list", + "path": [ + "joints", + 0, + "type" + ], + "value": "revolute" + } + } + }, + { + "start": "FusionHalfSectionViewCommand" + }, + { + "click": { + "plane": "XZ" + } + }, + { + "debug": 1 + }, + { + "key": "Enter" + }, + { + "expect": { + "section": true, + "dialog": null + } + }, + { + "shot": "s20_section" + }, + { + "start": "FusionMoveJointsCommand" + }, + { + "widget": "0 deg" + }, + { + "key": "A", + "cmd": true + }, + { + "text": "90" + }, + { + "key": "Enter" + }, + { + "expect": { + "query": { + "command": "joint.list", + "path": [ + "joints", + 0, + "values", + 0, + "value" + ], + "value": 90 + } + } + }, + { + "expect": { + "section": true + } + }, + { + "shot": "s20_section_driven" + } +] \ No newline at end of file