mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-09 03:51:42 +00:00
369 lines
20 KiB
Rust
369 lines
20 KiB
Rust
//! Frames, groups and sections converted into each other in place: the layers stay where they are on
|
||
//! the page, what only a frame has goes with it, what can't convert is refused, one undo step.
|
||
|
||
use serde_json::{Value, json};
|
||
|
||
use crate::params::id_of;
|
||
use crate::{EngineError, Session};
|
||
use figcraft_doc::{NodeId, NodeKind, Sizing};
|
||
|
||
fn id(v: Value) -> NodeId {
|
||
id_of(&v["id"]).unwrap()
|
||
}
|
||
|
||
fn ex(s: &mut Session, cmd: &str, p: Value) -> Value {
|
||
s.execute(cmd, &p).unwrap_or_else(|e| panic!("{cmd} {p}: {e}"))
|
||
}
|
||
|
||
fn rect(s: &mut Session, parent: NodeId, x: f64, y: f64) -> NodeId {
|
||
id(ex(s, "shape.rectangle", json!({"parent": parent, "x": x, "y": y, "width": 40, "height": 20})))
|
||
}
|
||
|
||
/// A layer's box on the page: `[x0, y0, x1, y1]`, rounded.
|
||
fn page_box(s: &Session, n: NodeId) -> [f64; 4] {
|
||
let b = s.doc().unwrap().world_bounds(n).unwrap();
|
||
[b.x0, b.y0, b.x1, b.y1].map(|v| (v * 1000.0).round() / 1000.0)
|
||
}
|
||
|
||
fn kind(s: &Session, n: NodeId) -> NodeKind {
|
||
s.doc().unwrap().node(n).unwrap().kind.clone()
|
||
}
|
||
|
||
#[test]
|
||
fn adding_auto_layout_converts_a_single_group_and_arranges_its_children_directly() {
|
||
let mut s = Session::new();
|
||
let page = s.page().unwrap();
|
||
let (a, b) = (rect(&mut s, page, 10.0, 20.0), rect(&mut s, page, 90.0, 20.0));
|
||
let group = id(ex(&mut s, "object.group", json!({"ids": [a, b], "name": "Actions"})));
|
||
let siblings = s.doc().unwrap().node(page).unwrap().children.clone();
|
||
let before = [page_box(&s, group), page_box(&s, a), page_box(&s, b)];
|
||
|
||
let result = ex(&mut s, "layout.add", json!({"ids": [group], "direction": "horizontal"}));
|
||
assert_eq!(result["ids"], json!([group]), "the group's identity is retained");
|
||
let doc = s.doc().unwrap();
|
||
let frame = doc.node(group).unwrap();
|
||
assert!(matches!(frame.kind, NodeKind::Frame));
|
||
assert_eq!(frame.name, "Actions");
|
||
assert_eq!(frame.parent, Some(page));
|
||
assert_eq!(frame.children, vec![a, b], "no intermediate group or wrapper frame");
|
||
assert!(frame.layout.is_some() && frame.fills.is_empty() && !frame.clip);
|
||
assert_eq!(doc.node(page).unwrap().children, siblings);
|
||
assert_eq!([page_box(&s, group), page_box(&s, a), page_box(&s, b)], before);
|
||
|
||
// The second child now follows changes to the first through auto layout.
|
||
ex(&mut s, "node.set", json!({"id": a, "props": {"width": 60}}));
|
||
assert_eq!(page_box(&s, b), [110.0, 20.0, 150.0, 40.0]);
|
||
s.undo().unwrap();
|
||
s.undo().unwrap();
|
||
let restored = s.doc().unwrap().node(group).unwrap();
|
||
assert!(matches!(restored.kind, NodeKind::Group) && restored.layout.is_none());
|
||
assert_eq!(restored.children, vec![a, b]);
|
||
assert_eq!([page_box(&s, group), page_box(&s, a), page_box(&s, b)], before);
|
||
s.redo().unwrap();
|
||
assert!(matches!(kind(&s, group), NodeKind::Frame));
|
||
assert!(s.doc().unwrap().node(group).unwrap().layout.is_some());
|
||
}
|
||
|
||
#[test]
|
||
fn adding_auto_layout_to_multiple_groups_still_wraps_the_selection() {
|
||
let mut s = Session::new();
|
||
let page = s.page().unwrap();
|
||
let (a, b) = (rect(&mut s, page, 10.0, 20.0), rect(&mut s, page, 90.0, 20.0));
|
||
let g1 = id(ex(&mut s, "object.group", json!({"ids": [a]})));
|
||
let g2 = id(ex(&mut s, "object.group", json!({"ids": [b]})));
|
||
let result = ex(&mut s, "layout.add", json!({"ids": [g1, g2]}));
|
||
let frame = crate::params::id_of(&result["ids"][0]).unwrap();
|
||
let doc = s.doc().unwrap();
|
||
assert!(matches!(doc.node(frame).unwrap().kind, NodeKind::Frame));
|
||
assert_eq!(doc.node(frame).unwrap().children, vec![g1, g2]);
|
||
assert!(matches!(kind(&s, g1), NodeKind::Group) && matches!(kind(&s, g2), NodeKind::Group));
|
||
}
|
||
|
||
#[test]
|
||
fn adding_auto_layout_preserves_mask_runs_and_their_rendering() {
|
||
let mut s = Session::new();
|
||
let page = s.page().unwrap();
|
||
let (right, left) = (rect(&mut s, page, 300.0, 0.0), rect(&mut s, page, 0.0, 0.0));
|
||
let (mask, content) = (rect(&mut s, page, 100.0, 0.0), rect(&mut s, page, 80.0, 0.0));
|
||
let (mask2, content2) = (rect(&mut s, page, 200.0, 0.0), rect(&mut s, page, 180.0, 0.0));
|
||
let group = id(ex(&mut s, "object.group", json!({"ids": [right, left, mask, content, mask2, content2]})));
|
||
ex(&mut s, "node.set", json!({"ids": [mask, mask2], "props": {"isMask": true}}));
|
||
// Absolute layers keep the overlaps, isolating paint order from normal flow placement.
|
||
ex(&mut s, "node.set", json!({"ids": [mask, content, mask2, content2], "props": {"absolute": true}}));
|
||
let before = figcraft_render::render_node(s.doc().unwrap(), group, 1.0).unwrap();
|
||
ex(&mut s, "layout.add", json!({"ids": [group]}));
|
||
let doc = s.doc().unwrap();
|
||
let frame = doc.node(group).unwrap();
|
||
assert_eq!(frame.children, vec![left, right, mask, content, mask2, content2], "sort the unmasked prefix, retain mask runs");
|
||
let runs: Vec<Vec<NodeId>> = doc.paint_runs(frame).iter().map(|r| frame.children[r.clone()].to_vec()).collect();
|
||
assert_eq!(runs, vec![vec![left], vec![right], vec![mask, content], vec![mask2, content2]]);
|
||
let after = figcraft_render::render_node(doc, group, 1.0).unwrap();
|
||
assert_eq!((after.width(), after.height(), after.data()), (before.width(), before.height(), before.data()));
|
||
s.undo().unwrap();
|
||
assert_eq!(s.doc().unwrap().node(group).unwrap().children, vec![right, left, mask, content, mask2, content2]);
|
||
}
|
||
|
||
#[test]
|
||
fn a_hidden_mask_keeps_its_boundary_but_unmasked_children_still_sort() {
|
||
let mut s = Session::new();
|
||
let page = s.page().unwrap();
|
||
let mask = rect(&mut s, page, 100.0, 0.0);
|
||
let content = rect(&mut s, page, 80.0, 0.0);
|
||
let hidden_mask = rect(&mut s, page, 200.0, 0.0);
|
||
let (right, left) = (rect(&mut s, page, 300.0, 0.0), rect(&mut s, page, 250.0, 0.0));
|
||
let group = id(ex(&mut s, "object.group", json!({"ids": [mask, content, hidden_mask, right, left]})));
|
||
ex(&mut s, "node.set", json!({"ids": [mask, hidden_mask], "props": {"isMask": true}}));
|
||
ex(&mut s, "node.set", json!({"id": hidden_mask, "props": {"visible": false}}));
|
||
ex(&mut s, "layout.add", json!({"ids": [group], "direction": "horizontal"}));
|
||
let doc = s.doc().unwrap();
|
||
let frame = doc.node(group).unwrap();
|
||
assert_eq!(frame.children, vec![mask, content, hidden_mask, left, right]);
|
||
let runs: Vec<Vec<NodeId>> = doc.paint_runs(frame).iter().map(|r| frame.children[r.clone()].to_vec()).collect();
|
||
assert_eq!(runs, vec![vec![mask, content], vec![hidden_mask], vec![left], vec![right]]);
|
||
}
|
||
|
||
#[test]
|
||
fn adding_auto_layout_infers_gaps_from_flow_children_only() {
|
||
for props in [json!({"visible": false}), json!({"absolute": true})] {
|
||
let mut s = Session::new();
|
||
let page = s.page().unwrap();
|
||
let a = rect(&mut s, page, 0.0, 0.0);
|
||
let excluded = rect(&mut s, page, 80.0, 0.0);
|
||
let b = rect(&mut s, page, 160.0, 0.0);
|
||
let group = id(ex(&mut s, "object.group", json!({"ids": [a, excluded, b]})));
|
||
ex(&mut s, "node.set", json!({"id": excluded, "props": props}));
|
||
let before = [page_box(&s, a), page_box(&s, b)];
|
||
ex(&mut s, "layout.add", json!({"ids": [group]}));
|
||
let layout = s.doc().unwrap().node(group).unwrap().layout.as_ref().unwrap();
|
||
assert_eq!(layout.gap, 120.0);
|
||
assert_eq!([page_box(&s, a), page_box(&s, b)], before);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn hidden_children_do_not_influence_inferred_direction_or_padding() {
|
||
let mut s = Session::new();
|
||
let frame = id(ex(&mut s, "frame.create", json!({"width": 300, "height": 200})));
|
||
let (a, b) = (rect(&mut s, frame, 10.0, 20.0), rect(&mut s, frame, 90.0, 20.0));
|
||
let hidden = rect(&mut s, frame, 0.0, 1000.0);
|
||
ex(&mut s, "node.set", json!({"id": hidden, "props": {"visible": false}}));
|
||
let before = [page_box(&s, a), page_box(&s, b)];
|
||
ex(&mut s, "layout.add", json!({"ids": [frame]}));
|
||
let layout = s.doc().unwrap().node(frame).unwrap().layout.as_ref().unwrap();
|
||
assert_eq!(layout.mode, figcraft_doc::LayoutMode::Horizontal);
|
||
assert_eq!(layout.padding, [20.0, 170.0, 160.0, 10.0]);
|
||
assert_eq!(layout.gap, 40.0);
|
||
assert_eq!([page_box(&s, a), page_box(&s, b)], before);
|
||
}
|
||
|
||
#[test]
|
||
fn converting_a_group_preserves_fill_on_each_axis_in_its_parent() {
|
||
for (h, v) in [("fill", "fixed"), ("fixed", "fill"), ("fill", "fill")] {
|
||
let mut s = Session::new();
|
||
let parent = id(ex(&mut s, "frame.create", json!({"width": 500, "height": 300})));
|
||
let (a, b) = (rect(&mut s, parent, 0.0, 0.0), rect(&mut s, parent, 80.0, 0.0));
|
||
let group = id(ex(&mut s, "object.group", json!({"ids": [a, b]})));
|
||
let sibling = rect(&mut s, parent, 200.0, 0.0);
|
||
ex(&mut s, "node.set", json!({"id": parent, "props": {"layout": "horizontal", "padding": 0, "gap": 10}}));
|
||
ex(&mut s, "node.set", json!({"id": group, "props": {"sizingH": h, "sizingV": v}}));
|
||
let before = [page_box(&s, group), page_box(&s, sibling)];
|
||
ex(&mut s, "layout.add", json!({"ids": [group]}));
|
||
let cl = &s.doc().unwrap().node(group).unwrap().child_layout;
|
||
assert_eq!(cl.sizing_h, if h == "fill" { Sizing::Fill } else { Sizing::Hug });
|
||
assert_eq!(cl.sizing_v, if v == "fill" { Sizing::Fill } else { Sizing::Hug });
|
||
assert_eq!([page_box(&s, group), page_box(&s, sibling)], before);
|
||
s.undo().unwrap();
|
||
assert!(matches!(kind(&s, group), NodeKind::Group));
|
||
assert_eq!([page_box(&s, group), page_box(&s, sibling)], before);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn a_frame_becomes_a_group_around_its_layers() {
|
||
let mut s = Session::new();
|
||
let f = id(ex(
|
||
&mut s,
|
||
"frame.create",
|
||
json!({"x": 100, "y": 50, "width": 300, "height": 200, "name": "Card", "props": {"fill": "#FF0000", "cornerRadius": 8}}),
|
||
));
|
||
let (a, b) = (rect(&mut s, f, 20.0, 30.0), rect(&mut s, f, 120.0, 90.0));
|
||
ex(&mut s, "node.set", json!({"id": f, "props": {"opacity": 0.5}}));
|
||
let before = [page_box(&s, a), page_box(&s, b)];
|
||
let r = ex(&mut s, "object.convertToGroup", json!({"ids": [f]}));
|
||
assert_eq!(r["ids"], json!([f]));
|
||
let n = s.doc().unwrap().node(f).unwrap().clone();
|
||
assert!(matches!(n.kind, NodeKind::Group));
|
||
assert_eq!((n.name.as_str(), n.opacity, n.children.clone()), ("Card", 0.5, vec![a, b]), "the same layer: name, opacity, layers");
|
||
assert!(n.fills.is_empty() && n.strokes.is_empty() && !n.clip && n.corners.uniform() == Some(0.0), "what only a frame has is gone");
|
||
assert_eq!([page_box(&s, a), page_box(&s, b)], before, "the layers stay where they are");
|
||
// A group's box is its layers': (120, 80) to (260, 160).
|
||
assert_eq!(page_box(&s, f), [120.0, 80.0, 260.0, 160.0]);
|
||
ex(&mut s, "edit.undo", json!({}));
|
||
let n = s.doc().unwrap().node(f).unwrap();
|
||
assert!(matches!(n.kind, NodeKind::Frame) && n.fills.len() == 1 && n.clip, "one undo step brings the frame back");
|
||
assert_eq!(page_box(&s, f), [100.0, 50.0, 400.0, 250.0]);
|
||
}
|
||
|
||
#[test]
|
||
fn auto_layout_is_dropped_and_positions_kept() {
|
||
let mut s = Session::new();
|
||
let props = json!({"layout": "horizontal", "padding": 10, "gap": 10});
|
||
let f = id(ex(&mut s, "frame.create", json!({"x": 0, "y": 0, "width": 200, "height": 100, "props": props})));
|
||
let (a, b) = (rect(&mut s, f, 0.0, 0.0), rect(&mut s, f, 0.0, 0.0));
|
||
ex(&mut s, "node.set", json!({"id": b, "props": {"sizingH": "fill", "sizingV": "fill"}}));
|
||
// Laid out: a at (10, 10) 40 × 20, b fills (60, 10) to (190, 90).
|
||
let before = [page_box(&s, a), page_box(&s, b)];
|
||
assert_eq!(before, [[10.0, 10.0, 50.0, 30.0], [60.0, 10.0, 190.0, 90.0]]);
|
||
ex(&mut s, "object.convertToGroup", json!({"ids": [f]}));
|
||
let doc = s.doc().unwrap();
|
||
assert!(doc.node(f).unwrap().layout.is_none());
|
||
assert_eq!([page_box(&s, a), page_box(&s, b)], before, "the layout's positions and sizes are kept");
|
||
let cl = &doc.node(b).unwrap().child_layout;
|
||
assert_eq!((cl.sizing_h, cl.sizing_v), (Sizing::Fixed, Sizing::Fixed), "nothing to fill in a group");
|
||
// And back: a plain frame of the group's box, the layers still in place.
|
||
ex(&mut s, "object.convertToFrame", json!({"ids": [f]}));
|
||
let n = s.doc().unwrap().node(f).unwrap();
|
||
assert!(matches!(n.kind, NodeKind::Frame) && n.layout.is_none() && !n.clip && n.fills.is_empty());
|
||
assert_eq!((page_box(&s, f), [page_box(&s, a), page_box(&s, b)]), ([10.0, 10.0, 190.0, 90.0], before));
|
||
}
|
||
|
||
#[test]
|
||
fn a_rotated_group_becomes_a_frame_in_place() {
|
||
let mut s = Session::new();
|
||
let page = s.page().unwrap();
|
||
let (a, b) = (rect(&mut s, page, 10.0, 10.0), rect(&mut s, page, 80.0, 60.0));
|
||
let g = id(ex(&mut s, "object.group", json!({"ids": [a, b]})));
|
||
ex(&mut s, "node.set", json!({"id": g, "props": {"rotation": 30}}));
|
||
let before = [page_box(&s, a), page_box(&s, b), page_box(&s, g)];
|
||
ex(&mut s, "object.convertToFrame", json!({"ids": [g]}));
|
||
assert!(matches!(kind(&s, g), NodeKind::Frame));
|
||
assert_eq!([page_box(&s, a), page_box(&s, b), page_box(&s, g)], before);
|
||
// The layers now sit in the frame's space: a at its origin.
|
||
let c = s.doc().unwrap().node(a).unwrap().transform.as_coeffs();
|
||
assert!(c[4].abs() < 1e-6 && c[5].abs() < 1e-6, "{c:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn what_cannot_convert_is_refused_and_nothing_changes() {
|
||
let mut s = Session::new();
|
||
let page = s.page().unwrap();
|
||
let f = id(ex(&mut s, "frame.create", json!({"x": 0, "y": 0, "width": 100, "height": 100})));
|
||
let inner = id(ex(&mut s, "frame.create", json!({"x": 10, "y": 10, "width": 50, "height": 50, "parent": f})));
|
||
rect(&mut s, inner, 0.0, 0.0);
|
||
let comp = id(ex(&mut s, "component.create", json!({"ids": [f]})));
|
||
let inst = id(ex(&mut s, "instance.create", json!({"component": comp, "x": 300, "y": 0})));
|
||
let inst_inner = s.doc().unwrap().node(inst).unwrap().children[0];
|
||
let empty = id(ex(&mut s, "frame.create", json!({"x": 0, "y": 300, "width": 50, "height": 50})));
|
||
let loose = rect(&mut s, page, 500.0, 0.0);
|
||
let ok_frame = id(ex(&mut s, "frame.create", json!({"x": 0, "y": 500, "width": 50, "height": 50})));
|
||
rect(&mut s, ok_frame, 5.0, 5.0);
|
||
// (layer, what the message says)
|
||
for (layer, says) in [
|
||
(comp, "main component"),
|
||
(inst, "is an instance"),
|
||
(inst_inner, "inside an instance"),
|
||
(empty, "is empty"),
|
||
(loose, "neither a frame nor a group"),
|
||
] {
|
||
for cmd in ["object.convertToGroup", "object.convertToFrame"] {
|
||
if layer == empty && cmd == "object.convertToFrame" {
|
||
continue;
|
||
}
|
||
// Together with a frame that could convert: all or nothing.
|
||
let e = s.execute(cmd, &json!({"ids": [ok_frame, layer]})).unwrap_err().to_string();
|
||
assert!(e.contains(says), "{cmd} on {layer}: {e}");
|
||
assert!(matches!(kind(&s, ok_frame), NodeKind::Frame), "{cmd}: nothing converts when one can't");
|
||
}
|
||
}
|
||
// Already what was asked for: left alone, not an error.
|
||
assert_eq!(ex(&mut s, "object.convertToFrame", json!({"ids": [ok_frame]}))["ids"], json!([]));
|
||
// A frame inside a main component converts (its instances follow their main).
|
||
ex(&mut s, "object.convertToGroup", json!({"ids": [inner]}));
|
||
assert!(matches!(kind(&s, inner), NodeKind::Group));
|
||
}
|
||
|
||
#[test]
|
||
fn junk_parameters_are_refused_not_fatal() {
|
||
let junk = [json!(null), json!(true), json!(-1), json!(1e308), json!("x"), json!([[1]]), json!({"a": 1}), json!([0, 999_999, "7"])];
|
||
for cmd in ["object.convertToGroup", "object.convertToFrame", "object.convertToSection"] {
|
||
for j in &junk {
|
||
let mut s = Session::new();
|
||
let f = id(ex(&mut s, "frame.create", json!({"x": 0, "y": 0, "width": 50, "height": 50})));
|
||
rect(&mut s, f, 0.0, 0.0);
|
||
for p in [json!({"ids": j}), json!({"id": j}), j.clone()] {
|
||
let r = s.execute(cmd, &p);
|
||
assert!(!matches!(r, Err(EngineError::Internal { .. })), "{cmd} {p}: {r:?}");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn a_frame_on_the_page_becomes_a_section_and_back() {
|
||
let mut s = Session::new();
|
||
let f = id(ex(&mut s, "frame.create", json!({"x": 100, "y": 50, "width": 300, "height": 200, "name": "Flow", "props": {"fill": "#FFEEDD"}})));
|
||
let (a, b) = (rect(&mut s, f, 20.0, 30.0), rect(&mut s, f, 400.0, 90.0));
|
||
ex(&mut s, "layout.add", json!({"ids": [f], "direction": "horizontal"}));
|
||
ex(&mut s, "node.set", json!({"id": f, "props": {"width": 300, "height": 200}}));
|
||
let before = [page_box(&s, a), page_box(&s, b), page_box(&s, f)];
|
||
assert_eq!(ex(&mut s, "object.convertToSection", json!({"ids": [f]}))["ids"], json!([f]));
|
||
let n = s.doc().unwrap().node(f).unwrap().clone();
|
||
assert!(matches!(n.kind, NodeKind::Section { contents_hidden: false }));
|
||
assert_eq!((n.name.as_str(), n.children.clone(), n.fills.len()), ("Flow", vec![a, b], 1), "same name, layers and fill");
|
||
assert!(n.layout.is_none() && !n.clip, "no auto layout or clipping on a section");
|
||
assert_eq!([page_box(&s, a), page_box(&s, b), page_box(&s, f)], before, "nothing moves");
|
||
// And back: a frame that keeps the fill and doesn't clip, so the look stays.
|
||
assert_eq!(ex(&mut s, "object.convertToFrame", json!({"ids": [f]}))["ids"], json!([f]));
|
||
let n = s.doc().unwrap().node(f).unwrap().clone();
|
||
assert!(matches!(n.kind, NodeKind::Frame) && n.fills.len() == 1 && !n.clip);
|
||
assert_eq!([page_box(&s, a), page_box(&s, b), page_box(&s, f)], before);
|
||
// Each conversion is one undo step.
|
||
ex(&mut s, "edit.undo", json!({}));
|
||
assert!(matches!(kind(&s, f), NodeKind::Section { .. }));
|
||
ex(&mut s, "edit.undo", json!({}));
|
||
assert!(matches!(kind(&s, f), NodeKind::Frame));
|
||
assert!(s.doc().unwrap().node(f).unwrap().layout.is_some(), "the frame's auto layout is back");
|
||
}
|
||
|
||
#[test]
|
||
fn only_frames_on_the_page_or_in_a_section_become_sections() {
|
||
let mut s = Session::new();
|
||
let outer = id(ex(&mut s, "frame.create", json!({"x": 0, "y": 0, "width": 300, "height": 300})));
|
||
let inner = id(ex(&mut s, "frame.create", json!({"parent": outer, "x": 10, "y": 10, "width": 100, "height": 100})));
|
||
let err = s.execute("object.convertToSection", &json!({"ids": [inner]})).unwrap_err().to_string();
|
||
assert!(err.contains("inside a frame"), "{err}");
|
||
let r = rect(&mut s, outer, 150.0, 150.0);
|
||
assert!(s.execute("object.convertToSection", &json!({"ids": [r]})).is_err(), "a rectangle isn't a frame");
|
||
// A frame inside a section converts; a section holding a section can't become a frame.
|
||
let sec = id(ex(&mut s, "node.create", json!({"type": "section", "x": 500, "y": 0, "width": 400, "height": 400})));
|
||
let in_sec = id(ex(&mut s, "frame.create", json!({"parent": sec, "x": 20, "y": 20, "width": 100, "height": 100})));
|
||
ex(&mut s, "object.convertToSection", json!({"ids": [in_sec]}));
|
||
assert!(matches!(kind(&s, in_sec), NodeKind::Section { .. }));
|
||
let err = s.execute("object.convertToFrame", &json!({"ids": [sec]})).unwrap_err().to_string();
|
||
assert!(err.contains("holds sections"), "{err}");
|
||
assert!(matches!(kind(&s, sec), NodeKind::Section { .. }), "nothing changed");
|
||
}
|
||
|
||
#[test]
|
||
fn wrapping_errors_name_the_command_that_was_run() {
|
||
// Grouping, framing, auto layout, masks and booleans share one wrapping step; a refusal names
|
||
// the command that asked, not `object.group`.
|
||
let mut s = Session::new();
|
||
let page = s.page().unwrap();
|
||
for cmd in ["object.group", "object.frameSelection", "layout.add", "bool.union"] {
|
||
let err = s.execute(cmd, &json!({"ids": [page]})).unwrap_err().to_string();
|
||
assert!(err.contains(&format!("`{cmd}`")), "{cmd}: {err}");
|
||
}
|
||
let r = rect(&mut s, page, 0.0, 0.0);
|
||
let err = s.execute("object.mask", &json!({"ids": [page, r]})).unwrap_err().to_string();
|
||
assert!(err.contains("`object.mask`"), "{err}");
|
||
let section = id(ex(&mut s, "node.create", json!({"type": "section", "x": 300, "y": 0, "width": 100, "height": 100})));
|
||
ex(&mut s, "node.set", json!({"ids": [section], "props": {"fills": ["#FFFFFF"], "strokes": ["#000000"], "strokeWeight": 1}}));
|
||
let err = s.execute("vector.outlineStroke", &json!({"ids": [section]})).unwrap_err().to_string();
|
||
assert!(err.contains("`vector.outlineStroke`"), "{err}");
|
||
let f = id(ex(&mut s, "frame.create", json!({"x": 100, "y": 0, "width": 100, "height": 100})));
|
||
let inner = rect(&mut s, f, 10.0, 10.0);
|
||
let err = s.execute("object.wrapInSection", &json!({"ids": [inner, r]})).unwrap_err().to_string();
|
||
assert!(err.contains("`object.wrapInSection`"), "{err}");
|
||
}
|