mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-27 03:46:50 -06:00
128 lines
7.6 KiB
Rust
128 lines
7.6 KiB
Rust
//! The Variables editor's commands: renaming, scopes, duplicates, modes, collections and aliases.
|
|
|
|
use serde_json::{Value, json};
|
|
|
|
use crate::Session;
|
|
use crate::params::id_of;
|
|
|
|
fn ex(s: &mut Session, cmd: &str, p: Value) -> Value {
|
|
s.execute(cmd, &p).unwrap()
|
|
}
|
|
|
|
fn tokens(s: &mut Session) {
|
|
ex(s, "variable.createCollection", json!({"name": "Tokens", "modes": ["Light", "Dark"]}));
|
|
ex(s, "variable.create", json!({"collection": "Tokens", "name": "space/sm", "type": "number", "value": 8}));
|
|
ex(s, "variable.create", json!({"collection": "Tokens", "name": "space/md", "type": "number", "value": 16}));
|
|
ex(s, "variable.create", json!({"collection": "Tokens", "name": "brand", "type": "color", "value": "#0D99FF"}));
|
|
}
|
|
|
|
#[test]
|
|
fn rename_describe_scope_and_duplicate() {
|
|
let mut s = Session::new();
|
|
tokens(&mut s);
|
|
ex(&mut s, "variable.update", json!({"variable": "space/sm", "name": " spacing / small ", "description": "Tight gaps"}));
|
|
let v = s.doc().unwrap().variables.get("spacing/small").cloned().unwrap();
|
|
assert_eq!(v.description, "Tight gaps");
|
|
// Names stay unique in a collection, and can't be blank.
|
|
assert!(s.execute("variable.update", &json!({"variable": "spacing/small", "name": "space/md"})).is_err());
|
|
assert!(s.execute("variable.update", &json!({"variable": "spacing/small", "name": " / "})).is_err());
|
|
// Scopes: a subset limits pickers, all of them (or ALL_SCOPES) is no limit, [] hides it.
|
|
ex(&mut s, "variable.update", json!({"variable": "spacing/small", "scopes": ["GAP", "CORNER_RADIUS"]}));
|
|
let v = s.doc().unwrap().variables.get("spacing/small").cloned().unwrap();
|
|
assert!(v.in_scope("GAP") && !v.in_scope("OPACITY"));
|
|
ex(&mut s, "variable.update", json!({"variable": "spacing/small", "scopes": ["ALL_SCOPES"]}));
|
|
assert_eq!(s.doc().unwrap().variables.get("spacing/small").unwrap().scopes, None);
|
|
ex(&mut s, "variable.update", json!({"variable": "spacing/small", "scopes": []}));
|
|
assert!(!s.doc().unwrap().variables.get("spacing/small").unwrap().in_scope("GAP"));
|
|
assert!(s.execute("variable.update", &json!({"variable": "brand", "scopes": ["GAP"]})).is_err());
|
|
ex(&mut s, "variable.update", json!({"variable": "brand", "scopes": ["ALL_FILLS"]}));
|
|
let brand = s.doc().unwrap().variables.get("brand").cloned().unwrap();
|
|
assert!(brand.in_scope("TEXT_FILL") && !brand.in_scope("STROKE_COLOR"));
|
|
// Duplicates sit after the original with a fresh id and name.
|
|
let id = ex(&mut s, "variable.duplicate", json!({"variable": "brand"}))["id"].as_str().unwrap().to_string();
|
|
let id2 = ex(&mut s, "variable.duplicate", json!({"variable": "brand"}))["id"].as_str().unwrap().to_string();
|
|
let vars = &s.doc().unwrap().variables;
|
|
assert_eq!(vars.get(&id).unwrap().name, "brand copy");
|
|
assert_eq!(vars.get(&id2).unwrap().name, "brand copy 2");
|
|
assert_eq!(vars.get(&id).unwrap().scopes, brand.scopes);
|
|
}
|
|
|
|
#[test]
|
|
fn modes_and_collections_rename_and_delete() {
|
|
let mut s = Session::new();
|
|
tokens(&mut s);
|
|
let f = id_of(&ex(&mut s, "frame.create", json!({"x": 0, "y": 0, "width": 50, "height": 50}))["id"]).unwrap();
|
|
ex(&mut s, "variable.bind", json!({"ids": [f], "field": "width", "variable": "space/md"}));
|
|
ex(&mut s, "variable.set", json!({"variable": "space/md", "mode": "Dark", "value": 24}));
|
|
ex(&mut s, "variable.setMode", json!({"ids": [f], "collection": "Tokens", "mode": "Dark"}));
|
|
assert_eq!(s.doc().unwrap().node(f).unwrap().width, 24.0);
|
|
ex(&mut s, "variable.renameMode", json!({"collection": "Tokens", "mode": "Dark", "name": "Night"}));
|
|
// Deleting the mode a frame uses puts it back on the default mode.
|
|
ex(&mut s, "variable.removeMode", json!({"collection": "Tokens", "mode": "Night"}));
|
|
assert_eq!(s.doc().unwrap().node(f).unwrap().width, 16.0);
|
|
assert!(s.execute("variable.removeMode", &json!({"collection": "Tokens", "mode": "Light"})).is_err(), "last mode stays");
|
|
// A new mode gets a fresh id even after a delete.
|
|
let m = ex(&mut s, "variable.addMode", json!({"collection": "Tokens", "name": "Dim"}))["id"].as_str().unwrap().to_string();
|
|
let coll = s.doc().unwrap().variables.collection("Tokens").cloned().unwrap();
|
|
assert_eq!(coll.modes.iter().filter(|x| x.id == m).count(), 1);
|
|
ex(&mut s, "variable.createCollection", json!({"name": "Other"}));
|
|
assert!(s.execute("variable.renameCollection", &json!({"collection": "Tokens", "name": "Other"})).is_err());
|
|
ex(&mut s, "variable.renameCollection", json!({"collection": "Tokens", "name": "Primitives"}));
|
|
// Deleting a collection removes its variables; bound layers keep their values.
|
|
let r = ex(&mut s, "variable.deleteCollection", json!({"collection": "Primitives"}));
|
|
assert_eq!(r["deletedVariables"], 3);
|
|
let doc = s.doc().unwrap();
|
|
assert!(doc.variables.get("space/md").is_none());
|
|
let n = doc.node(f).unwrap();
|
|
assert!(n.bound.is_empty() && n.var_modes.is_empty());
|
|
assert_eq!(n.width, 16.0);
|
|
// Undo brings it all back.
|
|
ex(&mut s, "edit.undo", json!({}));
|
|
assert!(s.doc().unwrap().variables.get("space/md").is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn aliases_follow_their_target_and_refuse_loops_and_other_types() {
|
|
let mut s = Session::new();
|
|
tokens(&mut s);
|
|
let f = id_of(&ex(&mut s, "frame.create", json!({"x": 0, "y": 0, "width": 50, "height": 50}))["id"]).unwrap();
|
|
ex(&mut s, "variable.bind", json!({"ids": [f], "field": "width", "variable": "space/md"}));
|
|
ex(&mut s, "variable.set", json!({"variable": "space/md", "value": {"alias": "space/sm"}}));
|
|
// Stored by id, resolved through the target.
|
|
let sm = s.doc().unwrap().variables.get("space/sm").unwrap().id.clone();
|
|
let md = s.doc().unwrap().variables.get("space/md").cloned().unwrap();
|
|
assert!(md.values.values().any(|v| *v == figcraft_doc::VariableValue::Alias(sm.clone())));
|
|
assert_eq!(s.doc().unwrap().node(f).unwrap().width, 8.0);
|
|
ex(&mut s, "variable.set", json!({"variable": "space/sm", "value": 12}));
|
|
assert_eq!(s.doc().unwrap().node(f).unwrap().width, 12.0);
|
|
// sm → md would loop (md already aliases sm); self-aliases too; types must match.
|
|
assert!(s.execute("variable.set", &json!({"variable": "space/sm", "value": {"alias": "space/md"}})).is_err());
|
|
assert!(s.execute("variable.set", &json!({"variable": "space/sm", "value": {"alias": "space/sm"}})).is_err());
|
|
assert!(s.execute("variable.set", &json!({"variable": "space/sm", "value": {"alias": "brand"}})).is_err());
|
|
assert!(s.execute("variable.set", &json!({"variable": "space/sm", "value": {"alias": "nope"}})).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn field_scopes_by_property_and_layer() {
|
|
let mut s = Session::new();
|
|
let f = id_of(&ex(&mut s, "frame.create", json!({"x": 0, "y": 0, "width": 50, "height": 50}))["id"]).unwrap();
|
|
let r = id_of(&ex(&mut s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 5, "height": 5}))["id"]).unwrap();
|
|
let doc = s.doc().unwrap();
|
|
let scope = |id, field| crate::cmd::variables::field_scope(doc, id, field);
|
|
assert_eq!(scope(f, "fill"), Some("FRAME_FILL"));
|
|
assert_eq!(scope(r, "fill"), Some("SHAPE_FILL"));
|
|
assert_eq!(scope(f, "paddingHorizontal"), Some("GAP"));
|
|
assert_eq!(scope(f, "visible"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn create_collection_refuses_a_taken_name() {
|
|
let mut s = Session::new();
|
|
ex(&mut s, "variable.createCollection", json!({"name": "Theme"}));
|
|
let before = s.doc().unwrap().variables.collections.len();
|
|
assert!(s.execute("variable.createCollection", &json!({"name": "Theme"})).is_err());
|
|
assert_eq!(s.doc().unwrap().variables.collections.len(), before);
|
|
ex(&mut s, "variable.createCollection", json!({"name": "Theme 2"}));
|
|
assert_eq!(s.doc().unwrap().variables.collections.len(), before + 1);
|
|
}
|