Files
figcraft/crates/format/src/fig/source_identity_tests.rs
T
Skyfuller1303 4b29d8c34d M1: apply instance overrides by key and order, settle bound paints, keep stroke outline styles
Override rows of an instance can name a layer of its main by the key the layer
had before the main was edited. The key now resolves to the layer's current id
when it is unambiguous and the current id does not exist already. Rows of an
enclosing instance win over those of the instances nested in it, whatever order
the wire lists them in, and an explicit "no style" on an override detaches the
style instead of falling back to the older field.

Paints bound to variables take the colours of the active modes when a document
loads, before the first edit, without touching other pending changes.

A stroke-only vector that imports as the outline of its stroke now moves the
stroke's shared style and colour variables to the fill it becomes, and vectors
whose single segments carry their own stroke style say so in a warning.
2026-10-16 03:32:38 +02:00

1121 lines
50 KiB
Rust

//! Synthetic .fig source identity: cached library components, contextual sources, main switches
//! and invalid provenance.
use figcraft_doc::color::PaintKind;
use figcraft_doc::{Document, NodeId, NodeKind, PropDef};
use serde_json::{Value, json};
use super::kiwi::{Kv, Schema};
use super::*;
type Fields = Vec<(&'static str, Kv<'static>)>;
fn o(fields: Fields) -> Kv<'static> {
Kv::Object(fields)
}
fn g(id: u64) -> Kv<'static> {
o(vec![("sessionID", Kv::Uint(1)), ("localID", Kv::Uint(id))])
}
fn s(value: &str) -> Kv<'static> {
Kv::Str(value.into())
}
fn matrix(x: f32) -> Kv<'static> {
o(vec![
("m00", Kv::Float(1.0)),
("m01", Kv::Float(0.0)),
("m02", Kv::Float(x)),
("m10", Kv::Float(0.0)),
("m11", Kv::Float(1.0)),
("m12", Kv::Float(0.0)),
])
}
fn node(id: u64, parent: Option<u64>, ty: &'static str, name: &str, more: Fields) -> Kv<'static> {
let mut fields = vec![("guid", g(id)), ("phase", Kv::Enum("CREATED")), ("type", Kv::Enum(ty)), ("name", s(name))];
if let Some(parent) = parent {
fields.push(("parentIndex", o(vec![("guid", g(parent)), ("position", s(&format!("{id:04}")))])));
}
fields.extend(more);
o(fields)
}
fn symbol(main: u64, overrides: Vec<Kv<'static>>) -> Kv<'static> {
o(vec![("symbolID", g(main)), ("symbolOverrides", Kv::Array(overrides))])
}
fn row(path: &[u64], more: Fields) -> Kv<'static> {
let mut fields = vec![("guidPath", o(vec![("guids", Kv::Array(path.iter().map(|id| g(*id)).collect()))]))];
fields.extend(more);
o(fields)
}
fn solid(r: f32, g: f32, b: f32) -> Kv<'static> {
o(vec![
("type", Kv::Enum("SOLID")),
("color", o(vec![("r", Kv::Float(r)), ("g", Kv::Float(g)), ("b", Kv::Float(b)), ("a", Kv::Float(1.0))])),
("opacity", Kv::Float(1.0)),
("visible", Kv::Bool(true)),
])
}
fn schema() -> Schema {
Schema::parse(export::SCHEMA).unwrap()
}
fn cached_component_message() -> Kv<'static> {
o(vec![(
"nodeChanges",
Kv::Array(vec![
node(1, None, "DOCUMENT", "Document", vec![]),
node(2, Some(1), "CANVAS", "Visible page", vec![]),
node(
3,
Some(1),
"CANVAS",
"Remote library",
vec![
("internalOnly", Kv::Bool(true)),
(
"variableModeBySetMap",
o(vec![("entries", Kv::Array(vec![o(vec![("variableSetID", o(vec![("guid", g(30))])), ("variableModeID", g(52))])]))]),
),
],
),
node(
30,
Some(3),
"VARIABLE_SET",
"Remote theme",
vec![(
"variableSetModes",
Kv::Array(vec![
o(vec![("id", g(51)), ("name", s("Light")), ("sortPosition", s("!"))]),
o(vec![("id", g(52)), ("name", s("Dark")), ("sortPosition", s("\""))]),
]),
)],
),
node(
9,
Some(3),
"FRAME",
"Cached component set",
vec![("isStateGroup", Kv::Bool(true)), ("size", o(vec![("x", Kv::Float(24.0)), ("y", Kv::Float(24.0))])), ("transform", matrix(0.0))],
),
node(
10,
Some(9),
"SYMBOL",
"Cached remote icon",
vec![("size", o(vec![("x", Kv::Float(24.0)), ("y", Kv::Float(24.0))])), ("transform", matrix(0.0))],
),
node(
11,
Some(10),
"ROUNDED_RECTANGLE",
"Cached path",
vec![("size", o(vec![("x", Kv::Float(24.0)), ("y", Kv::Float(24.0))])), ("transform", matrix(0.0))],
),
node(
12,
Some(10),
"INSTANCE",
"Nested cached instance",
vec![
("size", o(vec![("x", Kv::Float(12.0)), ("y", Kv::Float(12.0))])),
("transform", matrix(0.0)),
("symbolData", symbol(22, vec![row(&[23], vec![("fillPaints", Kv::Array(vec![solid(0.2, 0.7, 0.1)]))])])),
],
),
node(
13,
Some(9),
"SYMBOL",
"Nested cached main",
vec![("size", o(vec![("x", Kv::Float(12.0)), ("y", Kv::Float(12.0))])), ("transform", matrix(0.0))],
),
node(
14,
Some(13),
"ROUNDED_RECTANGLE",
"Nested cached path",
vec![("size", o(vec![("x", Kv::Float(12.0)), ("y", Kv::Float(12.0))])), ("transform", matrix(0.0))],
),
node(
15,
Some(10),
"INSTANCE",
"Nested internal instance",
vec![
("size", o(vec![("x", Kv::Float(12.0)), ("y", Kv::Float(12.0))])),
("transform", matrix(0.0)),
("symbolData", symbol(13, vec![])),
],
),
node(
20,
Some(2),
"INSTANCE",
"Visible icon instance",
vec![
("size", o(vec![("x", Kv::Float(24.0)), ("y", Kv::Float(24.0))])),
("transform", matrix(0.0)),
("symbolData", symbol(10, vec![])),
],
),
node(
22,
Some(2),
"SYMBOL",
"Visible nested main",
vec![("size", o(vec![("x", Kv::Float(12.0)), ("y", Kv::Float(12.0))])), ("transform", matrix(0.0))],
),
node(
23,
Some(22),
"ROUNDED_RECTANGLE",
"Visible nested path",
vec![("size", o(vec![("x", Kv::Float(12.0)), ("y", Kv::Float(12.0))])), ("transform", matrix(0.0))],
),
]),
)])
}
fn cached_component_fixture() -> Vec<u8> {
let schema = schema();
super::tests::canvas(&schema, &cached_component_message())
}
fn cached_component_override_fixture() -> Vec<u8> {
let mut message = cached_component_message();
let Kv::Object(fields) = &mut message else { panic!("cached component fixture has an unexpected wire shape") };
let changes = &mut fields.iter_mut().find(|(key, _)| *key == "nodeChanges").unwrap().1;
let Kv::Array(nodes) = changes else { panic!("cached component fixture has an unexpected wire shape") };
let instance = nodes.iter_mut().find(|node| node.get("name").and_then(Kv::str) == Some("Visible icon instance")).unwrap();
let Kv::Object(fields) = instance else { panic!("cached component fixture has an unexpected wire shape") };
fields.iter_mut().find(|(key, _)| *key == "symbolData").unwrap().1 =
symbol(10, vec![row(&[12], vec![("symbolData", symbol(22, vec![row(&[26], vec![("overriddenSymbolID", g(24))])]))])]);
nodes.extend([
node(
24,
Some(3),
"SYMBOL",
"Overridden cached main",
vec![("size", o(vec![("x", Kv::Float(12.0)), ("y", Kv::Float(12.0))])), ("transform", matrix(0.0))],
),
node(
25,
Some(24),
"ROUNDED_RECTANGLE",
"Overridden cached path",
vec![("size", o(vec![("x", Kv::Float(12.0)), ("y", Kv::Float(12.0))])), ("transform", matrix(0.0))],
),
node(
26,
Some(22),
"INSTANCE",
"Deep cached instance",
vec![("size", o(vec![("x", Kv::Float(12.0)), ("y", Kv::Float(12.0))])), ("transform", matrix(0.0)), ("symbolData", symbol(27, vec![]))],
),
node(
27,
Some(3),
"SYMBOL",
"Base deep main",
vec![("size", o(vec![("x", Kv::Float(12.0)), ("y", Kv::Float(12.0))])), ("transform", matrix(0.0))],
),
node(
28,
Some(27),
"ROUNDED_RECTANGLE",
"Base deep path",
vec![("size", o(vec![("x", Kv::Float(12.0)), ("y", Kv::Float(12.0))])), ("transform", matrix(0.0))],
),
]);
super::tests::canvas(&schema(), &message)
}
fn cached_component_default_swap_fixture() -> Vec<u8> {
let mut message = cached_component_message();
let Kv::Object(fields) = &mut message else { panic!("cached component fixture has an unexpected wire shape") };
let changes = &mut fields.iter_mut().find(|(key, _)| *key == "nodeChanges").unwrap().1;
let Kv::Array(nodes) = changes else { panic!("cached component fixture has an unexpected wire shape") };
let owner = nodes.iter_mut().find(|node| node.get("name").and_then(Kv::str) == Some("Visible nested main")).unwrap();
let Kv::Object(fields) = owner else { panic!("cached component fixture has an unexpected wire shape") };
fields.push((
"componentPropDefs",
Kv::Array(vec![o(vec![
("id", g(90)),
("name", s("Icon")),
("type", Kv::Enum("INSTANCE_SWAP")),
("initialValue", o(vec![("guidValue", g(24))])),
])]),
));
nodes.extend([
node(
24,
Some(3),
"SYMBOL",
"Default cached main",
vec![("size", o(vec![("x", Kv::Float(12.0)), ("y", Kv::Float(12.0))])), ("transform", matrix(0.0))],
),
node(
25,
Some(24),
"ROUNDED_RECTANGLE",
"Default cached path",
vec![("size", o(vec![("x", Kv::Float(12.0)), ("y", Kv::Float(12.0))])), ("transform", matrix(0.0))],
),
]);
super::tests::canvas(&schema(), &message)
}
fn assert_default_swap(doc: &Document) {
let owner = named_node(doc, "Visible nested main");
let target = named_node(doc, "Default cached main");
let NodeKind::Component { props, .. } = &doc.node(owner).unwrap().kind else { panic!("visible component lost its property definition") };
assert_eq!(props.get("Icon"), Some(&PropDef::InstanceSwap { default: target, preferred: Vec::new() }), "{props:?}");
assert!(doc.is_remote(target));
}
fn named_node(doc: &Document, name: &str) -> NodeId {
doc.nodes().find(|node| node.name == name).unwrap().id
}
fn assert_dark_remote_mode(doc: &Document, node: NodeId) {
let collection = doc.variables.collections.iter().find(|collection| collection.name == "Remote theme").unwrap();
let dark = collection.modes.iter().find(|mode| mode.name == "Dark").unwrap();
assert_eq!(doc.modes_for(node).get(&collection.id), Some(&dark.id));
}
fn assert_no_linked_library_records(doc: &Document) {
assert!(doc.libraries.enabled.is_empty());
assert!(doc.libraries.components.is_empty());
assert!(doc.libraries.styles.is_empty());
assert!(doc.libraries.collections.is_empty());
assert!(doc.libraries.variables.is_empty());
}
#[test]
fn hidden_cached_component_is_retained_and_linked() {
let (doc, warnings) = import(&cached_component_fixture(), "Cached library.fig").unwrap();
assert!(warnings.is_empty(), "{warnings:?}");
assert_eq!(doc.pages.len(), 1, "hidden cache must not become a visible page");
let check = |doc: &Document| {
let store = doc.library_store().expect("private .fig cache is retained");
let component = named_node(doc, "Cached remote icon");
let path = doc.node(component).unwrap().children[0];
let set = doc.node(component).unwrap().parent.unwrap();
assert!(matches!(doc.node(set).unwrap().kind, NodeKind::ComponentSet { .. }));
assert_eq!(doc.node(doc.node(set).unwrap().parent.unwrap()).unwrap().parent, Some(store));
assert!(doc.is_remote(component) && doc.is_remote(path));
assert!(matches!(doc.node(component).unwrap().kind, NodeKind::Component { .. }));
assert_dark_remote_mode(doc, component);
assert_no_linked_library_records(doc);
// The visible instance resolves to the cached main, and its layer to the main's layer.
let instance = named_node(doc, "Visible icon instance");
assert!(matches!(doc.node(instance).unwrap().kind, NodeKind::Instance { component: linked, .. } if linked == component));
assert_eq!(doc.node(doc.node(instance).unwrap().children[0]).unwrap().source, Some(path));
// A cached main nested in another cached main is kept too, and so is an instance in the
// cache of a visible main, with the override its row carries.
let nested_main = named_node(doc, "Nested cached main");
let nested = named_node(doc, "Nested internal instance");
assert!(doc.is_remote(nested_main));
assert!(matches!(doc.node(nested).unwrap().kind, NodeKind::Instance { component: linked, .. } if linked == nested_main));
let external_main = named_node(doc, "Visible nested main");
let external = doc
.node(component)
.unwrap()
.children
.iter()
.copied()
.find(|id| doc.node(*id).is_some_and(|n| n.name == "Nested cached instance"))
.unwrap();
assert!(matches!(doc.node(external).unwrap().kind, NodeKind::Instance { component: linked, .. } if linked == external_main));
let external_child = doc.node(external).unwrap().children[0];
assert_eq!(doc.node(external_child).unwrap().source, Some(doc.node(external_main).unwrap().children[0]));
assert_eq!(fill_rgb_node(doc, external_child), Some([f64::from(0.2_f32), f64::from(0.7_f32), f64::from(0.1_f32)]));
};
check(&doc);
let (saved, warnings) = crate::load_bytes(&crate::save_bytes(&doc, None).unwrap()).unwrap();
assert!(warnings.is_empty(), "{warnings:?}");
check(&saved);
}
#[test]
fn visible_instance_override_retains_and_links_hidden_cached_main() {
let (doc, warnings) = import(&cached_component_override_fixture(), "Cached override.fig").unwrap();
assert!(warnings.is_empty(), "{warnings:?}");
let nested = named_node(&doc, "Nested cached instance");
let deep = named_node(&doc, "Deep cached instance");
assert_eq!(doc.node(deep).unwrap().parent, Some(nested));
let NodeKind::Instance { component: overridden, .. } = doc.node(deep).unwrap().kind else { panic!("nested-row override lost its instance") };
let overridden = doc.node(overridden).unwrap();
assert_eq!(overridden.name, "Overridden cached main");
assert!(doc.is_remote(overridden.id));
assert_eq!(doc.node(doc.node(deep).unwrap().children[0]).unwrap().source, Some(overridden.children[0]));
}
#[test]
fn visible_component_default_swap_retains_hidden_cached_main() {
let (doc, warnings) = import(&cached_component_default_swap_fixture(), "Cached default swap.fig").unwrap();
assert!(warnings.is_empty(), "{warnings:?}");
assert_default_swap(&doc);
let (saved, _) = crate::load_bytes(&crate::save_bytes(&doc, None).unwrap()).unwrap();
assert_default_swap(&saved);
}
#[test]
fn public_rest_import_ignores_private_fig_provenance() {
let value = json!({
"name": "Public REST",
"document": {"type": "DOCUMENT", "children": [{"type": "CANVAS", "name": "Page", "children": []}]},
"_figLibraryContexts": [{"type": "FRAME", "name": "Private cache", "children": [{"type": "COMPONENT", "name": "Must not import", "children": []}]}]
});
let (doc, warnings) = crate::rest::import(&value, &|_| None).unwrap();
assert!(warnings.is_empty(), "{warnings:?}");
assert!(doc.library_store().is_none() && doc.nodes().all(|node| node.name != "Must not import"));
// Valid provenance on mapped layers links nothing either.
for message in [message(false), external_namespace_message()] {
let (json, _) = super::map::to_rest(&message, "Identity");
let (doc, _) = crate::rest::import(&json, &|_| None).unwrap();
assert!(doc.nodes().all(|n| n.source.is_none()));
}
}
#[test]
fn the_last_change_of_a_guid_wins_and_removed_changes_delete() {
let moved = node(12, Some(11), "FRAME", "Moved frame", vec![]);
let mut removed = node(13, Some(10), "FRAME", "Removed frame", vec![]);
let Kv::Object(fields) = &mut removed else { panic!() };
fields.iter_mut().find(|(key, _)| *key == "phase").unwrap().1 = Kv::Enum("REMOVED");
let message = o(vec![(
"nodeChanges",
Kv::Array(vec![
node(1, None, "DOCUMENT", "Document", vec![]),
node(2, Some(1), "CANVAS", "Page", vec![]),
node(10, Some(2), "FRAME", "Left", vec![]),
node(11, Some(2), "FRAME", "Right", vec![]),
node(12, Some(10), "FRAME", "Moved frame", vec![]),
node(13, Some(10), "FRAME", "Removed frame", vec![]),
moved,
removed,
]),
)]);
let (doc, _) = import(&super::tests::canvas(&schema(), &message), "Changes.fig").unwrap();
assert_eq!(doc.node(named_node(&doc, "Moved frame")).unwrap().parent, Some(named_node(&doc, "Right")));
assert!(doc.nodes().all(|n| n.name != "Removed frame"));
assert!(doc.node(named_node(&doc, "Left")).unwrap().children.is_empty());
}
#[test]
fn internal_library_parent_cycles_are_bounded() {
let message = o(vec![(
"nodeChanges",
Kv::Array(vec![
node(1, None, "CANVAS", "Visible page", vec![]),
node(2, Some(1), "INSTANCE", "Visible cyclic main", vec![("symbolData", symbol(11, vec![]))]),
node(3, Some(9), "CANVAS", "Cyclic library", vec![("internalOnly", Kv::Bool(true))]),
node(9, Some(11), "FRAME", "Loop frame", vec![]),
node(10, Some(3), "SYMBOL", "Unrelated component", vec![]),
node(11, Some(3), "SYMBOL", "Loop component", vec![]),
]),
)]);
let (doc, warnings) = import(&super::tests::canvas(&schema(), &message), "Cyclic library.fig").unwrap();
assert!(warnings.iter().any(|warning| warning.contains("deeper than")), "{warnings:?}");
assert!(doc.node_count() < 1000, "the loop is cut at the nesting limit");
}
#[test]
fn parent_loops_through_the_internal_canvas_end() {
// The canvas is a child of a frame inside one of its own symbols, and the used main sits below
// that frame: the walk up from the main reaches the canvas again.
let message = o(vec![(
"nodeChanges",
Kv::Array(vec![
node(1, None, "CANVAS", "Visible page", vec![]),
node(2, Some(1), "INSTANCE", "Uses the main", vec![("symbolData", symbol(12, vec![]))]),
node(3, Some(9), "CANVAS", "Loop library", vec![("internalOnly", Kv::Bool(true))]),
node(9, Some(11), "FRAME", "Loop frame", vec![]),
node(11, Some(3), "SYMBOL", "Loop symbol", vec![]),
node(12, Some(9), "SYMBOL", "Used main", vec![]),
]),
)]);
let (doc, _) = import(&super::tests::canvas(&schema(), &message), "Loop.fig").unwrap();
assert!(doc.nodes().any(|n| n.name == "Used main"));
}
fn fill_rgb_node(doc: &Document, id: NodeId) -> Option<[f64; 3]> {
let node = doc.node(id)?;
match &node.fills.first()?.kind {
PaintKind::Solid { color } => Some([f64::from(color.r), f64::from(color.g), f64::from(color.b)]),
_ => None,
}
}
fn alias_schema() -> Schema {
let schema = export::SCHEMA.replace("message NodeChange GUID guid=1,", "message NodeChange GUID guid=1, GUID overrideKey=500,");
Schema::parse(&schema).unwrap()
}
fn alias(id: u64) -> Fields {
vec![("overrideKey", g(id))]
}
fn import_alias_message(message: Kv<'static>) -> Document {
let schema = alias_schema();
let bytes = super::tests::canvas(&schema, &message);
let (doc, warnings) = import(&bytes, "Alias.fig").unwrap();
assert!(warnings.is_empty(), "{warnings:?}");
doc
}
fn fill_rgb(doc: &Document, name: &str) -> Option<[f64; 3]> {
let node = doc.nodes().find(|n| n.name == name)?;
fill_rgb_node(doc, node.id)
}
fn alias_priority_message() -> Kv<'static> {
let nested_local = symbol(10, vec![row(&[112], vec![("fillPaints", Kv::Array(vec![solid(0.0, 0.0, 1.0)]))])]);
o(vec![(
"nodeChanges",
Kv::Array(vec![
node(1, None, "DOCUMENT", "Document", vec![]),
node(2, Some(1), "CANVAS", "Page", vec![]),
node(10, Some(2), "SYMBOL", "Inner main", vec![]),
node(11, Some(10), "FRAME", "Normal frame", vec![]),
node(12, Some(11), "ROUNDED_RECTANGLE", "Alias leaf", alias(112)),
node(20, Some(2), "SYMBOL", "Outer main", vec![]),
node(
21,
Some(20),
"INSTANCE",
"Nested slot",
vec![
("overrideKey", g(221)),
("symbolData", nested_local),
("derivedSymbolData", Kv::Array(vec![row(&[112], vec![("opacity", Kv::Float(0.25))])])),
],
),
node(
30,
Some(2),
"INSTANCE",
"Alias copy",
vec![
(
"symbolData",
symbol(
20,
vec![
row(&[221, 112], vec![("fillPaints", Kv::Array(vec![solid(0.0, 1.0, 0.0)]))]),
row(&[21, 12], vec![("fillPaints", Kv::Array(vec![solid(1.0, 0.0, 0.0)]))]),
],
),
),
("derivedSymbolData", Kv::Array(vec![row(&[221, 112], vec![("fillPaints", Kv::Array(vec![solid(1.0, 0.5, 0.0)]))])])),
],
),
]),
)])
}
#[test]
fn override_aliases_normalize_lazily_and_keep_outer_wire_order() {
let doc = import_alias_message(alias_priority_message());
let main = named(&doc, "Inner main");
let leaf = child(&doc, child(&doc, main, 0), 0);
let outer_main = named(&doc, "Outer main");
let main_slot = child(&doc, outer_main, 0);
let main_leaf = child(&doc, child(&doc, main_slot, 0), 0);
let copy = named(&doc, "Alias copy");
let nested = child(&doc, copy, 0);
let frame = child(&doc, nested, 0);
let expanded_leaf = child(&doc, frame, 0);
assert_eq!(doc.node(main_leaf).unwrap().source, Some(leaf));
assert_eq!(doc.node(expanded_leaf).unwrap().source, Some(main_leaf));
assert_eq!(fill_rgb_node(&doc, expanded_leaf), Some([1.0, 0.5, 0.0]));
assert_eq!(doc.node(expanded_leaf).unwrap().opacity, 0.25);
assert_eq!(fill_rgb_node(&doc, leaf), None, "source node stays unchanged");
}
#[test]
fn selected_swapped_main_owns_alias_resolution_scope() {
let message = o(vec![(
"nodeChanges",
Kv::Array(vec![
node(1, None, "DOCUMENT", "Document", vec![]),
node(2, Some(1), "CANVAS", "Page", vec![]),
node(10, Some(2), "SYMBOL", "Old main", vec![]),
node(11, Some(10), "ROUNDED_RECTANGLE", "Old leaf", alias(112)),
node(20, Some(2), "SYMBOL", "Swapped main", vec![]),
node(21, Some(20), "ROUNDED_RECTANGLE", "Swapped leaf", alias(112)),
node(
30,
Some(2),
"INSTANCE",
"Swap",
vec![
("overriddenSymbolID", g(20)),
("symbolData", symbol(10, vec![row(&[112], vec![("fillPaints", Kv::Array(vec![solid(1.0, 0.0, 0.0)]))])])),
],
),
]),
)]);
let doc = import_alias_message(message);
let copy = named(&doc, "Swap");
let source = named(&doc, "Swapped leaf");
let expanded = child(&doc, copy, 0);
assert_eq!(doc.node(expanded).unwrap().source, Some(source));
assert_eq!(fill_rgb_node(&doc, expanded), Some([1.0, 0.0, 0.0]));
assert_eq!(fill_rgb(&doc, "Old leaf"), None);
}
#[test]
fn ambiguous_alias_is_ignored_but_current_guid_still_matches() {
let message = o(vec![(
"nodeChanges",
Kv::Array(vec![
node(1, None, "DOCUMENT", "Document", vec![]),
node(2, Some(1), "CANVAS", "Page", vec![]),
node(10, Some(2), "SYMBOL", "Main", vec![]),
node(11, Some(10), "ROUNDED_RECTANGLE", "First", alias(13)),
node(12, Some(10), "ROUNDED_RECTANGLE", "Second", alias(112)),
node(13, Some(10), "ROUNDED_RECTANGLE", "Current GUID", alias(112)),
node(
20,
Some(2),
"INSTANCE",
"Copy",
vec![(
"symbolData",
symbol(
10,
vec![
row(&[112], vec![("fillPaints", Kv::Array(vec![solid(1.0, 0.0, 0.0)]))]),
row(&[13], vec![("fillPaints", Kv::Array(vec![solid(0.0, 0.0, 1.0)]))]),
],
),
)],
),
]),
)]);
let doc = import_alias_message(message);
let copy = named(&doc, "Copy");
let first = child(&doc, copy, 0);
let second = child(&doc, copy, 1);
let current = child(&doc, copy, 2);
assert_eq!(doc.node(first).unwrap().source, Some(named(&doc, "First")));
assert_eq!(fill_rgb_node(&doc, first), None, "ambiguous alias must not be guessed");
assert_eq!(doc.node(second).unwrap().source, Some(named(&doc, "Second")));
assert_eq!(fill_rgb_node(&doc, second), None, "ambiguous alias must not match either target");
assert_eq!(doc.node(current).unwrap().source, Some(named(&doc, "Current GUID")));
assert_eq!(fill_rgb_node(&doc, current), Some([0.0, 0.0, 1.0]), "a current GUID outranks an alias with the same key");
}
fn message(swapped: bool) -> Kv<'static> {
let shape = |id, parent, x| node(id, Some(parent), "ROUNDED_RECTANGLE", "Same layer", vec![("transform", matrix(x))]);
let mut changes = vec![
node(1, None, "DOCUMENT", "Identity", vec![]),
node(2, Some(1), "CANVAS", "Page", vec![]),
node(10, Some(2), "SYMBOL", "Renamed Left main", vec![]),
node(11, Some(10), "FRAME", "Frame", vec![("transform", matrix(2.0))]),
shape(12, 11, 2.0),
shape(13, 11, 4.0),
shape(14, 11, 6.0),
node(15, Some(11), "FRAME", "Deep frame", vec![]),
node(16, Some(15), "FRAME", "Deeper frame", vec![]),
shape(17, 16, 8.0),
node(
20,
Some(2),
"SYMBOL",
"State=Right, Renamed=Yes",
vec![(
"componentPropDefs",
Kv::Array(vec![o(vec![
("id", g(200)),
("name", s("Enabled")),
("type", Kv::Enum("BOOL")),
("initialValue", o(vec![("boolValue", Kv::Bool(true))])),
])]),
)],
),
node(21, Some(20), "FRAME", "Frame", vec![("transform", matrix(if swapped { 23.0 } else { 2.0 }))]),
shape(22, 21, 2.0),
shape(23, 21, 4.0),
shape(24, 21, 6.0),
node(30, Some(2), "SYMBOL", "Outer", vec![]),
node(40, Some(30), "INSTANCE", "Original wrapper", vec![("symbolData", symbol(50, vec![row(&[60, 21], vec![("transform", matrix(3.0))])]))]),
node(50, Some(2), "SYMBOL", "Renamed wrapper main", vec![]),
node(
60,
Some(50),
"INSTANCE",
"Same nested slot",
vec![
("symbolData", symbol(20, vec![])),
("visible", Kv::Bool(false)),
("opacity", Kv::Float(0.3)),
("mask", Kv::Bool(true)),
("componentPropAssignments", Kv::Array(vec![o(vec![("defID", g(200)), ("value", o(vec![("boolValue", Kv::Bool(false))]))])])),
],
),
node(61, Some(50), "INSTANCE", "Same nested slot", vec![("symbolData", symbol(20, vec![]))]),
];
let mut nested = vec![
("name", s("Renamed nested slot")),
("visible", Kv::Bool(true)),
("opacity", Kv::Float(1.0)),
("mask", Kv::Bool(false)),
("componentPropAssignments", Kv::Array(vec![])),
];
if swapped {
nested.push(("overriddenSymbolID", g(10)));
}
changes.push(node(
70,
Some(2),
"INSTANCE",
"Copy",
vec![(
"symbolData",
symbol(
30,
vec![
row(&[40], vec![("name", s("Renamed wrapper"))]),
row(&[40, 60], nested),
row(&[40, 60, if swapped { 11 } else { 21 }], vec![("transform", matrix(2.0))]),
],
),
)],
));
o(vec![("nodeChanges", Kv::Array(changes))])
}
fn import_message(swapped: bool) -> Document {
let schema = Schema::parse(export::SCHEMA).unwrap();
let bytes = super::tests::canvas(&schema, &message(swapped));
let (doc, notes) = import(&bytes, "Identity.fig").unwrap();
assert!(notes.is_empty(), "{notes:?}");
doc
}
fn named(doc: &Document, name: &str) -> NodeId {
doc.nodes().find(|n| n.name == name).unwrap().id
}
fn child(doc: &Document, id: NodeId, index: usize) -> NodeId {
doc.node(id).unwrap().children[index]
}
fn assert_contextual(doc: &Document) {
let outer = named(doc, "Outer");
let right = named(doc, "State=Right, Renamed=Yes");
let wrapper_main = named(doc, "Renamed wrapper main");
let copy = named(doc, "Copy");
let main_wrapper = child(doc, outer, 0);
let copy_wrapper = child(doc, copy, 0);
assert_eq!(doc.node(copy).unwrap().source, Some(outer));
assert_eq!(doc.node(copy_wrapper).unwrap().source, Some(main_wrapper));
assert!(matches!(&doc.node(copy_wrapper).unwrap().kind, NodeKind::Instance {component, ..} if *component == wrapper_main));
for index in 0..2 {
let default_slot = child(doc, main_wrapper, index);
let copy_slot = child(doc, copy_wrapper, index);
assert_eq!(doc.node(copy_slot).unwrap().source, Some(default_slot), "slot {index}");
assert!(matches!(&doc.node(default_slot).unwrap().kind, NodeKind::Instance {component, ..} if *component == right));
assert!(matches!(&doc.node(copy_slot).unwrap().kind, NodeKind::Instance {component, ..} if *component == right));
}
let default_slot = child(doc, main_wrapper, 0);
let copy_slot = child(doc, copy_wrapper, 0);
let inherited = child(doc, default_slot, 0);
let copy_frame = child(doc, copy_slot, 0);
assert!(matches!(doc.node(copy_frame).unwrap().kind, NodeKind::Frame));
assert_eq!(doc.node(inherited).unwrap().transform.translation().x, 3.0);
assert_eq!(doc.node(copy_frame).unwrap().transform.translation().x, 2.0);
assert_eq!(doc.node(copy_frame).unwrap().source, Some(inherited));
for index in 0..3 {
assert_eq!(doc.node(child(doc, copy_frame, index)).unwrap().source, Some(child(doc, inherited, index)));
assert_eq!(doc.node(child(doc, copy_frame, index)).unwrap().transform.translation().x, 2.0 + 2.0 * index as f64);
}
let a = doc.node(default_slot).unwrap();
let b = doc.node(copy_slot).unwrap();
assert!(!a.visible && a.is_mask && a.opacity < 1.0);
assert!(b.visible && !b.is_mask && b.opacity == 1.0);
assert!(matches!(&a.kind, NodeKind::Instance {props, ..} if props.len() == 1 && props.values().all(|p| p == &json!(false))));
assert!(matches!(&b.kind, NodeKind::Instance {props, ..} if props.is_empty()));
}
#[test]
fn contextual_sources_preserve_inherited_geometry_and_explicit_clears() {
assert_contextual(&import_message(false));
}
fn assert_swapped(doc: &Document) {
let outer = named(doc, "Outer");
let right = named(doc, "State=Right, Renamed=Yes");
let copy = named(doc, "Copy");
let main_wrapper = child(doc, outer, 0);
let copy_wrapper = child(doc, copy, 0);
let copy_slot = child(doc, copy_wrapper, 0);
let default_slot = child(doc, main_wrapper, 0);
let left = named(doc, "Renamed Left main");
assert_eq!(doc.node(copy).unwrap().source, Some(outer));
assert_eq!(doc.node(copy_wrapper).unwrap().source, Some(main_wrapper));
assert_eq!(doc.node(copy_slot).unwrap().source, Some(default_slot));
assert!(matches!(&doc.node(copy_slot).unwrap().kind, NodeKind::Instance {component, props} if *component == left && props.is_empty()));
assert!(matches!(&doc.node(default_slot).unwrap().kind, NodeKind::Instance {component, ..} if *component == right));
let current_frame = child(doc, copy_slot, 0);
let left_frame = child(doc, left, 0);
assert!(matches!(doc.node(current_frame).unwrap().kind, NodeKind::Frame));
assert_eq!(doc.node(current_frame).unwrap().source, Some(left_frame));
assert_eq!(doc.node(current_frame).unwrap().transform.translation().x, 2.0);
assert_eq!(doc.node(child(doc, right, 0)).unwrap().transform.translation().x, 23.0);
for index in 0..3 {
assert_eq!(doc.node(child(doc, current_frame, index)).unwrap().source, Some(child(doc, left_frame, index)));
assert_eq!(doc.node(child(doc, current_frame, index)).unwrap().transform.translation().x, 2.0 + 2.0 * index as f64);
}
let deep_copy = child(doc, child(doc, child(doc, current_frame, 3), 0), 0);
let deep_main = child(doc, child(doc, child(doc, left_frame, 3), 0), 0);
assert_eq!(doc.node(deep_copy).unwrap().source, Some(deep_main));
assert_eq!(doc.node(deep_copy).unwrap().transform.translation().x, 8.0);
let other = child(doc, copy_wrapper, 1);
assert_eq!(doc.node(other).unwrap().source, Some(child(doc, main_wrapper, 1)));
assert!(matches!(&doc.node(other).unwrap().kind, NodeKind::Instance {component, ..} if *component == right));
assert!(doc.node(copy_slot).unwrap().visible && !doc.node(copy_slot).unwrap().is_mask && doc.node(copy_slot).unwrap().opacity == 1.0);
}
#[test]
fn swapped_children_use_current_main_through_frames_and_multiple_siblings() {
assert_swapped(&import_message(true));
}
fn edit_node(value: &mut Value, id: &str, edit: &mut dyn FnMut(&mut Value)) {
if value.get("id").and_then(Value::as_str) == Some(id) {
edit(value);
}
if let Some(kids) = value.get_mut("children").and_then(Value::as_array_mut) {
for child in kids {
edit_node(child, id, edit);
}
}
}
#[test]
fn missing_foreign_self_and_wrong_namespace_provenance_is_not_guessed() {
for (origin, id) in [("1:99999", "I1:70;1:40"), ("1:12", "I1:70;1:40"), ("1:70", "1:70"), ("1:40", "I1:70;1:40")] {
let (mut json, _) = super::map::to_rest(&message(false), "Identity");
edit_node(&mut json["document"]["children"][0], id, &mut |node| {
node[crate::rest::FIG_SOURCE_FIELD]["origin"] = json!(origin);
if id == "1:70" {
node["componentId"] = json!("1:70"); // a root cannot be its own component
} else if origin == "1:40" {
node["id"] = json!("I1:999;1:40");
} else {
node["id"] = json!(format!("I1:70;{origin}")); // valid namespace, but missing/foreign target
}
});
let (doc, _) = crate::rest::import_fig(&json, &|_| None).unwrap();
let target = if id == "1:70" { named(&doc, "Copy") } else { named(&doc, "Renamed wrapper") };
assert!(doc.node(target).unwrap().source.is_none(), "{origin}/{id}");
}
}
#[test]
fn forward_main_references_and_duplicate_ids_are_conservative() {
let (mut json, _) = super::map::to_rest(&message(false), "Identity");
json["document"]["children"][0]["children"].as_array_mut().unwrap().rotate_right(1);
let (doc, _) = crate::rest::import_fig(&json, &|_| None).unwrap();
assert!(doc.node(named(&doc, "Copy")).unwrap().source.is_some());
// 1:40 is nested inside Outer, so duplicate that actual template at page level.
let template = json["document"]["children"][0]["children"].as_array().unwrap().iter().find(|n| n["id"] == "1:30").unwrap()["children"][0].clone();
json["document"]["children"][0]["children"].as_array_mut().unwrap().push(template);
let (doc, _) = crate::rest::import_fig(&json, &|_| None).unwrap();
assert!(doc.node(named(&doc, "Renamed wrapper")).unwrap().source.is_none());
}
#[test]
fn missing_contextual_targets_do_not_fall_back_to_a_global_slot() {
let (mut json, _) = super::map::to_rest(&message(false), "Identity");
edit_node(&mut json["document"]["children"][0], "1:40", &mut |node| {
node["children"].as_array_mut().unwrap().retain(|n| n["id"] != "I1:40;1:60");
});
let (doc, _) = crate::rest::import_fig(&json, &|_| None).unwrap();
let copy_slot = child(&doc, child(&doc, named(&doc, "Copy"), 0), 0);
assert!(doc.node(copy_slot).unwrap().source.is_none());
assert!(doc.nodes().any(|n| n.name == "Same nested slot"), "the raw slot still exists");
}
#[test]
fn instance_to_empty_frame_keeps_its_source_identity() {
let mut msg = message(false);
let Kv::Object(top) = &mut msg else { panic!() };
let Kv::Array(nodes) = &mut top.iter_mut().find(|(key, _)| *key == "nodeChanges").unwrap().1 else { panic!() };
let copy = nodes.iter_mut().find(|n| n.get("name").and_then(Kv::str) == Some("Copy")).unwrap();
let Kv::Object(fields) = copy else { panic!() };
let Kv::Object(symbol) = &mut fields.iter_mut().find(|(key, _)| *key == "symbolData").unwrap().1 else { panic!() };
let Kv::Array(overrides) = &mut symbol.iter_mut().find(|(key, _)| *key == "symbolOverrides").unwrap().1 else { panic!() };
overrides.push(row(&[40, 61], vec![("type", Kv::Enum("FRAME"))]));
let (doc, _) = import(&super::tests::canvas(&schema(), &msg), "Identity.fig").unwrap();
let main = child(&doc, named(&doc, "Outer"), 0);
let copy = child(&doc, named(&doc, "Copy"), 0);
let frame = child(&doc, copy, 1);
assert!(matches!(doc.node(frame).unwrap().kind, NodeKind::Frame));
assert!(doc.node(frame).unwrap().children.is_empty());
assert_eq!(doc.node(frame).unwrap().source, Some(child(&doc, main, 1)));
}
#[test]
fn explicit_frame_to_instance_resets_children_to_the_resolved_main() {
let mut msg = message(false);
let Kv::Object(top) = &mut msg else { panic!() };
let Kv::Array(nodes) = &mut top.iter_mut().find(|(key, _)| *key == "nodeChanges").unwrap().1 else { panic!() };
nodes.push(node(80, Some(30), "FRAME", "Original frame", vec![]));
nodes.push(node(81, Some(80), "ROUNDED_RECTANGLE", "Old frame child", vec![]));
let copy = nodes.iter_mut().find(|n| n.get("name").and_then(Kv::str) == Some("Copy")).unwrap();
let Kv::Object(fields) = copy else { panic!() };
let Kv::Object(symbol) = &mut fields.iter_mut().find(|(key, _)| *key == "symbolData").unwrap().1 else { panic!() };
let Kv::Array(overrides) = &mut symbol.iter_mut().find(|(key, _)| *key == "symbolOverrides").unwrap().1 else { panic!() };
overrides.push(row(&[80], vec![("type", Kv::Enum("INSTANCE")), ("overriddenSymbolID", g(10))]));
let (doc, notes) = import(&super::tests::canvas(&schema(), &msg), "Identity.fig").unwrap();
assert!(notes.is_empty(), "{notes:?}");
let original = child(&doc, named(&doc, "Outer"), 1);
let current = child(&doc, named(&doc, "Copy"), 1);
let left = named(&doc, "Renamed Left main");
assert!(matches!(doc.node(original).unwrap().kind, NodeKind::Frame));
assert_eq!(doc.node(current).unwrap().source, Some(original));
assert!(matches!(&doc.node(current).unwrap().kind, NodeKind::Instance {component, ..} if *component == left));
assert_eq!(doc.node(child(&doc, current, 0)).unwrap().source, Some(child(&doc, left, 0)));
}
// The orphan symbol is decoded and expanded but is not a layer on any imported page. This
// matches an external main: its instance becomes a frame while the rendered body survives.
fn external_namespace_message() -> Kv<'static> {
o(vec![(
"nodeChanges",
Kv::Array(vec![
node(1, None, "DOCUMENT", "External namespace", vec![]),
node(2, Some(1), "CANVAS", "Page", vec![]),
node(
10,
Some(2),
"SYMBOL",
"Known main",
vec![(
"componentPropDefs",
Kv::Array(vec![o(vec![
("id", g(200)),
("name", s("Enabled")),
("type", Kv::Enum("BOOL")),
("initialValue", o(vec![("boolValue", Kv::Bool(true))])),
])]),
)],
),
node(11, Some(10), "FRAME", "Known frame", vec![("transform", matrix(2.0))]),
node(12, Some(11), "ROUNDED_RECTANGLE", "Known leaf", vec![("transform", matrix(4.0))]),
node(30, Some(2), "SYMBOL", "Outer external", vec![]),
node(
90,
Some(30),
"INSTANCE",
"External body",
vec![("symbolData", symbol(99, vec![row(&[110, 11], vec![("transform", matrix(3.0))])]))],
),
node(99, None, "SYMBOL", "External orphan", vec![]),
node(100, Some(99), "FRAME", "Body frame", vec![]),
node(101, Some(99), "FRAME", "Other body frame", vec![]),
node(
110,
Some(100),
"INSTANCE",
"Same slot",
vec![
("symbolData", symbol(10, vec![])),
("visible", Kv::Bool(false)),
("opacity", Kv::Float(0.3)),
("mask", Kv::Bool(true)),
("componentPropAssignments", Kv::Array(vec![o(vec![("defID", g(200)), ("value", o(vec![("boolValue", Kv::Bool(false))]))])])),
],
),
node(111, Some(100), "INSTANCE", "Same slot", vec![("symbolData", symbol(10, vec![]))]),
node(
70,
Some(2),
"INSTANCE",
"External copy",
vec![(
"symbolData",
symbol(
30,
vec![
row(
&[90, 110],
vec![
("name", s("Renamed owned slot")),
("visible", Kv::Bool(true)),
("opacity", Kv::Float(1.0)),
("mask", Kv::Bool(false)),
("componentPropAssignments", Kv::Array(vec![])),
],
),
row(&[90, 110, 11], vec![("transform", matrix(2.0))]),
],
),
)],
),
]),
)])
}
fn assert_external_namespace(doc: &Document) {
let outer = named(doc, "Outer external");
let copy = named(doc, "External copy");
let main = named(doc, "Known main");
let body = child(doc, outer, 0);
let copied_body = child(doc, copy, 0);
assert!(matches!(doc.node(body).unwrap().kind, NodeKind::Frame));
assert!(matches!(doc.node(copied_body).unwrap().kind, NodeKind::Frame));
assert_eq!(doc.node(body).unwrap().source, None, "the absent external main must not acquire a guessed native node");
assert_eq!(doc.node(copied_body).unwrap().source, Some(body));
let frame = child(doc, body, 0);
let copied_frame = child(doc, copied_body, 0);
assert_eq!(doc.node(frame).unwrap().source, None, "materialized ordinary layers are owned by the frame");
assert_eq!(doc.node(copied_frame).unwrap().source, Some(frame));
for index in 0..2 {
let slot = child(doc, frame, index);
let copied_slot = child(doc, copied_frame, index);
assert_eq!(doc.node(slot).unwrap().source, Some(main), "known owned instance has its real root main");
assert_eq!(doc.node(copied_slot).unwrap().source, Some(slot), "copied slots preserve the contextual owned layer identity");
assert!(matches!(&doc.node(slot).unwrap().kind, NodeKind::Instance {component, ..} if *component == main));
assert!(matches!(&doc.node(copied_slot).unwrap().kind, NodeKind::Instance {component, ..} if *component == main));
let inherited = child(doc, slot, 0);
let current = child(doc, copied_slot, 0);
assert_eq!(doc.node(inherited).unwrap().source, Some(child(doc, main, 0)));
assert_eq!(doc.node(current).unwrap().source, Some(inherited));
assert_eq!(doc.node(inherited).unwrap().transform.translation().x, if index == 0 { 3.0 } else { 2.0 });
assert_eq!(doc.node(current).unwrap().transform.translation().x, 2.0);
assert_eq!(doc.node(child(doc, current, 0)).unwrap().source, Some(child(doc, inherited, 0)));
assert_eq!(doc.node(child(doc, current, 0)).unwrap().transform.translation().x, 4.0);
}
let old = doc.node(child(doc, frame, 0)).unwrap();
let current = doc.node(child(doc, copied_frame, 0)).unwrap();
assert!(!old.visible && old.is_mask && old.opacity < 1.0);
assert!(current.visible && !current.is_mask && current.opacity == 1.0);
assert_eq!(current.name, "Renamed owned slot");
assert!(matches!(&old.kind, NodeKind::Instance {props, ..} if props.len() == 1));
assert!(matches!(&current.kind, NodeKind::Instance {props, ..} if props.is_empty()));
}
#[test]
fn absent_external_mains_keep_known_owned_instances_and_contextual_copies() {
let (doc, notes) = import(&super::tests::canvas(&schema(), &external_namespace_message()), "External.fig").unwrap();
assert!(notes.iter().any(|w| w.contains("components that aren't in the file")), "{notes:?}");
assert!(!notes.iter().any(|w| w.contains("source link")), "materialized own layers are not unresolved copies: {notes:?}");
assert_external_namespace(&doc);
}
#[test]
fn external_namespace_is_private_and_requires_actual_wire_instances() {
for mode in 0..3 {
let (mut json, _) = super::map::to_rest(&external_namespace_message(), "External");
if mode == 0 {
let (doc, _) = crate::rest::import(&json, &|_| None).unwrap();
assert!(doc.nodes().all(|n| n.source.is_none()));
continue;
}
edit_node(&mut json["document"]["children"][0], "1:90", &mut |node| {
if mode == 1 {
node["type"] = json!("FRAME");
} else {
node[crate::rest::FIG_SOURCE_FIELD]["wireInstance"] = json!(false);
node[crate::rest::FIG_SOURCE_FIELD]["wireFrame"] = json!(true);
}
});
let (doc, _) = crate::rest::import_fig(&json, &|_| None).unwrap();
let raw_slot = child(&doc, child(&doc, child(&doc, named(&doc, "Outer external"), 0), 0), 0);
assert!(doc.node(raw_slot).unwrap().source.is_none(), "ordinary/fake frame mode {mode} must not establish a namespace");
}
}
#[test]
fn external_namespace_does_not_cover_present_wrong_kind_or_blocked_main() {
for mode in 0..3 {
let (mut json, _) = super::map::to_rest(&external_namespace_message(), "External");
let page = &mut json["document"]["children"][0];
match mode {
0 => page["children"].as_array_mut().unwrap().push(json!({"id":"1:99", "type":"FRAME", "name":"Wrong kind",
"_figSource":{"origin":"1:99","expanded":false,"wireFrame":true,"wireInstance":false}})),
1 => edit_node(page, "1:90", &mut |node| {
node["componentId"] = json!("1:30");
}),
_ => page["children"].as_array_mut().unwrap().push(json!({"id":"1:99", "type":"COMPONENT", "name":"Cycle main",
"_figSource":{"origin":"1:99","expanded":false,"wireFrame":false,"wireInstance":false},
"children":[{"id":"1:199", "type":"INSTANCE", "name":"Cycle edge", "componentId":"1:30",
"_figSource":{"origin":"1:199","expanded":false,"wireFrame":false,"wireInstance":true}}]})),
}
let (doc, _) = crate::rest::import_fig(&json, &|_| None).unwrap();
let body = child(&doc, named(&doc, "Outer external"), 0);
assert!(matches!(doc.node(body).unwrap().kind, NodeKind::Frame));
let slot = child(&doc, child(&doc, body, 0), 0);
assert!(doc.node(slot).unwrap().source.is_none(), "existing invalid/cycle/self main mode {mode} must not become a materialized namespace");
}
}
#[test]
fn external_namespace_rejects_foreign_siblings_duplicates_and_bad_paths() {
for mode in 0..4 {
let (mut json, _) = super::map::to_rest(&external_namespace_message(), "External");
let page = &mut json["document"]["children"][0];
match mode {
0 => {
// Same root namespace and GUID, but physically under a different frame slot.
let mut moved = None;
edit_node(page, "I1:70;1:90;1:100", &mut |node| {
moved = Some(node["children"].as_array_mut().unwrap().remove(0));
});
edit_node(page, "I1:70;1:90;1:101", &mut |node| {
node["children"] = json!([moved.take().unwrap()]);
});
}
1 => edit_node(page, "I1:70;1:90;1:110", &mut |node| {
node["id"] = json!("I1:70;1:999;1:110");
}),
2 => {
let duplicate = page["children"].as_array().unwrap().iter().find(|n| n["id"] == "1:30").unwrap()["children"][0].clone();
page["children"].as_array_mut().unwrap().push(duplicate);
}
_ => edit_node(page, "I1:90;1:100", &mut |node| {
node["id"] = json!("I1:999;1:100");
}),
}
let (doc, warnings) = crate::rest::import_fig(&json, &|_| None).unwrap();
let slot = named(&doc, "Renamed owned slot");
assert!(doc.node(slot).unwrap().source.is_none(), "invalid frame namespace mode {mode}");
assert!(warnings.iter().any(|w| w.contains("source link")), "invalid expanded paths remain reported: {warnings:?}");
}
}
fn no_style() -> Kv<'static> {
o(vec![("sessionID", Kv::Uint(4_294_967_295)), ("localID", Kv::Uint(4_294_967_295))])
}
#[test]
fn an_explicit_no_style_detaches_and_beats_the_older_style_field() {
let message = o(vec![(
"nodeChanges",
Kv::Array(vec![
node(1, None, "DOCUMENT", "Document", vec![]),
node(2, Some(1), "CANVAS", "Page", vec![]),
node(5, Some(2), "RECTANGLE", "Red", vec![("styleType", Kv::Enum("FILL"))]),
node(6, Some(2), "RECTANGLE", "Box", vec![("styleIdForFill", o(vec![("guid", no_style())])), ("inheritFillStyleID", g(5))]),
node(7, Some(2), "RECTANGLE", "Linked", vec![("inheritFillStyleID", g(5))]),
]),
)]);
let (json, _) = super::map::to_rest(&message, "Detach");
let find = |name: &str| json["document"]["children"][0]["children"].as_array().unwrap().iter().find(|n| n["name"] == name).unwrap().clone();
assert!(find("Box")["styles"].get("fill").is_none(), "{}", find("Box")["styles"]);
assert!(find("Linked")["styles"].get("fill").is_some(), "the older field still links when nothing detaches");
}