mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-25 19:06:47 -06:00
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
96 lines
5.2 KiB
Rust
96 lines
5.2 KiB
Rust
//! `node.set` doesn't store what can't apply: auto layout keys go only to layers that can have auto
|
|
//! layout (the others are named in `warnings`, and with none of them the call is refused), and a
|
|
//! `fontStyle` the family lacks says which face draws instead.
|
|
|
|
use serde_json::{Value, json};
|
|
|
|
use crate::{EngineError, Session};
|
|
|
|
fn ex(s: &mut Session, cmd: &str, p: Value) -> Value {
|
|
s.execute(cmd, &p).unwrap_or_else(|e| panic!("{cmd}: {e}"))
|
|
}
|
|
|
|
fn id(v: &Value) -> crate::NodeId {
|
|
crate::params::id_of(v).unwrap()
|
|
}
|
|
|
|
fn bad_msg(r: crate::Result<Value>) -> String {
|
|
match r {
|
|
Err(EngineError::BadParams { msg, .. }) => msg,
|
|
other => panic!("expected BadParams, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn auto_layout_keys_on_layers_without_auto_layout_are_refused() {
|
|
let mut s = Session::new();
|
|
let t = id(&ex(&mut s, "node.create", json!({"type": "text", "text": "Hi"}))["id"]);
|
|
let r = id(&ex(&mut s, "node.create", json!({"type": "rectangle"}))["id"]);
|
|
for (target, key, value) in [(t, "gap", json!(8)), (r, "padding", json!(4)), (t, "mainAlign", json!("center")), (r, "wrap", json!(true))] {
|
|
let msg = bad_msg(s.execute("node.set", &json!({"ids": [target], "props": {key: value}})));
|
|
assert!(msg.contains(key) && msg.contains("auto layout"), "{msg}");
|
|
assert!(s.doc().unwrap().node(target).unwrap().layout.is_none(), "no hidden auto layout on {target}");
|
|
}
|
|
// Frames still take them, with or without a layout mode first.
|
|
let f = id(&ex(&mut s, "node.create", json!({"type": "frame"}))["id"]);
|
|
let out = ex(&mut s, "node.set", json!({"ids": [f], "props": {"gap": 8, "padding": 4}}));
|
|
assert_eq!(s.doc().unwrap().node(f).unwrap().layout.as_ref().map(|l| l.gap), Some(8.0));
|
|
assert!(out.get("warnings").is_none(), "{out}");
|
|
}
|
|
|
|
#[test]
|
|
fn with_several_layers_the_frames_take_auto_layout_keys_and_the_others_are_named() {
|
|
let mut s = Session::new();
|
|
let f = id(&ex(&mut s, "node.create", json!({"type": "frame", "name": "Card"}))["id"]);
|
|
let t = id(&ex(&mut s, "node.create", json!({"type": "text", "text": "Hi", "name": "Title"}))["id"]);
|
|
let out = ex(&mut s, "node.set", json!({"ids": [f, t], "props": {"gap": 8, "opacity": 0.5}}));
|
|
let doc = s.doc().unwrap();
|
|
assert_eq!(doc.node(f).unwrap().layout.as_ref().map(|l| l.gap), Some(8.0));
|
|
assert!(doc.node(t).unwrap().layout.is_none(), "no hidden auto layout on the text");
|
|
assert_eq!(doc.node(t).unwrap().opacity, 0.5, "the text still gets the other keys");
|
|
let w = out["warnings"][0].as_str().unwrap();
|
|
assert!(w.contains("`gap`") && w.contains(&format!("{t} “Title”")) && w.contains("TEXT"), "{w}");
|
|
// One undo step for all of it.
|
|
ex(&mut s, "edit.undo", json!({}));
|
|
assert!(s.doc().unwrap().node(f).unwrap().layout.as_ref().is_none_or(|l| l.gap != 8.0));
|
|
}
|
|
|
|
#[test]
|
|
fn a_font_style_draws_in_the_nearest_face_and_says_so_when_it_differs() {
|
|
figcraft_text::scan_system_fonts();
|
|
// Any installed family with a regular and a bold face and nothing heavier (CI machines differ).
|
|
let pick = figcraft_text::family_names().into_iter().find_map(|f| {
|
|
let st = figcraft_text::family_styles(&f);
|
|
let has = |w: u16| st.iter().any(|x| figcraft_doc::style_weight(x) == w && !x.to_lowercase().contains("italic"));
|
|
(has(400) && has(700) && !st.iter().any(|x| figcraft_doc::style_weight(x) > 700)).then_some((f, st))
|
|
});
|
|
let Some((family, styles)) = pick else { return };
|
|
let mut s = Session::new();
|
|
let t = id(&ex(&mut s, "node.create", json!({"type": "text", "text": "Hi", "props": {"fontFamily": family}}))["id"]);
|
|
let set = |s: &mut Session, style: &str| ex(s, "node.set", json!({"ids": [t], "props": {"fontStyle": style}}));
|
|
// Any spelling of a style it has, and names that resolve to the weight they ask for, say nothing.
|
|
let bold = styles.iter().find(|x| figcraft_doc::style_weight(x) == 700).unwrap().clone();
|
|
for style in [bold.clone(), bold.to_uppercase(), bold.replace(' ', ""), "Roman".into(), "Normal".into()] {
|
|
let out = set(&mut s, &style);
|
|
assert!(out.get("warnings").is_none(), "{style}: {out}");
|
|
}
|
|
// A heavier weight than the family has applies (as a text layer resolves it) and says which face draws.
|
|
let out = set(&mut s, "Black");
|
|
let w = out["warnings"][0].as_str().unwrap();
|
|
assert!(w.contains(&family) && w.contains("`Black`") && w.contains("nearest"), "{w}");
|
|
assert_eq!(s.doc().unwrap().node(t).unwrap().kind.text().unwrap().spans()[0].2.font_style, "Black");
|
|
// A family that isn't installed keeps any style name without a word (files from elsewhere name theirs).
|
|
let out = ex(&mut s, "node.set", json!({"ids": [t], "props": {"fontFamily": "No Such Family 7f3a", "fontStyle": "Hairline"}}));
|
|
assert!(out.get("warnings").is_none(), "{out}");
|
|
}
|
|
|
|
#[test]
|
|
fn a_refused_call_changes_nothing() {
|
|
let mut s = Session::new();
|
|
let t = id(&ex(&mut s, "node.create", json!({"type": "text", "text": "Hi"}))["id"]);
|
|
let before = s.doc().unwrap().node(t).unwrap().fills.clone();
|
|
// `fill` comes before `gap` (keys apply in order), so this checks the rollback.
|
|
assert!(s.execute("node.set", &json!({"ids": [t], "props": {"fill": "#FF0000", "gap": 8}})).is_err());
|
|
assert_eq!(s.doc().unwrap().node(t).unwrap().fills, before);
|
|
}
|