#979: test that layer context menus offer Create Shapes from Vector Layer

Right-clicking vector footage in the Timeline or the Composition viewer offered no vector conversion. The test imports an SVG and checks the item is absent for a solid, present on the SVG layer in both menus, and converts it. Without the fix the menu item is missing.
This commit is contained in:
skylar taylor-barrick
2026-10-22 18:55:49 -06:00
parent 1c3522be4a
commit d87a6940f4
@@ -637,3 +637,66 @@ fn timeline_context_menu_creates_shapes_and_masks_from_text() {
assert!(h.query_by_label("Create Shapes from Text").is_none(), "no text conversion for a solid");
assert!(h.query_by_label("Create Masks from Text").is_none(), "no text conversion for a solid");
}
// Regression test for #979 (#958): right-clicking a vector footage layer (SVG) in the Timeline
// or the Composition viewer offered no Create Shapes from Vector Layer; raster layers must not.
#[test]
fn layer_context_menus_offer_create_shapes_from_vector_layer_for_vector_footage() {
use effectcraft_engine::project::LayerSource;
let dir = std::env::temp_dir().join(format!("ec-ui-vector-menu-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let svg = dir.join("logo.svg");
std::fs::write(
&svg,
r##"<svg xmlns="http://www.w3.org/2000/svg" width="40" height="20" viewBox="0 0 40 20"><rect width="40" height="20" fill="#ff0000"/></svg>"##,
)
.unwrap();
let mut s = effectcraft_host::session();
let item = s.execute("file.import", json!({"paths": [svg.to_string_lossy()]})).unwrap()["items"][0].as_u64().unwrap();
s.execute("comp.new", json!({"name":"Vec","width":320,"height":180,"duration":4})).unwrap();
let logo = s.execute("layer.addItem", json!({"item": item})).unwrap()["layer"].as_u64().unwrap();
let plate = s.execute("layer.newSolid", json!({"name":"Plate","width":40,"height":40,"color":"#406080"})).unwrap()["layer"].as_u64().unwrap();
s.execute("edit.deselectAll", json!({})).unwrap();
let mut h = Harness::builder().with_size(vec2(1600.0, 1000.0)).build_eframe(|_| EffectcraftApp::new(s));
h.run_steps(3);
let right_click = |h: &mut Harness<'_, EffectcraftApp>, p: Pos2| {
for pressed in [true, false] {
h.input_mut().events.push(Event::PointerMoved(p));
h.input_mut().events.push(Event::PointerButton { pos: p, button: PointerButton::Secondary, pressed, modifiers: Modifiers::NONE });
h.step();
}
h.run_steps(3);
};
let row = |h: &Harness<'_, EffectcraftApp>, id: u64| {
let r = rect(h, &format!("timeline.layer.{id}.row"));
pos2(r.min.x + 210.0, r.center().y)
};
let label = "Create Shapes from Vector Layer";
let shapes =
|h: &Harness<'_, EffectcraftApp>| h.state().session.active_comp().unwrap().layers.iter().filter(|l| matches!(l.source, LayerSource::Shape)).count();
// A raster-less solid does not offer the conversion.
h.state_mut().session.execute("layer.select", json!({"layers": [plate]})).unwrap();
h.run_steps(3);
let at = row(&h, plate);
right_click(&mut h, at);
assert!(h.query_by_label(label).is_none(), "no vector conversion for a solid");
h.key_press(egui::Key::Escape);
h.run_steps(3);
// The Timeline row of the SVG layer offers it, and it converts the layer.
h.state_mut().session.execute("layer.select", json!({"layers": [logo]})).unwrap();
h.run_steps(3);
let at = row(&h, logo);
right_click(&mut h, at);
h.get_by_label(label).click();
h.run_steps(3);
assert_eq!(shapes(&h), 1, "a shape layer was created from the SVG layer");
// The Composition viewer's menu offers it for the SVG layer under the pointer too.
h.state_mut().session.execute("edit.undo", json!({})).unwrap();
h.state_mut().session.execute("prop.set", json!({"layer": plate, "path": "transform/position", "value": [40.0, 40.0]})).unwrap();
h.state_mut().session.execute("layer.select", json!({"layers": [logo]})).unwrap();
h.run_steps(3);
let comp = rect(&h, "viewer.comp");
right_click(&mut h, comp.center());
assert!(h.query_by_label(label).is_some(), "the viewer's context menu offers the vector conversion");
let _ = std::fs::remove_dir_all(&dir);
}