UI scenarios are found, not listed: build.rs makes one test per tests/scenarios/*.json, named after the file, and a scenario waiting for a fix says so in its own file with a {"pending": "why"} step (its test is ignored with that reason). No shared list to edit, no numbering required
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
305c1c9a9b
commit
acc7b83625
9 files changed
+363
-119
No files matched your search
@@ -17,3 +17,6 @@ serde_json = { workspace = true }
|
||||
log = { workspace = true }
|
||||
web-time = { workspace = true }
|
||||
image = { workspace = true, features = ["jpeg"] }
|
||||
|
||||
[build-dependencies]
|
||||
serde_json = { workspace = true }
|
||||
+33
-1
@@ -1,5 +1,6 @@
|
||||
//! Build information for Help › About: the git commit and the build date (UTC). Both fall back to
|
||||
//! "unknown" when git is missing (a source tarball).
|
||||
//! "unknown" when git is missing (a source tarball). Also one test per UI scenario file
|
||||
//! (`tests/scenarios/*.json`), so adding a scenario needs no Rust edit.
|
||||
|
||||
use std::process::Command;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
@@ -34,4 +35,35 @@ fn main() {
|
||||
println!("cargo:rerun-if-changed={dir}/logs/HEAD");
|
||||
}
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
scenario_tests();
|
||||
}
|
||||
|
||||
/// `$OUT_DIR/scenario_tests.rs`: a `#[test]` per scenario file, named after it; a scenario with
|
||||
/// a `{"pending": "why"}` step is `#[ignore]`d with that reason (it waits for a fix).
|
||||
fn scenario_tests() {
|
||||
let dir = std::path::Path::new("tests/scenarios");
|
||||
println!("cargo:rerun-if-changed=tests/scenarios");
|
||||
let mut files: Vec<std::path::PathBuf> = std::fs::read_dir(dir)
|
||||
.map(|rd| rd.filter_map(|e| e.ok().map(|e| e.path())).filter(|p| p.extension().is_some_and(|x| x == "json")).collect())
|
||||
.unwrap_or_default();
|
||||
files.sort();
|
||||
let mut out = String::new();
|
||||
for f in files {
|
||||
let Some(stem) = f.file_stem().and_then(|s| s.to_str()) else { continue };
|
||||
let mut name: String = stem.chars().map(|c| if c.is_ascii_alphanumeric() || c == '_' { c } else { '_' }).collect();
|
||||
if !name.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_') {
|
||||
name.insert_str(0, "s_");
|
||||
}
|
||||
let text = std::fs::read_to_string(&f).unwrap_or_default();
|
||||
let pending = serde_json::from_str::<serde_json::Value>(&text)
|
||||
.ok()
|
||||
.and_then(|v| v.as_array().and_then(|a| a.iter().find_map(|s| s.get("pending").map(|p| p.as_str().unwrap_or("pending").to_string()))));
|
||||
if let Some(why) = pending {
|
||||
out += &format!("#[test]\n#[ignore = {why:?}]\nfn {name}() {{\n run({stem:?});\n}}\n\n");
|
||||
} else {
|
||||
out += &format!("#[test]\nfn {name}() {{\n run({stem:?});\n}}\n\n");
|
||||
}
|
||||
}
|
||||
let path = std::path::Path::new(&std::env::var("OUT_DIR").unwrap_or_else(|_| ".".into())).join("scenario_tests.rs");
|
||||
let _ = std::fs::write(path, out);
|
||||
}
|
||||
@@ -24,7 +24,8 @@
|
||||
//! `{"dimension": "d1"}` or `{"handle": "arrow"}` (a manipulator handle: see [`publish_handle`])
|
||||
//! - `{"autosave": true}`: autosave into the scenario's recovery folder; `{"restart": "crash" |
|
||||
//! "close"}`: the app dies (or closes) and a new one starts on the same folders
|
||||
//! - `{"note": "…"}`: a comment
|
||||
//! - `{"note": "…"}`: a comment; `{"pending": "why"}`: the scenario waits for a fix (its test
|
||||
//! is ignored)
|
||||
//! - `{"expect": {…}}`: checks, see [`check`]; `{"until": {…}}` waits (frames) until they pass
|
||||
//!
|
||||
//! The same files drive the live app over the control channel for screenshots.
|
||||
@@ -324,7 +325,7 @@ impl Harness {
|
||||
self.call("ui.drag", json!({"x0": x0, "y0": y0, "x1": x1, "y1": y1, "steps": steps, "shift": flag("shift"), "ctrl": flag("ctrl")}))
|
||||
} 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 s.get("note").is_some() {
|
||||
} else if s.get("note").is_some() || s.get("pending").is_some() {
|
||||
return Ok(());
|
||||
} 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.
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
//! User scenarios in the real UI, headless (see `solvecraft_ui_egui::scenario`). Each JSON file
|
||||
//! under `tests/scenarios/` is one workflow done with clicks, keys and drags.
|
||||
//! under `tests/scenarios/` is one workflow done with clicks, keys and drags, and one test here:
|
||||
//! `build.rs` makes a test per file, named after it. A scenario waiting for a fix starts with a
|
||||
//! `{"pending": "why"}` step (its test is ignored with that reason). Adding a scenario is adding
|
||||
//! its file.
|
||||
|
||||
use solvecraft_ui_egui::scenario::Harness;
|
||||
|
||||
@@ -13,72 +16,7 @@ fn run(name: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! scenarios {
|
||||
($($name:ident),* $(,)?) => {
|
||||
$(#[test]
|
||||
fn $name() {
|
||||
run(stringify!($name));
|
||||
})*
|
||||
};
|
||||
}
|
||||
|
||||
scenarios!(
|
||||
s01_box_fillet_hole_shell,
|
||||
s02_revolve_on_xz,
|
||||
s03_sketch_on_face_cut,
|
||||
s04_mirror_body,
|
||||
s05_delete_and_undo,
|
||||
s06_edit_early_feature,
|
||||
s07_components_joint_drive,
|
||||
s08_sheet_metal_flat_dxf,
|
||||
s09_step_roundtrip_hole,
|
||||
s10_parameters_drive_model,
|
||||
s11_sketch_constraints,
|
||||
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,
|
||||
s21_named_views,
|
||||
s26_drive_joint_typed,
|
||||
s22_sheet_metal_fold,
|
||||
s23_configurations,
|
||||
s24_motion_study,
|
||||
s25_autosave_recovery,
|
||||
s27_move_triad,
|
||||
s33_move_x_arrow,
|
||||
s34_component_move_carries_sketches,
|
||||
s35_construction_mode,
|
||||
s36_sketch_palette_visible,
|
||||
s32_cut_in_active_component,
|
||||
s30_pattern_a_hole,
|
||||
s41_type_value_after_picking,
|
||||
s42_drag_snap
|
||||
);
|
||||
|
||||
/// Scenarios written ahead of their fixes or features (the user's open bugs): run with
|
||||
/// `--ignored`; each moves to the list above once its fix lands.
|
||||
macro_rules! pending {
|
||||
($($name:ident: $why:literal),* $(,)?) => {
|
||||
$(#[test]
|
||||
#[ignore = $why]
|
||||
fn $name() {
|
||||
run(stringify!($name));
|
||||
})*
|
||||
};
|
||||
}
|
||||
|
||||
pending!(
|
||||
s31_sketch_on_face_projects: "user bug: a sketch on a face projects its loops as reference (solvecraft-sketch)",
|
||||
s37_browser_chevrons: "every chevron works; the browser has no Joints folder (solvecraft-shell)",
|
||||
s38_view_anim_no_jump: "the last frame of an animation to Top turns the view 22.5 degrees (solvecraft-ui)",
|
||||
s39_view_cube_hover: "view cube edge and corner hover light 1 patch, Fusion lights 2 and 3 (solvecraft-ui)",
|
||||
s40_toolbar_customize_persists: "toolbar Pin/Remove/reorder (toolbar:<id> handles, ui.menu toolbar_command) and a full-height workspace switcher (solvecraft-shell)",
|
||||
);
|
||||
include!(concat!(env!("OUT_DIR"), "/scenario_tests.rs"));
|
||||
|
||||
/// A scenario file named by `SOLVECRAFT_SCENARIO_FILE` (for trying one out):
|
||||
/// `SOLVECRAFT_SCENARIO_FILE=x.json cargo test --test scenarios adhoc -- --ignored --nocapture`.
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
[
|
||||
{
|
||||
"pending": "user bug: a sketch on a face projects its loops as reference (solvecraft-sketch)"
|
||||
},
|
||||
{
|
||||
"note": "User bug 1 (owner solvecraft-sketch): Create Sketch on a body face projects the face's loops as reference geometry (linked), with no big square of real lines, and the face stays pickable for Project."
|
||||
},
|
||||
@@ -138,4 +141,4 @@
|
||||
"errors": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -1,4 +1,7 @@
|
||||
[
|
||||
{
|
||||
"pending": "every chevron works; the browser has no Joints folder (solvecraft-shell)"
|
||||
},
|
||||
{
|
||||
"note": "Browser chevrons expand and collapse every kind of node (folders, components, groups, the origin, named views, document settings). Model: two components with a body each, a joint, a sketch, an offset plane, a named view."
|
||||
},
|
||||
@@ -520,4 +523,4 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -1,20 +1,77 @@
|
||||
[
|
||||
{"note": "Animating to Top or Bottom from iso, front or any orbit turns the view smoothly to the end: no frame (the last included) turns it more than 10 degrees."},
|
||||
{"start": "PrimitiveBox"},
|
||||
{"key": "Escape"},
|
||||
{"view_anim": "top"},
|
||||
{"view_anim": "home"},
|
||||
{"view_anim": "bottom"},
|
||||
{"call": "ui.view", "params": {"view": "front"}},
|
||||
{"view_anim": "top"},
|
||||
{"call": "ui.view", "params": {"view": "front"}},
|
||||
{"view_anim": "bottom"},
|
||||
{"camera": {"yaw": 2.3, "pitch": 0.35}},
|
||||
{"view_anim": "top"},
|
||||
{"camera": {"yaw": -1.1, "pitch": -0.4}},
|
||||
{"view_anim": "top"},
|
||||
{"camera": {"yaw": 2.3, "pitch": 0.35}},
|
||||
{"view_anim": "bottom"},
|
||||
{"camera": {"yaw": 0.4, "pitch": 1.2}},
|
||||
{"view_anim": "front"}
|
||||
{
|
||||
"pending": "the last frame of an animation to Top turns the view 22.5 degrees (solvecraft-ui)"
|
||||
},
|
||||
{
|
||||
"note": "Animating to Top or Bottom from iso, front or any orbit turns the view smoothly to the end: no frame (the last included) turns it more than 10 degrees."
|
||||
},
|
||||
{
|
||||
"start": "PrimitiveBox"
|
||||
},
|
||||
{
|
||||
"key": "Escape"
|
||||
},
|
||||
{
|
||||
"view_anim": "top"
|
||||
},
|
||||
{
|
||||
"view_anim": "home"
|
||||
},
|
||||
{
|
||||
"view_anim": "bottom"
|
||||
},
|
||||
{
|
||||
"call": "ui.view",
|
||||
"params": {
|
||||
"view": "front"
|
||||
}
|
||||
},
|
||||
{
|
||||
"view_anim": "top"
|
||||
},
|
||||
{
|
||||
"call": "ui.view",
|
||||
"params": {
|
||||
"view": "front"
|
||||
}
|
||||
},
|
||||
{
|
||||
"view_anim": "bottom"
|
||||
},
|
||||
{
|
||||
"camera": {
|
||||
"yaw": 2.3,
|
||||
"pitch": 0.35
|
||||
}
|
||||
},
|
||||
{
|
||||
"view_anim": "top"
|
||||
},
|
||||
{
|
||||
"camera": {
|
||||
"yaw": -1.1,
|
||||
"pitch": -0.4
|
||||
}
|
||||
},
|
||||
{
|
||||
"view_anim": "top"
|
||||
},
|
||||
{
|
||||
"camera": {
|
||||
"yaw": 2.3,
|
||||
"pitch": 0.35
|
||||
}
|
||||
},
|
||||
{
|
||||
"view_anim": "bottom"
|
||||
},
|
||||
{
|
||||
"camera": {
|
||||
"yaw": 0.4,
|
||||
"pitch": 1.2
|
||||
}
|
||||
},
|
||||
{
|
||||
"view_anim": "front"
|
||||
}
|
||||
]
|
||||
@@ -1,16 +1,113 @@
|
||||
[
|
||||
{"note": "View cube hover from the home view: a face lights one patch, an edge the 2 patches on its faces, a corner the 3 around it (Fusion)."},
|
||||
{"call": "ui.view", "params": {"view": "home"}},
|
||||
{"move": {"cube": [0, 0, 1]}},
|
||||
{"expect": {"count": {"name": "cube_highlights", "value": 1}}},
|
||||
{"move": {"cube": [0, -0.9, 1]}},
|
||||
{"expect": {"count": {"name": "cube_highlights", "value": 2}}},
|
||||
{"move": {"cube": [1, -0.9, 0]}},
|
||||
{"expect": {"count": {"name": "cube_highlights", "value": 2}}},
|
||||
{"move": {"cube": [0.9, -0.9, 1]}},
|
||||
{"expect": {"count": {"name": "cube_highlights", "value": 3}}},
|
||||
{"move": {"cube": [0.9, -1, 0.9]}},
|
||||
{"expect": {"count": {"name": "cube_highlights", "value": 3}}},
|
||||
{"move": [800, 500]},
|
||||
{"expect": {"count": {"name": "cube_highlights", "value": 0}}}
|
||||
{
|
||||
"pending": "view cube edge and corner hover light 1 patch, Fusion lights 2 and 3 (solvecraft-ui)"
|
||||
},
|
||||
{
|
||||
"note": "View cube hover from the home view: a face lights one patch, an edge the 2 patches on its faces, a corner the 3 around it (Fusion)."
|
||||
},
|
||||
{
|
||||
"call": "ui.view",
|
||||
"params": {
|
||||
"view": "home"
|
||||
}
|
||||
},
|
||||
{
|
||||
"move": {
|
||||
"cube": [
|
||||
0,
|
||||
0,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"count": {
|
||||
"name": "cube_highlights",
|
||||
"value": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"move": {
|
||||
"cube": [
|
||||
0,
|
||||
-0.9,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"count": {
|
||||
"name": "cube_highlights",
|
||||
"value": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"move": {
|
||||
"cube": [
|
||||
1,
|
||||
-0.9,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"count": {
|
||||
"name": "cube_highlights",
|
||||
"value": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"move": {
|
||||
"cube": [
|
||||
0.9,
|
||||
-0.9,
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"count": {
|
||||
"name": "cube_highlights",
|
||||
"value": 3
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"move": {
|
||||
"cube": [
|
||||
0.9,
|
||||
-1,
|
||||
0.9
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"count": {
|
||||
"name": "cube_highlights",
|
||||
"value": 3
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"move": [
|
||||
800,
|
||||
500
|
||||
]
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"count": {
|
||||
"name": "cube_highlights",
|
||||
"value": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,17 +1,127 @@
|
||||
[
|
||||
{"note": "Toolbar customizing like Fusion survives a restart: Pin to Toolbar, Remove from Toolbar, drag to reorder. Toolbar buttons publish handles `toolbar:<command id>`. The workspace switcher spans the toolbar's height."},
|
||||
{"expect": {"spans": {"inner": "workspace_switcher", "outer": "toolbar", "axis": "y", "tol": 6}}},
|
||||
{"expect": {"shown_handles": ["toolbar:Extrude", "toolbar:Revolve"], "hidden_handles": ["toolbar:FusionShellBodyCommand"]}},
|
||||
{"call": "ui.menu", "params": {"x": 400, "y": 80, "target": {"type": "toolbar_command", "id": "FusionShellBodyCommand"}}},
|
||||
{"call": "ui.menuPick", "params": {"item": "Pin to Toolbar"}},
|
||||
{"expect": {"shown_handles": ["toolbar:FusionShellBodyCommand"]}},
|
||||
{"call": "ui.menu", "params": {"x": 400, "y": 80, "target": {"type": "toolbar_command", "id": "Revolve"}}},
|
||||
{"call": "ui.menuPick", "params": {"item": "Remove from Toolbar"}},
|
||||
{"expect": {"hidden_handles": ["toolbar:Revolve"]}},
|
||||
{"drag": [{"handle": "toolbar:FusionShellBodyCommand"}, {"handle": "toolbar:Extrude"}], "steps": 12},
|
||||
{"expect": {"handle_order": ["toolbar:FusionShellBodyCommand", "toolbar:Extrude"]}},
|
||||
{"restart": "close"},
|
||||
{"expect": {"shown_handles": ["toolbar:FusionShellBodyCommand"], "hidden_handles": ["toolbar:Revolve"]}},
|
||||
{"expect": {"handle_order": ["toolbar:FusionShellBodyCommand", "toolbar:Extrude"]}},
|
||||
{"expect": {"spans": {"inner": "workspace_switcher", "outer": "toolbar", "axis": "y", "tol": 6}}}
|
||||
{
|
||||
"pending": "toolbar Pin/Remove/reorder (toolbar:<id> handles, ui.menu toolbar_command) and a full-height workspace switcher (solvecraft-shell)"
|
||||
},
|
||||
{
|
||||
"note": "Toolbar customizing like Fusion survives a restart: Pin to Toolbar, Remove from Toolbar, drag to reorder. Toolbar buttons publish handles `toolbar:<command id>`. The workspace switcher spans the toolbar's height."
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"spans": {
|
||||
"inner": "workspace_switcher",
|
||||
"outer": "toolbar",
|
||||
"axis": "y",
|
||||
"tol": 6
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"shown_handles": [
|
||||
"toolbar:Extrude",
|
||||
"toolbar:Revolve"
|
||||
],
|
||||
"hidden_handles": [
|
||||
"toolbar:FusionShellBodyCommand"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"call": "ui.menu",
|
||||
"params": {
|
||||
"x": 400,
|
||||
"y": 80,
|
||||
"target": {
|
||||
"type": "toolbar_command",
|
||||
"id": "FusionShellBodyCommand"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"call": "ui.menuPick",
|
||||
"params": {
|
||||
"item": "Pin to Toolbar"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"shown_handles": [
|
||||
"toolbar:FusionShellBodyCommand"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"call": "ui.menu",
|
||||
"params": {
|
||||
"x": 400,
|
||||
"y": 80,
|
||||
"target": {
|
||||
"type": "toolbar_command",
|
||||
"id": "Revolve"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"call": "ui.menuPick",
|
||||
"params": {
|
||||
"item": "Remove from Toolbar"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"hidden_handles": [
|
||||
"toolbar:Revolve"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"drag": [
|
||||
{
|
||||
"handle": "toolbar:FusionShellBodyCommand"
|
||||
},
|
||||
{
|
||||
"handle": "toolbar:Extrude"
|
||||
}
|
||||
],
|
||||
"steps": 12
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"handle_order": [
|
||||
"toolbar:FusionShellBodyCommand",
|
||||
"toolbar:Extrude"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"restart": "close"
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"shown_handles": [
|
||||
"toolbar:FusionShellBodyCommand"
|
||||
],
|
||||
"hidden_handles": [
|
||||
"toolbar:Revolve"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"handle_order": [
|
||||
"toolbar:FusionShellBodyCommand",
|
||||
"toolbar:Extrude"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"expect": {
|
||||
"spans": {
|
||||
"inner": "workspace_switcher",
|
||||
"outer": "toolbar",
|
||||
"axis": "y",
|
||||
"tol": 6
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
Reference in new issue
Block a user