mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-13 05:42:38 +00:00
`proto.settings` took any string as the device ("Pixel Nonexistent", "iphone 16") and saved it in
the file: the presenter then drew no device frame while its picker showed the name as if it were
one. An unknown colour was already refused with the device's colours, but a typo in the device
went unnoticed, and an agent couldn't read the settings back (`proto.settings {}` answered
`{ok: true}`, and neither `inspect_document` nor `get_node` shows a page's prototype settings).
An unknown name, or a value that isn't a string, is now refused with every name the picker offers
("None", "Custom size" and the devices), and nothing changes. A known name is taken in any case and
with spaces around it, and stored as the picker spells it (`known_device`). Every call answers the
page's settings as they are after it (`settings_json`): device, background, rotation, the colour
shown, the screen as presented (null without a device), the custom size and responsive, beside the
`ok: true` it answered before; `proto.settings {}` reads them.
Test: `proto_present_tests::unknown_devices_are_refused_and_settings_read_back`. Docs: mcp.md,
prototyping-parity.md, ROADMAP.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvG6E2LxvJ6MuFmiLA6Zqm
128 lines
7.6 KiB
Rust
128 lines
7.6 KiB
Rust
//! Prototype presentation settings through the session: the device's rotation (saved in the
|
|
//! file), the presenter's scale options and explicit variable modes set back to Auto.
|
|
|
|
use figcraft_doc::NodeKind;
|
|
use figcraft_doc::proto::{DeviceRotation, PrototypeSettings};
|
|
use serde_json::json;
|
|
|
|
use crate::Session;
|
|
use crate::params::id_of;
|
|
|
|
fn settings(s: &Session) -> PrototypeSettings {
|
|
let doc = s.doc().unwrap();
|
|
match &doc.node(s.page().unwrap()).unwrap().kind {
|
|
NodeKind::Page { prototype, .. } => prototype.clone(),
|
|
_ => panic!("not a page"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn device_rotation_turns_phones_and_tablets_and_is_saved() {
|
|
let mut s = Session::new();
|
|
s.execute("proto.settings", &json!({"device": "iPhone 16"})).unwrap();
|
|
assert_eq!(settings(&s).rotation, DeviceRotation::Portrait);
|
|
assert_eq!(settings(&s).device().map(|d| (d.width, d.height)), Some((393.0, 852.0)));
|
|
// Landscape swaps the screen's sides.
|
|
s.execute("proto.settings", &json!({"rotation": "landscape"})).unwrap();
|
|
assert!(settings(&s).landscape());
|
|
assert_eq!(settings(&s).device().map(|d| (d.width, d.height)), Some((852.0, 393.0)));
|
|
// Undoable, and degrees work too.
|
|
s.execute("edit.undo", &json!({})).unwrap();
|
|
assert_eq!(settings(&s).rotation, DeviceRotation::Portrait);
|
|
s.execute("proto.settings", &json!({"rotation": 90})).unwrap();
|
|
assert!(settings(&s).landscape());
|
|
// Laptops and desktops don't turn: the rotation is kept but doesn't apply.
|
|
s.execute("proto.settings", &json!({"device": "MacBook Air"})).unwrap();
|
|
assert_eq!(settings(&s).rotation, DeviceRotation::Landscape);
|
|
assert!(!settings(&s).landscape());
|
|
assert_eq!(settings(&s).device().map(|d| (d.width, d.height)), Some((1280.0, 832.0)));
|
|
// Junk is refused and changes nothing.
|
|
for bad in [json!("sideways"), json!(45), json!(true), json!({}), json!(f64::MAX)] {
|
|
assert!(s.execute("proto.settings", &json!({"rotation": bad})).is_err(), "{bad}");
|
|
}
|
|
assert_eq!(settings(&s).rotation, DeviceRotation::Landscape);
|
|
// Saved in the file and read back; files without it open in portrait.
|
|
s.execute("proto.settings", &json!({"device": "iPad mini"})).unwrap();
|
|
let bytes = figcraft_format::save_bytes(s.doc().unwrap(), None).unwrap();
|
|
let (doc, _) = figcraft_format::load_bytes(&bytes).unwrap();
|
|
let NodeKind::Page { prototype, .. } = &doc.node(doc.pages[0]).unwrap().kind else { panic!("not a page") };
|
|
assert_eq!(prototype.device().map(|d| (d.width, d.height)), Some((1133.0, 744.0)));
|
|
let old: PrototypeSettings = serde_json::from_value(json!({"device": "iPhone 16"})).unwrap();
|
|
assert_eq!(old.rotation, DeviceRotation::Portrait);
|
|
}
|
|
|
|
#[test]
|
|
fn present_options_ask_the_ui_and_check_their_values() {
|
|
let mut s = Session::new();
|
|
s.ui_requests.clear();
|
|
s.execute("proto.presentOptions", &json!({"scale": "fitWidth", "deviceFrame": false})).unwrap();
|
|
let r = s.ui_requests.last().unwrap();
|
|
assert_eq!((r.kind.as_str(), &r.data), ("present.options", &json!({"scale": "fitWidth", "deviceFrame": false})));
|
|
for scale in crate::cmd::proto::PRESENT_SCALES {
|
|
s.execute("proto.presentOptions", &json!({"scale": scale})).unwrap();
|
|
}
|
|
assert!(s.execute("proto.presentOptions", &json!({"scale": "huge"})).is_err());
|
|
assert!(s.execute("proto.presentOptions", &json!({"deviceFrame": "yes"})).is_err());
|
|
assert!(s.execute("proto.presentOptions", &json!({})).is_err(), "nothing to set");
|
|
// It's a viewing option: the document doesn't change.
|
|
assert!(!s.ds().unwrap().is_dirty());
|
|
}
|
|
|
|
#[test]
|
|
fn explicit_variable_modes_go_back_to_auto() {
|
|
let mut s = Session::new();
|
|
let id = |v: serde_json::Value| id_of(&v["id"]).unwrap();
|
|
let f = id(s.execute("frame.create", &json!({"x": 0, "y": 0, "width": 100, "height": 100})).unwrap());
|
|
let r = id(s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10, "parent": f})).unwrap());
|
|
s.execute("variable.createCollection", &json!({"name": "Tokens", "modes": ["Light", "Dark"]})).unwrap();
|
|
s.execute("variable.create", &json!({"collection": "Tokens", "name": "size", "type": "number", "value": 20})).unwrap();
|
|
s.execute("variable.set", &json!({"variable": "size", "mode": "Dark", "value": 40})).unwrap();
|
|
s.execute("variable.bind", &json!({"ids": [r], "field": "width", "variable": "size"})).unwrap();
|
|
let width = |s: &Session| s.doc().unwrap().node(r).unwrap().width;
|
|
s.execute("variable.setMode", &json!({"ids": [f], "collection": "Tokens", "mode": "Dark"})).unwrap();
|
|
assert_eq!(width(&s), 40.0);
|
|
// Auto: the frame inherits the page's mode again, and the layer follows.
|
|
s.execute("variable.setMode", &json!({"ids": [f], "collection": "Tokens", "mode": null})).unwrap();
|
|
assert!(s.doc().unwrap().node(f).unwrap().var_modes.is_empty());
|
|
assert_eq!(width(&s), 20.0);
|
|
s.execute("edit.undo", &json!({})).unwrap();
|
|
assert_eq!(width(&s), 40.0);
|
|
// The page set to Dark: Auto inherits it.
|
|
let page = s.page().unwrap();
|
|
s.execute("variable.setMode", &json!({"ids": [page], "collection": "Tokens", "mode": "Dark"})).unwrap();
|
|
s.execute("variable.setMode", &json!({"ids": [f], "collection": "Tokens", "mode": "Light"})).unwrap();
|
|
assert_eq!(width(&s), 20.0);
|
|
s.execute("variable.setMode", &json!({"ids": [f], "collection": "Tokens", "mode": null})).unwrap();
|
|
assert_eq!(width(&s), 40.0);
|
|
assert!(s.execute("variable.setMode", &json!({"ids": [f], "collection": "Tokens", "mode": "Dusk"})).is_err());
|
|
assert!(s.execute("variable.setMode", &json!({"ids": [f], "collection": "Tokens"})).is_err(), "a mode, or null for Auto");
|
|
}
|
|
|
|
/// #2027: an unknown device is refused with the devices to pick from, as an unknown colour is, and
|
|
/// nothing changes; a known one is taken whatever its case and stored as the picker names it; the
|
|
/// settings read back from `proto.settings` without parameters.
|
|
#[test]
|
|
fn unknown_devices_are_refused_and_settings_read_back() {
|
|
let mut s = Session::new();
|
|
let before = settings(&s);
|
|
for junk in [json!("Pixel Nonexistent"), json!(""), json!(42), json!(["iPhone 16"])] {
|
|
let err = s.execute("proto.settings", &json!({"device": junk, "responsive": true})).unwrap_err().to_string();
|
|
assert!(err.contains("unknown device") && err.contains("iPhone 17|") && err.contains("Custom size"), "{junk}: {err}");
|
|
assert_eq!(settings(&s), before, "{junk}: nothing changes");
|
|
}
|
|
let set = s.execute("proto.settings", &json!({"device": " iphone 16 ", "color": "black"})).unwrap();
|
|
assert_eq!(settings(&s).device, "iPhone 16", "stored as the picker names it");
|
|
assert_eq!((&set["device"], &set["color"]), (&json!("iPhone 16"), &json!("Black")));
|
|
let read = s.execute("proto.settings", &json!({})).unwrap();
|
|
assert_eq!(read, set, "a call without parameters reads the settings");
|
|
assert_eq!((&read["ok"], &read["rotation"], &read["responsive"]), (&json!(true), &json!("portrait"), &json!(false)));
|
|
assert_eq!(read["screen"], json!({"width": 393.0, "height": 852.0}));
|
|
assert!(read["background"].as_str().is_some_and(|c| c.starts_with('#')), "{read}");
|
|
// "None" and "Custom size" in any case; no device has no screen or colour.
|
|
s.execute("proto.settings", &json!({"device": "custom SIZE", "width": 800, "height": 600})).unwrap();
|
|
assert_eq!(settings(&s).device, PrototypeSettings::CUSTOM_DEVICE);
|
|
let none = s.execute("proto.settings", &json!({"device": "none"})).unwrap();
|
|
assert_eq!((&none["device"], &none["screen"], &none["color"]), (&json!("None"), &json!(null), &json!(null)));
|
|
assert_eq!(none["customSize"], json!({"width": 800.0, "height": 600.0}));
|
|
}
|