Files
vectorcraft/crates/engine/src/tests/variables.rs
T
2026-11-08 23:50:24 +07:00

2835 lines
165 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Variables (data merge): definitions with kinds, bindings to objects, datasets
//! applied in one undo step, and validation that rejects instead of reinterpreting.
use serde_json::{Value, json};
use vectorcraft_doc::NodeId;
use crate::tests_links::{BLUE, Folder, RED, bounds, image, place, png, write};
use crate::*;
fn session() -> Session {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
s
}
fn text(s: &mut Session, content: &str) -> NodeId {
let r = s.execute("text.create", &json!({"x": 10, "y": 10, "text": content})).unwrap();
NodeId(r["id"].as_u64().unwrap())
}
fn rect(s: &mut Session) -> NodeId {
let r = s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 50, "height": 40})).unwrap();
NodeId(r["id"].as_u64().unwrap())
}
/// A group of the objects `ids` → the group's id.
fn group(s: &mut Session, ids: &[NodeId]) -> NodeId {
s.execute("select.set", &json!({"ids": ids.iter().map(|i| i.0).collect::<Vec<_>>()})).unwrap();
let r = s.execute("object.group", &json!({})).unwrap();
NodeId(r["id"].as_u64().unwrap())
}
fn plain(s: &Session, id: NodeId) -> String {
match &s.doc().unwrap().doc.node(id).unwrap().kind {
vectorcraft_doc::NodeKind::Text(t) => t.plain_text(),
_ => panic!("not text"),
}
}
#[test]
fn text_and_visibility_datasets_apply_in_one_undo_step() {
let mut s = session();
let t = text(&mut s, "Alice");
let r = rect(&mut s);
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("variable.define", &json!({"name": "Show", "kind": "visibility"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).unwrap();
s.execute("variable.bind", &json!({"variable": "Show", "ids": [r.0]})).unwrap();
s.execute("dataset.new", &json!({"name": "B", "values": {"Name": "Bob", "Show": false}})).unwrap();
let v = s.execute("dataset.select", &json!({"name": "B"})).unwrap();
assert_eq!((v["applied"].as_u64(), v["skipped"].as_u64()), (Some(2), Some(0)));
assert_eq!(plain(&s, t), "Bob");
assert!(!s.doc().unwrap().doc.node(r).unwrap().visible);
// One undo step covers the whole dataset application.
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(plain(&s, t), "Alice");
assert!(s.doc().unwrap().doc.node(r).unwrap().visible);
s.execute("edit.redo", &json!({})).unwrap();
assert_eq!(plain(&s, t), "Bob");
}
#[test]
fn dataset_next_and_prev_wrap_around() {
let mut s = session();
let t = text(&mut s, "x");
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).unwrap();
for name in ["A", "B"] {
s.execute("dataset.new", &json!({"name": name, "values": {"Name": name}})).unwrap();
}
s.execute("dataset.next", &json!({})).unwrap();
assert_eq!(plain(&s, t), "A");
s.execute("dataset.next", &json!({})).unwrap();
assert_eq!(plain(&s, t), "B");
s.execute("dataset.next", &json!({})).unwrap();
assert_eq!(plain(&s, t), "A");
s.execute("dataset.prev", &json!({})).unwrap();
assert_eq!(plain(&s, t), "B");
let v = s.execute("dataset.list", &json!({})).unwrap();
assert_eq!(v["active"], "B");
}
#[test]
fn applying_a_dataset_reports_the_area_type_it_overflows() {
let mut s = session();
let area = |s: &mut Session, y: u32, content: &str| {
let r = s.execute("text.create", &json!({"x": 10, "y": y, "text": content, "size": 12, "area": {"width": 120, "height": 40}})).unwrap();
NodeId(r["id"].as_u64().unwrap())
};
let long = "word ".repeat(60);
let a = area(&mut s, 10, "x");
let p = text(&mut s, "x");
// Area type that already overflows, bound to no variable, isn't named.
area(&mut s, 200, &long);
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("variable.define", &json!({"name": "Title", "kind": "text"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Name", "ids": [a.0, p.0]})).unwrap();
// `variable.bind` refuses a second text variable on one type object; a document saved
// before it did can hold two.
Arc::make_mut(&mut s.doc_mut().unwrap().doc).variables.bind(a, "Title");
s.execute("dataset.new", &json!({"name": "Short", "values": {"Name": "Ann", "Title": "Ann"}})).unwrap();
s.execute("dataset.new", &json!({"name": "Long", "values": {"Name": long, "Title": long}})).unwrap();
// The area type is named once though two variables rewrote it; the point type is never named.
let v = s.execute("dataset.select", &json!({"name": "Long"})).unwrap();
assert_eq!((v["applied"].as_u64(), &v["overflow"]), (Some(3), &json!([a.0])), "{v}");
let v = s.execute("dataset.select", &json!({"name": "Short"})).unwrap();
assert!(v.get("overflow").is_none(), "{v}");
let v = s.execute("dataset.next", &json!({})).unwrap();
assert_eq!((&v["name"], &v["overflow"]), (&json!("Long"), &json!([a.0])), "{v}");
let v = s.execute("dataset.prev", &json!({})).unwrap();
assert!(v.get("overflow").is_none(), "{v}");
let v = s.execute("dataset.prev", &json!({})).unwrap();
assert_eq!((&v["name"], &v["overflow"]), (&json!("Long"), &json!([a.0])), "{v}");
}
/// Area type threaded into a rectangle → (session, first frame, second frame, the story).
fn threaded(second_height: u32) -> (Session, NodeId, NodeId, String) {
let mut s = session();
let story = "The quick brown fox jumps over the lazy dog and keeps running. ".repeat(3) + "\nSecond paragraph.\nThird.";
let r = s.execute("text.create", &json!({"x": 10, "y": 10, "text": story, "area": {"width": 150, "height": 60}})).unwrap();
let a = NodeId(r["id"].as_u64().unwrap());
let r = s.execute("shape.rectangle", &json!({"x": 200, "y": 10, "width": 150, "height": second_height})).unwrap();
let b = NodeId(r["id"].as_u64().unwrap());
s.execute("select.set", &json!({"ids": [a.0, b.0]})).unwrap();
s.execute("text.thread.create", &json!({})).unwrap();
(s, a, b, story)
}
/// Whether `dataset.list` says the data set `name` matches the art.
fn matches(s: &mut Session, name: &str) -> bool {
let sets = s.execute("dataset.list", &json!({})).unwrap();
let set = sets["datasets"].as_array().unwrap().iter().find(|d| d["name"] == name).unwrap().clone();
set["matches"].as_bool().unwrap()
}
#[test]
fn a_text_value_on_a_frame_of_threaded_type_replaces_the_whole_story() {
let (mut s, a, b, story) = threaded(200);
assert!(!plain(&s, a).is_empty() && !plain(&s, b).is_empty());
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Name", "ids": [a.0]})).unwrap();
// A capture reads the whole story, every frame's text in thread order.
s.execute("dataset.capture", &json!({"name": "Orig"})).unwrap();
let orig = s.doc().unwrap().doc.variables.dataset("Orig").unwrap().values.get("Name").cloned();
assert_eq!(orig, Some(vectorcraft_doc::DataValue::Text(story.clone())));
s.execute("dataset.new", &json!({"name": "B", "values": {"Name": "Bob"}})).unwrap();
let v = s.execute("dataset.select", &json!({"name": "B"})).unwrap();
assert_eq!((v["applied"].as_u64(), v["skipped"].as_u64()), (Some(1), Some(0)));
assert_eq!((plain(&s, a), plain(&s, b)), ("Bob".to_string(), String::new()));
assert!(matches(&mut s, "B"));
// The captured row applied back gives the story back, every time.
for _ in 0..2 {
s.execute("dataset.select", &json!({"name": "Orig"})).unwrap();
assert_eq!(format!("{}{}", plain(&s, a), plain(&s, b)), story);
assert!(!plain(&s, b).is_empty());
assert!(matches(&mut s, "Orig"));
s.execute("dataset.select", &json!({"name": "B"})).unwrap();
assert_eq!(format!("{}{}", plain(&s, a), plain(&s, b)), "Bob");
}
}
#[test]
fn a_text_value_on_a_later_frame_of_threaded_type_replaces_the_whole_story() {
let (mut s, a, b, story) = threaded(200);
s.execute("variable.define", &json!({"name": "Tail", "kind": "text"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Tail", "ids": [b.0]})).unwrap();
s.execute("dataset.capture", &json!({"name": "Orig"})).unwrap();
s.execute("dataset.new", &json!({"name": "T", "values": {"Tail": "END"}})).unwrap();
let v = s.execute("dataset.select", &json!({"name": "T"})).unwrap();
assert_eq!(v["applied"].as_u64(), Some(1));
assert_eq!((plain(&s, a), plain(&s, b)), ("END".to_string(), String::new()));
assert!(matches(&mut s, "T"));
s.execute("dataset.select", &json!({"name": "Orig"})).unwrap();
assert_eq!(format!("{}{}", plain(&s, a), plain(&s, b)), story);
assert!(matches(&mut s, "Orig"));
}
#[test]
fn a_story_with_a_locked_frame_is_neither_read_nor_rewritten() {
let (mut s, a, b, story) = threaded(200);
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Name", "ids": [a.0]})).unwrap();
s.execute("dataset.new", &json!({"name": "B", "values": {"Name": "Bob"}})).unwrap();
s.execute("select.set", &json!({"ids": [b.0]})).unwrap();
s.execute("object.lock", &json!({})).unwrap();
let v = s.execute("dataset.select", &json!({"name": "B"})).unwrap();
assert_eq!((v["applied"].as_u64(), v["skipped"].as_u64()), (Some(0), Some(1)));
assert_eq!(format!("{}{}", plain(&s, a), plain(&s, b)), story);
assert!(s.doc().unwrap().doc.captured_values().is_empty());
}
#[test]
fn two_text_variables_in_one_story_apply_in_definition_order() {
let (mut s, a, b, _) = threaded(200);
s.execute("variable.define", &json!({"name": "Head", "kind": "text"})).unwrap();
s.execute("variable.define", &json!({"name": "Tail", "kind": "text"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Tail", "ids": [a.0]})).unwrap();
s.execute("variable.bind", &json!({"variable": "Head", "ids": [b.0]})).unwrap();
s.execute("dataset.new", &json!({"name": "R", "values": {"Head": "one", "Tail": "two"}})).unwrap();
let v = s.execute("dataset.select", &json!({"name": "R"})).unwrap();
assert_eq!(v["applied"].as_u64(), Some(2));
assert_eq!(format!("{}{}", plain(&s, a), plain(&s, b)), "two");
}
#[test]
fn applying_a_dataset_to_threaded_type_reports_overflow_on_the_last_frame() {
let (mut s, a, b, _) = threaded(40);
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Name", "ids": [a.0]})).unwrap();
s.execute("dataset.new", &json!({"name": "Long", "values": {"Name": "word ".repeat(120)}})).unwrap();
s.execute("dataset.new", &json!({"name": "Short", "values": {"Name": "Ann"}})).unwrap();
let v = s.execute("dataset.select", &json!({"name": "Long"})).unwrap();
assert_eq!(&v["overflow"], &json!([b.0]), "{v}");
let v = s.execute("dataset.select", &json!({"name": "Short"})).unwrap();
assert!(v.get("overflow").is_none(), "{v}");
}
#[test]
fn mismatched_and_missing_bindings_are_skipped_and_counted() {
let mut s = session();
let t = text(&mut s, "x");
let r = rect(&mut s);
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
// A text variable bound to a rectangle applies to nothing; a dataset value for an
// unbound variable is ignored.
s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).unwrap();
s.execute("dataset.new", &json!({"name": "D", "values": {"Name": "y", "Ghost": "z"}})).unwrap_err();
s.execute("dataset.new", &json!({"name": "D", "values": {"Name": "y"}})).unwrap();
s.execute("variable.bind", &json!({"variable": "Name", "ids": [r.0]})).unwrap_err();
let v = s.execute("dataset.select", &json!({"name": "D"})).unwrap();
assert_eq!((v["applied"].as_u64(), v["skipped"].as_u64()), (Some(1), Some(0)));
assert_eq!(plain(&s, t), "y");
}
/// A row with no value for any bound variable, such as one from Import Data Sets… before anything
/// is bound, applies nothing, and the reply's `warning` names it. A bound variable with a value in
/// the row gives no warning, even when its object is locked and skipped.
#[test]
fn a_row_with_no_value_for_a_bound_variable_warns_that_nothing_was_applied() {
let mut s = session();
let t = text(&mut s, "x");
s.execute("dataset.import", &json!({"csv": "Name,Title\nAnn,Boss\nBob,Chief\n"})).unwrap();
let (a, b) = ("Data Set 1", "Data Set 2");
let warning = |row: &str| format!("nothing in data set {row} was applied: no object is bound to a variable it has a value for");
let v = s.execute("dataset.select", &json!({"name": a})).unwrap();
assert_eq!((v["applied"].as_u64(), v["skipped"].as_u64(), &v["warning"]), (Some(0), Some(0), &json!(warning(a))), "{v}");
let v = s.execute("dataset.next", &json!({})).unwrap();
assert_eq!(v["warning"], json!(warning(b)), "{v}");
let v = s.execute("dataset.prev", &json!({})).unwrap();
assert_eq!(v["warning"], json!(warning(a)), "{v}");
assert_eq!(plain(&s, t), "x");
// A bound variable that the row has no value for still applies nothing.
s.execute("variable.define", &json!({"name": "Other", "kind": "text"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Other", "ids": [t.0]})).unwrap();
let v = s.execute("dataset.select", &json!({"name": b})).unwrap();
assert_eq!((v["applied"].as_u64(), v["skipped"].as_u64(), &v["warning"]), (Some(0), Some(1), &json!(warning(b))), "{v}");
// Bound to a variable with a value, the row applies it and gives no warning.
s.execute("variable.unbind", &json!({"variable": "Other", "ids": [t.0]})).unwrap();
s.execute("variable.bind", &json!({"variable": "Title", "ids": [t.0]})).unwrap();
let v = s.execute("dataset.select", &json!({"name": a})).unwrap();
assert_eq!(v["applied"].as_u64(), Some(1), "{v}");
assert!(v.get("warning").is_none(), "{v}");
assert_eq!(plain(&s, t), "Boss");
// Locked art is skipped, and its binding still counts as bound.
s.execute("select.set", &json!({"ids": [t.0]})).unwrap();
s.execute("object.lock", &json!({})).unwrap();
let v = s.execute("dataset.select", &json!({"name": b})).unwrap();
assert_eq!(v["applied"].as_u64(), Some(0), "{v}");
assert!(v.get("warning").is_none(), "{v}");
}
#[test]
fn deleting_a_variable_cleans_bindings_values_and_leaves_datasets_alone() {
let mut s = session();
let t = text(&mut s, "x");
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).unwrap();
s.execute("dataset.new", &json!({"name": "D", "values": {"Name": "y"}})).unwrap();
s.execute("dataset.select", &json!({"name": "D"})).unwrap();
s.execute("variable.delete", &json!({"names": ["Name"]})).unwrap();
let vars = s.execute("variable.list", &json!({})).unwrap();
assert_eq!(vars["variables"].as_array().map(Vec::len), Some(0));
let sets = s.execute("dataset.list", &json!({})).unwrap();
assert!(sets["datasets"][0].get("values").is_none_or(|v| v == &json!({})));
// The dataset named "D" is still the active one: `variable.delete` prunes variables, and
// the dataset's own name is its own namespace.
assert_eq!(sets["active"], "D");
s.execute("dataset.delete", &json!({"names": ["D"]})).unwrap();
let sets = s.execute("dataset.list", &json!({})).unwrap();
assert_eq!(sets["datasets"].as_array().map(Vec::len), Some(0));
assert!(sets["active"].is_null());
let v: Value = serde_json::from_str(&serde_json::to_string(&s.doc().unwrap().doc).unwrap()).unwrap();
assert!(v["variables"].get("bindings").is_none_or(|v| v == &json!({})));
}
/// A dataset may share a variable's name: they are separate namespaces, and deleting the
/// variable must not silently unselect the dataset that happens to be called the same.
#[test]
fn a_dataset_survives_a_variable_of_the_same_name_being_deleted() {
let mut s = session();
s.execute("variable.define", &json!({"name": "Row", "kind": "text"})).unwrap();
s.execute("dataset.new", &json!({"name": "Row", "values": {"Row": "y"}})).unwrap();
s.execute("dataset.select", &json!({"name": "Row"})).unwrap();
s.execute("variable.delete", &json!({"names": ["Row"]})).unwrap();
let sets = s.execute("dataset.list", &json!({})).unwrap();
assert_eq!(sets["datasets"][0]["name"], "Row", "the dataset is not a variable");
assert_eq!(sets["active"], "Row", "the active dataset keeps its name");
}
/// Deleting art lets its bindings go, as it does for Asset Export's assets: a binding that
/// outlived its object would count as a skip in every dataset application and grow the file.
#[test]
fn deleted_art_lets_its_bindings_go() {
let mut s = session();
let t = text(&mut s, "x");
let r = rect(&mut s);
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("variable.define", &json!({"name": "Show", "kind": "visibility"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).unwrap();
s.execute("variable.bind", &json!({"variable": "Show", "ids": [r.0]})).unwrap();
s.execute("dataset.new", &json!({"name": "D", "values": {"Name": "y", "Show": false}})).unwrap();
s.execute("select.set", &json!({"ids": [t.0]})).unwrap();
s.execute("edit.clear", &json!({})).unwrap();
let vars = s.execute("variable.list", &json!({})).unwrap();
let of = |name: &str| vars["variables"].as_array().map(|a| a.iter().find(|v| v["name"] == name).unwrap()["bindings"].as_u64().unwrap()).unwrap();
assert_eq!(of("Name"), 0, "the deleted text object took its binding with it");
assert_eq!(of("Show"), 1, "the rectangle is still bound");
// Nothing stale is left for the application to skip.
let v = s.execute("dataset.select", &json!({"name": "D"})).unwrap();
assert_eq!((v["applied"].as_u64(), v["skipped"].as_u64()), (Some(1), Some(0)));
}
/// `deleted` counts what was deleted, not what was asked for.
#[test]
fn delete_reports_what_it_actually_removed() {
let mut s = session();
s.execute("variable.define", &json!({"name": "A", "kind": "text"})).unwrap();
s.execute("variable.define", &json!({"name": "B", "kind": "text"})).unwrap();
s.execute("dataset.new", &json!({"name": "D"})).unwrap();
assert_eq!(s.execute("variable.delete", &json!({"names": ["A"]})).unwrap()["deleted"], 1);
assert_eq!(s.execute("variable.delete", &json!({"names": ["A"]})).unwrap()["deleted"], 0, "already gone");
assert_eq!(s.execute("variable.delete", &json!({"names": ["A", "B"]})).unwrap()["deleted"], 1, "only B was left");
assert_eq!(s.execute("dataset.delete", &json!({"names": ["D"]})).unwrap()["deleted"], 1);
assert_eq!(s.execute("dataset.delete", &json!({"names": ["D"]})).unwrap()["deleted"], 0);
}
/// An object may be driven by several variables, so binding it again adds to what it has
/// instead of quietly dropping the earlier one.
#[test]
fn one_object_can_be_driven_by_several_variables() {
let mut s = session();
let t = text(&mut s, "x");
let r = rect(&mut s);
s.execute("variable.define", &json!({"name": "Title", "kind": "text"})).unwrap();
s.execute("variable.define", &json!({"name": "Shown", "kind": "visibility"})).unwrap();
s.execute("variable.define", &json!({"name": "Badge", "kind": "visibility"})).unwrap();
// A text object driven by a text variable and a visibility variable keeps both.
assert_eq!(s.execute("variable.bind", &json!({"variable": "Title", "ids": [t.0]})).unwrap()["bound"], 1);
let v = s.execute("variable.bind", &json!({"variable": "Shown", "ids": [t.0]})).unwrap();
assert_eq!((v["bound"].clone(), v["already"].clone()), (json!(1), json!([])), "Title is untouched");
// A rectangle driven by a visibility variable as well as nothing else yet.
s.execute("variable.bind", &json!({"variable": "Badge", "ids": [r.0]})).unwrap();
s.execute("dataset.new", &json!({"name": "D", "values": {"Title": "Hat", "Shown": true, "Badge": false}})).unwrap();
let v = s.execute("dataset.select", &json!({"name": "D"})).unwrap();
assert_eq!((v["applied"].as_u64(), v["skipped"].as_u64()), (Some(3), Some(0)), "every binding applied");
assert_eq!(plain(&s, t), "Hat");
assert!(s.doc().unwrap().doc.node(t).unwrap().visible);
assert!(!s.doc().unwrap().doc.node(r).unwrap().visible);
let vars = s.execute("variable.list", &json!({})).unwrap();
let of = |n: &str| vars["variables"].as_array().unwrap().iter().find(|v| v["name"] == n).unwrap()["bindings"].as_u64();
assert_eq!((of("Title"), of("Shown"), of("Badge")), (Some(1), Some(1), Some(1)));
// Binding again to one it already has is not a change, and says so.
let v = s.execute("variable.bind", &json!({"variable": "Shown", "ids": [t.0]})).unwrap();
assert_eq!((v["bound"].clone(), v["already"].clone()), (json!(0), json!([t.0])));
// Unbinding one variable leaves the object's others alone.
assert_eq!(s.execute("variable.unbind", &json!({"ids": [t.0], "variable": "Shown"})).unwrap()["unbound"], 1);
s.execute("edit.undo", &json!({})).unwrap();
let vars = s.execute("variable.list", &json!({})).unwrap();
assert_eq!(vars["variables"][1]["bindings"], 1, "the other binding is still there");
assert!(s.execute("variable.unbind", &json!({"ids": [t.0], "variable": "Ghost"})).is_err());
// Deleting a variable drops only its own binding.
s.execute("variable.delete", &json!({"names": ["Shown"]})).unwrap();
let vars = s.execute("variable.list", &json!({})).unwrap();
assert_eq!(vars["variables"][0]["bindings"], 1, "Title still drives the text");
}
/// A type object holds one text variable: binding another text variable to it is refused, the
/// error names the one it has, and nothing is bound, not even the other objects of the call.
/// Variables of other kinds still share the object.
#[test]
fn a_type_object_holds_one_text_variable() {
let mut s = session();
let t = text(&mut s, "x");
let u = text(&mut s, "y");
for (name, kind) in [("Title", "text"), ("Price", "text"), ("Shown", "visibility")] {
s.execute("variable.define", &json!({"name": name, "kind": kind})).unwrap();
}
s.execute("variable.bind", &json!({"variable": "Title", "ids": [t.0]})).unwrap();
let bindings = |s: &Session, name: &str| s.doc().unwrap().doc.variables.objects_of(name);
let e = s.execute("variable.bind", &json!({"variable": "Price", "ids": [u.0, t.0]})).unwrap_err().to_string();
assert!(e.contains("`variable.bind`") && e.contains(&format!("node {} already has the text variable `Title`", t.0)), "{e}");
assert_eq!((bindings(&s, "Title"), bindings(&s, "Price")), (vec![t], vec![]), "nothing was bound");
// The same variable again is not a second one, and another type object takes Price.
assert_eq!(s.execute("variable.bind", &json!({"variable": "Title", "ids": [t.0]})).unwrap()["already"], json!([t.0]));
s.execute("variable.bind", &json!({"variable": "Price", "ids": [u.0]})).unwrap();
s.execute("variable.bind", &json!({"variable": "Shown", "ids": [t.0, u.0]})).unwrap();
assert_eq!(bindings(&s, "Shown"), vec![t, u]);
// After Title is unbound from it, the object takes Price.
s.execute("variable.unbind", &json!({"ids": [t.0], "variable": "Title"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Price", "ids": [t.0]})).unwrap();
assert_eq!(bindings(&s, "Price"), vec![t, u]);
}
/// Make Text Dynamic refuses type that already has a text variable and defines no variable;
/// Make Visibility Dynamic still binds it.
#[test]
fn make_text_dynamic_refuses_type_that_has_a_text_variable() {
let mut s = session();
let t = text(&mut s, "Alice");
s.execute("select.set", &json!({"ids": [t.0]})).unwrap();
s.execute("variable.makeTextDynamic", &json!({})).unwrap();
let e = s.execute("variable.makeTextDynamic", &json!({})).unwrap_err().to_string();
assert!(e.contains("`variable.makeTextDynamic`") && e.contains(&format!("node {} already has the text variable `Alice`", t.0)), "{e}");
let vars = s.execute("variable.list", &json!({})).unwrap();
assert_eq!(vars["variables"].as_array().map(Vec::len), Some(1), "{vars}");
let v = s.execute("variable.makeVisibilityDynamic", &json!({})).unwrap();
assert_eq!(v["ids"], json!([t.0]));
}
/// With unbound type selected before type that has a text variable, Make Text Dynamic is still
/// refused: it defines no variable and binds neither object.
#[test]
fn make_text_dynamic_refuses_a_selection_where_a_later_object_has_a_text_variable() {
let mut s = session();
let u = text(&mut s, "Bob");
let t = text(&mut s, "Alice");
s.execute("select.set", &json!({"ids": [t.0]})).unwrap();
s.execute("variable.makeTextDynamic", &json!({})).unwrap();
s.execute("select.set", &json!({"ids": [u.0, t.0]})).unwrap();
let e = s.execute("variable.makeTextDynamic", &json!({})).unwrap_err().to_string();
assert!(e.contains(&format!("node {} already has the text variable `Alice`", t.0)), "{e}");
let doc = &s.doc().unwrap().doc;
assert_eq!(doc.variables.variables.len(), 1, "no variable was defined");
assert!(!doc.variables.bindings.contains_key(&u), "the unbound object stays unbound");
assert_eq!(doc.variables.bindings[&t].iter().collect::<Vec<_>>(), ["Alice"]);
}
/// A document saved with two text variables on one type object opens with both bindings, and
/// applying a row still writes both, the later one defined last. Binding either again changes
/// nothing, and binding a third is refused with the first one named.
#[test]
fn a_document_with_two_text_variables_on_one_type_object_opens_unchanged() {
let mut s = session();
let t = text(&mut s, "x");
let u = text(&mut s, "y");
for name in ["Title", "Price", "Note"] {
s.execute("variable.define", &json!({"name": name, "kind": "text"})).unwrap();
}
s.execute("variable.bind", &json!({"variable": "Title", "ids": [t.0]})).unwrap();
Arc::make_mut(&mut s.doc_mut().unwrap().doc).variables.bind(t, "Price");
s.execute("dataset.new", &json!({"name": "D", "values": {"Title": "Hat", "Price": "9.99"}})).unwrap();
let saved = s.execute("document.export", &json!({"format": "vectorcraft"})).unwrap()["dataBase64"].clone();
let mut o = Session::new();
o.execute("document.open", &json!({"name": "two.vectorcraft", "dataBase64": saved})).unwrap();
let held: Vec<String> = o.doc().unwrap().doc.variables.bindings[&t].iter().cloned().collect();
assert_eq!(held, ["Price", "Title"]);
let v = o.execute("dataset.select", &json!({"name": "D"})).unwrap();
assert_eq!(v["applied"], json!(2), "{v}");
assert_eq!(plain(&o, t), "9.99");
for name in ["Title", "Price"] {
let v = o.execute("variable.bind", &json!({"variable": name, "ids": [t.0, u.0]})).unwrap();
assert_eq!((&v["bound"], &v["already"]), (&json!(1), &json!([t.0])), "{name}: {v}");
o.execute("edit.undo", &json!({})).unwrap();
}
let e = o.execute("variable.bind", &json!({"variable": "Note", "ids": [t.0]})).unwrap_err().to_string();
assert!(e.contains(&format!("node {} already has the text variable `Title`", t.0)), "{e}");
}
/// `unbound` counts what was actually dropped, like `variable.delete` does.
#[test]
fn unbind_reports_what_it_actually_removed() {
let mut s = session();
let t = text(&mut s, "x");
s.execute("variable.define", &json!({"name": "Title", "kind": "text"})).unwrap();
s.execute("variable.define", &json!({"name": "Price", "kind": "visibility"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Title", "ids": [t.0]})).unwrap();
s.execute("variable.bind", &json!({"variable": "Price", "ids": [t.0]})).unwrap();
assert_eq!(s.execute("variable.unbind", &json!({"ids": [t.0], "variable": "Title"})).unwrap()["unbound"], 1);
assert_eq!(s.execute("variable.unbind", &json!({"ids": [t.0], "variable": "Title"})).unwrap()["unbound"], 0, "already gone");
// Without `variable` the object's last binding goes and the entry with it.
assert_eq!(s.execute("variable.unbind", &json!({"ids": [t.0]})).unwrap()["unbound"], 1);
assert_eq!(s.execute("variable.unbind", &json!({"ids": [t.0]})).unwrap()["unbound"], 0);
// An object with no binding left is not kept as an empty set.
let v: Value = serde_json::from_str(&serde_json::to_string(&s.doc().unwrap().doc).unwrap()).unwrap();
assert!(v["variables"].get("bindings").is_none_or(|v| v == &json!({})), "{v}");
}
/// Locked art is the document's to rewrite only while it isn't locked.
#[test]
fn locked_art_is_neither_bound_nor_rewritten() {
let mut s = session();
let t = text(&mut s, "x");
let r = rect(&mut s);
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("variable.define", &json!({"name": "Show", "kind": "visibility"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).unwrap();
s.execute("variable.bind", &json!({"variable": "Show", "ids": [r.0]})).unwrap();
s.execute("dataset.new", &json!({"name": "D", "values": {"Name": "y", "Show": false}})).unwrap();
s.execute("select.set", &json!({"ids": [t.0]})).unwrap();
s.execute("object.lock", &json!({})).unwrap();
assert!(s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).is_err(), "locked art takes no new binding");
// The bindings made before the lock still count, and the application skips the locked art.
let v = s.execute("dataset.select", &json!({"name": "D"})).unwrap();
assert_eq!((v["applied"].as_u64(), v["skipped"].as_u64()), (Some(1), Some(1)));
assert_eq!(plain(&s, t), "x", "the locked text was left alone");
assert!(!s.doc().unwrap().doc.node(r).unwrap().visible, "the unlocked rectangle still applied");
}
/// The art on a locked artboard acts as locked art: a data set skips it, Update Data Set reads
/// nothing from it, and it takes no new binding. Unlocking the artboard lets the data set rewrite it.
#[test]
fn art_on_a_locked_artboard_is_neither_bound_nor_rewritten() {
let mut s = session();
let t = NodeId(s.execute("text.create", &json!({"x": 2050, "y": 50, "text": "cat"})).unwrap()["id"].as_u64().unwrap());
let fresh = NodeId(s.execute("text.create", &json!({"x": 2100, "y": 100, "text": "new"})).unwrap()["id"].as_u64().unwrap());
let r = rect(&mut s);
s.execute("artboard.new", &json!({"x": 2000, "y": 0, "width": 400, "height": 400})).unwrap();
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("variable.define", &json!({"name": "Show", "kind": "visibility"})).unwrap();
s.execute("variable.define", &json!({"name": "Box", "kind": "visibility"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).unwrap();
s.execute("variable.bind", &json!({"variable": "Show", "ids": [t.0]})).unwrap();
s.execute("variable.bind", &json!({"variable": "Box", "ids": [r.0]})).unwrap();
s.execute("dataset.capture", &json!({"name": "A"})).unwrap();
s.execute("dataset.new", &json!({"name": "B", "values": {"Name": "Bob", "Show": false, "Box": false}})).unwrap();
s.execute("select.none", &json!({})).unwrap();
s.execute("artboard.setProps", &json!({"index": 1, "locked": true})).unwrap();
let e = s.execute("variable.bind", &json!({"variable": "Name", "ids": [fresh.0]})).unwrap_err().to_string();
assert!(e.contains(&format!("node {} is on a locked artboard", fresh.0)), "{e}");
let v = s.execute("dataset.select", &json!({"name": "B"})).unwrap();
assert_eq!((v["applied"].as_u64(), v["skipped"].as_u64()), (Some(1), Some(2)), "{v}");
assert_eq!(plain(&s, t), "cat", "the text on the locked artboard was left alone");
assert!(s.doc().unwrap().doc.node(t).unwrap().visible, "and still shows");
assert!(!s.doc().unwrap().doc.node(r).unwrap().visible, "the rectangle off it applied");
// Update Data Set keeps the row's values for the art it can't read.
let v = s.execute("dataset.update", &json!({})).unwrap();
assert_eq!(v["values"], 1, "{v}");
let row = s.doc().unwrap().doc.variables.dataset("B").unwrap().values.clone();
assert_eq!(row.get("Name"), Some(&vectorcraft_doc::DataValue::Text("Bob".into())));
assert_eq!(row.get("Show"), Some(&vectorcraft_doc::DataValue::Visible(false)));
s.execute("artboard.setProps", &json!({"index": 1, "locked": false})).unwrap();
let v = s.execute("dataset.select", &json!({"name": "B"})).unwrap();
assert_eq!((v["applied"].as_u64(), v["skipped"].as_u64()), (Some(3), Some(0)), "{v}");
assert_eq!(plain(&s, t), "Bob");
}
/// A locked artboard elsewhere in the document does not stop threaded type on an unlocked artboard
/// from having its whole story rewritten, and type on the locked artboard is still left alone.
#[test]
fn threaded_type_on_an_unlocked_artboard_is_rewritten_beside_a_locked_artboard() {
let (mut s, a, b, story) = threaded(200);
let far = NodeId(s.execute("text.create", &json!({"x": 2050, "y": 50, "text": "cat"})).unwrap()["id"].as_u64().unwrap());
s.execute("artboard.new", &json!({"x": 2000, "y": 0, "width": 400, "height": 400})).unwrap();
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("variable.define", &json!({"name": "Tail", "kind": "text"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Name", "ids": [a.0]})).unwrap();
s.execute("variable.bind", &json!({"variable": "Tail", "ids": [far.0]})).unwrap();
s.execute("dataset.new", &json!({"name": "B", "values": {"Name": "Bob", "Tail": "dog"}})).unwrap();
s.execute("select.none", &json!({})).unwrap();
s.execute("artboard.setProps", &json!({"index": 1, "locked": true})).unwrap();
let v = s.execute("dataset.select", &json!({"name": "B"})).unwrap();
assert_eq!((v["applied"].as_u64(), v["skipped"].as_u64()), (Some(1), Some(1)), "{v}");
assert_eq!((plain(&s, a), plain(&s, b)), ("Bob".to_string(), String::new()), "the whole story was rewritten");
assert_eq!(plain(&s, far), "cat", "the text on the locked artboard was left alone");
assert!(!story.is_empty());
}
/// Binding fails for locked art and for art in a hidden or locked group or layer, and the
/// error names which of the three applies.
#[test]
fn binding_errors_say_whether_a_layer_or_group_is_hidden_or_locked() {
let mut s = session();
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
let layer = NodeId(s.execute("layer.new", &json!({})).unwrap()["id"].as_u64().unwrap());
let t = text(&mut s, "x");
let r = rect(&mut s);
let bind = |s: &mut Session| s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).unwrap_err().to_string();
s.execute("layer.setProps", &json!({"id": layer.0, "visible": false})).unwrap();
let e = bind(&mut s);
assert!(e.contains(&format!("node {} is in a hidden group or layer", t.0)), "{e}");
s.execute("layer.setProps", &json!({"id": layer.0, "visible": true})).unwrap();
s.execute("select.set", &json!({"ids": [t.0, r.0]})).unwrap();
s.execute("object.group", &json!({})).unwrap();
s.execute("object.hide", &json!({})).unwrap();
let e = bind(&mut s);
assert!(e.contains(&format!("node {} is in a hidden group or layer", t.0)), "{e}");
s.execute("object.showAll", &json!({})).unwrap();
s.execute("layer.setProps", &json!({"id": layer.0, "locked": true})).unwrap();
let e = bind(&mut s);
assert!(e.contains(&format!("node {} is in a locked group or layer", t.0)), "{e}");
s.execute("layer.setProps", &json!({"id": layer.0, "locked": false})).unwrap();
s.execute("select.set", &json!({"ids": [t.0]})).unwrap();
s.execute("object.lock", &json!({})).unwrap();
let e = bind(&mut s);
assert!(e.contains(&format!("node {} is locked", t.0)), "{e}");
}
/// Make Text Dynamic / Make Visibility Dynamic: what the panel's buttons run. One step from a
/// selection to a bound variable, named after what it drives.
#[test]
fn making_a_selection_dynamic_names_and_binds_it() {
let mut s = session();
let t = text(&mut s, "Alice");
let r = rect(&mut s);
s.execute("select.set", &json!({"ids": [t.0]})).unwrap();
let v = s.execute("variable.makeTextDynamic", &json!({})).unwrap();
assert_eq!(v["name"], "Alice", "named after the text it drives");
assert_eq!(v["ids"], json!([t.0]));
s.execute("select.set", &json!({"ids": [r.0]})).unwrap();
let v = s.execute("variable.makeVisibilityDynamic", &json!({})).unwrap();
assert_eq!(v["kind"], "visibility");
// Type with the same name takes a number rather than colliding with the first variable.
let twin = text(&mut s, "Alice");
s.execute("select.set", &json!({"ids": [twin.0]})).unwrap();
let again = s.execute("variable.makeTextDynamic", &json!({})).unwrap();
assert_eq!(again["name"], "Alice 2");
let vars = s.execute("variable.list", &json!({})).unwrap();
assert_eq!(vars["variables"].as_array().map(Vec::len), Some(3));
// Nothing selected, art that can't take the kind, and locked art are errors.
s.execute("select.none", &json!({})).unwrap();
assert!(s.execute("variable.makeTextDynamic", &json!({})).is_err());
s.execute("select.set", &json!({"ids": [r.0]})).unwrap();
assert!(s.execute("variable.makeTextDynamic", &json!({})).is_err(), "a rectangle is not type");
}
/// Make Text Dynamic reports its errors under its own command id.
#[test]
fn make_dynamic_errors_name_the_command() {
let mut s = session();
let t = text(&mut s, "x");
let r = rect(&mut s);
s.execute("select.set", &json!({"ids": [t.0, r.0]})).unwrap();
s.execute("object.group", &json!({})).unwrap();
let e = s.execute("variable.makeTextDynamic", &json!({})).unwrap_err().to_string();
assert!(e.contains("`variable.makeTextDynamic`") && e.contains("is not type"), "{e}");
}
/// Lock Variables refuses defining, deleting and renaming variables, Make … Dynamic included,
/// while binding, unbinding and the dataset commands still run (an import that would define a
/// variable is refused; see `locked_variables_refuse_header_cells_that_define_variables`).
#[test]
fn locked_variables_refuse_new_deleted_and_renamed_variables() {
let mut s = session();
let t = text(&mut s, "Alice");
let r = rect(&mut s);
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("variable.define", &json!({"name": "Show", "kind": "visibility"})).unwrap();
assert_eq!(s.execute("variable.list", &json!({})).unwrap()["locked"], false);
assert_eq!(s.execute("variable.lock", &json!({})).unwrap()["locked"], true, "without `locked` it toggles");
assert_eq!(s.execute("variable.list", &json!({})).unwrap()["locked"], true);
s.execute("select.set", &json!({"ids": [t.0]})).unwrap();
for (cmd, p) in [
("variable.define", json!({"name": "Other", "kind": "text"})),
("variable.delete", json!({"names": ["Name"]})),
("variable.rename", json!({"name": "Name", "newName": "Renamed"})),
("variable.makeTextDynamic", json!({})),
("variable.makeVisibilityDynamic", json!({})),
] {
let e = s.execute(cmd, &p).unwrap_err();
assert!(matches!(&e, EngineError::Disabled(_, why) if why == "variables are locked"), "{cmd}: {e}");
}
let names = |s: &mut Session| {
s.execute("variable.list", &json!({})).unwrap()["variables"].as_array().unwrap().iter().map(|v| v["name"].clone()).collect::<Vec<_>>()
};
assert_eq!(names(&mut s), [json!("Name"), json!("Show")], "the variables are as they were");
// Binding and unbinding still work, and so do the datasets.
s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).unwrap();
s.execute("variable.bind", &json!({"variable": "Show", "ids": [r.0]})).unwrap();
s.execute("variable.unbind", &json!({"ids": [r.0], "variable": "Show"})).unwrap();
s.execute("dataset.capture", &json!({})).unwrap();
s.execute("dataset.new", &json!({"name": "Bob", "values": {"Name": "Bob"}})).unwrap();
s.execute("dataset.import", &json!({"csv": "Name\nCarol\n"})).unwrap();
s.execute("dataset.select", &json!({"name": "Data Set 1"})).unwrap();
assert_eq!(plain(&s, t), "Carol");
// Unlocking lets a variable be defined again; `locked` sets the state rather than toggling.
assert_eq!(s.execute("variable.lock", &json!({"locked": false})).unwrap()["locked"], false);
assert_eq!(s.execute("variable.lock", &json!({"locked": false})).unwrap()["locked"], false);
s.execute("variable.define", &json!({"name": "Other", "kind": "text"})).unwrap();
}
/// `variable.lock`'s params text names the dataset command it refuses: an import whose header
/// would define a variable.
#[test]
fn the_lock_params_text_names_the_refused_import() {
let params = cmd::find_command("variable.lock").unwrap().params;
assert!(params.contains("the dataset commands still run, except a dataset.import whose header would define a variable"), "{params}");
}
/// Locking is an undo step only when it changes the state, and the lock is saved with the
/// document: a file saved locked opens locked, and a file without the key opens unlocked.
#[test]
fn the_variables_lock_is_an_undo_step_and_is_saved() {
let mut s = session();
let steps = |s: &Session| s.doc().unwrap().history.undo.len();
let before = steps(&s);
s.execute("variable.lock", &json!({"locked": true})).unwrap();
assert_eq!(steps(&s), before + 1);
s.execute("variable.lock", &json!({"locked": true})).unwrap();
assert_eq!(steps(&s), before + 1, "locking locked variables is not a step");
assert!(s.doc().unwrap().is_dirty());
let saved = vectorcraft_format::save(&s.doc().unwrap().doc, false);
assert!(vectorcraft_format::load(&saved).unwrap().variables.locked, "a document saved locked opens locked");
let mut v: Value = serde_json::from_slice(&saved).unwrap();
assert_eq!(v["document"]["variables"]["locked"], true);
v["document"]["variables"].as_object_mut().unwrap().remove("locked");
assert!(!vectorcraft_format::load(&serde_json::to_vec(&v).unwrap()).unwrap().variables.locked, "a file without the key opens unlocked");
s.execute("edit.undo", &json!({})).unwrap();
assert!(!s.doc().unwrap().doc.variables.locked, "undo unlocks");
let v: Value = serde_json::from_slice(&vectorcraft_format::save(&s.doc().unwrap().doc, false)).unwrap();
assert!(v["document"].get("variables").is_none_or(|v| v.get("locked").is_none()), "unlocked variables write no key: {v}");
s.execute("edit.redo", &json!({})).unwrap();
assert!(s.doc().unwrap().doc.variables.locked, "redo locks again");
}
/// Capture Data Set records what the art holds now, which is how rows are made — the art is
/// edited and captured, rather than the values typed.
#[test]
fn capturing_records_what_the_art_holds_now() {
let mut s = session();
let t = text(&mut s, "Alice");
let r = rect(&mut s);
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("variable.define", &json!({"name": "Show", "kind": "visibility"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).unwrap();
s.execute("variable.bind", &json!({"variable": "Show", "ids": [r.0]})).unwrap();
let v = s.execute("dataset.capture", &json!({})).unwrap();
assert_eq!((v["name"].clone(), v["values"].clone()), (json!("Data Set 1"), json!(2)));
let sets = s.execute("dataset.list", &json!({})).unwrap();
assert_eq!(sets["datasets"][0]["values"]["Name"], json!({"text": "Alice"}));
assert_eq!(sets["datasets"][0]["values"]["Show"], json!({"visible": true}));
assert_eq!(sets["active"], "Data Set 1");
// Change the art, capture again: a second row, and the first still matches its own text.
s.execute("text.editRange", &json!({"id": t.0, "insert": "Bob"})).unwrap();
assert_eq!(s.execute("dataset.list", &json!({})).unwrap()["datasets"][0]["matches"], json!(false), "the art moved on");
s.execute("dataset.capture", &json!({})).unwrap();
let sets = s.execute("dataset.list", &json!({})).unwrap();
assert_eq!(sets["datasets"][1]["name"], "Data Set 2");
assert_eq!(sets["datasets"][1]["values"]["Name"], json!({"text": "Bob"}));
assert_eq!(sets["datasets"][0]["matches"], json!(false), "row 1 still describes the old text");
// A named capture, and a name that is taken is an error rather than a second row.
s.execute("dataset.capture", &json!({"name": "Winter"})).unwrap();
assert!(s.execute("dataset.capture", &json!({"name": "Winter"})).is_err());
// Nothing bound is nothing to capture.
let mut empty = session();
empty.execute("variable.define", &json!({"name": "V", "kind": "text"})).unwrap();
assert!(empty.execute("dataset.capture", &json!({})).is_err());
}
/// Update Data Set writes the art's current values into the row that is active.
#[test]
fn updating_writes_the_art_into_the_active_row() {
let mut s = session();
let t = text(&mut s, "Alice");
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).unwrap();
// No active row yet: say so rather than quietly writing into some other one.
assert!(s.execute("dataset.update", &json!({})).is_err());
s.execute("dataset.capture", &json!({})).unwrap();
s.execute("text.editRange", &json!({"id": t.0, "insert": "Bob"})).unwrap();
let v = s.execute("dataset.update", &json!({})).unwrap();
assert_eq!((v["name"].clone(), v["values"].clone()), (json!("Data Set 1"), json!(1)));
assert_eq!(s.execute("dataset.list", &json!({})).unwrap()["datasets"][0]["matches"], json!(true));
// One undo step puts the row back as it was.
s.execute("text.editRange", &json!({"id": t.0, "insert": "Caz"})).unwrap();
s.execute("dataset.update", &json!({})).unwrap();
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(plain(&s, t), "Caz");
}
/// Update Data Set writes only the variables the active row holds, so a row that sets some of
/// them still leaves the others alone when it is selected again. `all: true` adds the rest.
#[test]
fn updating_writes_only_the_variables_the_row_holds() {
let mut s = session();
let t = text(&mut s, "Alice");
let r = rect(&mut s);
s.execute("variable.define", &json!({"name": "Client", "kind": "text"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Client", "ids": [t.0]})).unwrap();
s.execute("variable.define", &json!({"name": "Badge", "kind": "visibility"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Badge", "ids": [r.0]})).unwrap();
s.execute("dataset.new", &json!({"name": "Badge off", "values": {"Badge": false}})).unwrap();
s.execute("dataset.select", &json!({"name": "Badge off"})).unwrap();
s.execute("text.editRange", &json!({"id": t.0, "insert": "Typed by hand"})).unwrap();
s.execute("object.setProps", &json!({"ids": [r.0], "visible": true})).unwrap();
let v = s.execute("dataset.update", &json!({})).unwrap();
assert_eq!((v["values"].clone(), v["left"].clone()), (json!(1), json!(["Client"])), "{v}");
let sets = s.execute("dataset.list", &json!({})).unwrap();
assert_eq!(sets["datasets"][0]["values"], json!({"Badge": {"visible": true}}), "{sets}");
// `all: true` adds every variable the art reports a value for.
let v = s.execute("dataset.update", &json!({"all": true})).unwrap();
assert_eq!((v["values"].clone(), v["left"].clone()), (json!(2), json!([])), "{v}");
let sets = s.execute("dataset.list", &json!({})).unwrap();
assert_eq!(sets["datasets"][0]["values"]["Client"], json!({"text": "Typed by hand"}), "{sets}");
}
/// A variable whose object is locked reports no value, so Update keeps the row's value for it
/// rather than dropping it from the row.
#[test]
fn updating_keeps_a_value_the_art_does_not_report() {
let mut s = session();
let t = text(&mut s, "First");
let r = rect(&mut s);
s.execute("variable.define", &json!({"name": "Label", "kind": "text"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Label", "ids": [t.0]})).unwrap();
s.execute("variable.define", &json!({"name": "Mark", "kind": "visibility"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Mark", "ids": [r.0]})).unwrap();
s.execute("dataset.new", &json!({"name": "Row", "values": {"Label": "Second", "Mark": true}})).unwrap();
s.execute("dataset.select", &json!({"name": "Row"})).unwrap();
s.execute("object.setProps", &json!({"ids": [t.0], "locked": true})).unwrap();
s.execute("object.setProps", &json!({"ids": [r.0], "visible": false})).unwrap();
for params in [json!({}), json!({"all": true})] {
let v = s.execute("dataset.update", &params).unwrap();
assert_eq!((v["values"].clone(), v["left"].clone()), (json!(1), json!(["Label"])), "{params}: {v}");
let sets = s.execute("dataset.list", &json!({})).unwrap();
assert_eq!(sets["datasets"][0]["values"], json!({"Label": {"text": "Second"}, "Mark": {"visible": false}}), "{params}: {sets}");
}
}
/// With every bound object locked, Update keeps the whole row and adds no undo step, and
/// Capture says the bound objects report no value rather than asking for a binding.
#[test]
fn updating_with_every_bound_object_locked_keeps_the_row() {
let mut s = session();
let t = text(&mut s, "First");
s.execute("variable.define", &json!({"name": "Label", "kind": "text"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Label", "ids": [t.0]})).unwrap();
s.execute("dataset.new", &json!({"name": "Row", "values": {"Label": "Second"}})).unwrap();
s.execute("dataset.select", &json!({"name": "Row"})).unwrap();
s.execute("object.setProps", &json!({"ids": [t.0], "locked": true})).unwrap();
let steps = |s: &Session| s.doc().unwrap().history.undo.len();
let before = steps(&s);
for params in [json!({}), json!({"all": true})] {
let v = s.execute("dataset.update", &params).unwrap();
assert_eq!((v["values"].clone(), v["left"].clone()), (json!(0), json!(["Label"])), "{params}: {v}");
let sets = s.execute("dataset.list", &json!({})).unwrap();
assert_eq!(sets["datasets"][0]["values"], json!({"Label": {"text": "Second"}}), "{params}: {sets}");
}
assert_eq!(steps(&s), before, "an update that writes nothing is not a step");
let e = s.execute("dataset.capture", &json!({})).unwrap_err().to_string();
assert!(e.contains("no bound object reports a value"), "{e}");
assert_eq!(s.execute("dataset.list", &json!({})).unwrap()["datasets"].as_array().unwrap().len(), 1);
// With nothing bound at all, both still ask for a binding.
s.execute("variable.unbind", &json!({"ids": [t.0]})).unwrap();
for cmd in ["dataset.update", "dataset.capture"] {
let e = s.execute(cmd, &json!({})).unwrap_err().to_string();
assert!(e.contains("bind something to a variable first"), "{cmd}: {e}");
}
}
/// A variable defined after a row was captured stays out of that row on Update, and joins it
/// with `all: true`.
#[test]
fn a_variable_defined_after_a_row_joins_it_only_with_all() {
let mut s = session();
let name = text(&mut s, "First job");
let note = text(&mut s, "First note");
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Name", "ids": [name.0]})).unwrap();
s.execute("dataset.capture", &json!({"name": "Job 1"})).unwrap();
s.execute("variable.define", &json!({"name": "Note", "kind": "text"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Note", "ids": [note.0]})).unwrap();
s.execute("text.editRange", &json!({"id": name.0, "insert": "Second job"})).unwrap();
let v = s.execute("dataset.update", &json!({})).unwrap();
assert_eq!((v["values"].clone(), v["left"].clone()), (json!(1), json!(["Note"])), "{v}");
let sets = s.execute("dataset.list", &json!({})).unwrap();
assert_eq!(sets["datasets"][0]["values"], json!({"Name": {"text": "Second job"}}), "{sets}");
s.execute("dataset.update", &json!({"all": true})).unwrap();
let sets = s.execute("dataset.list", &json!({})).unwrap();
assert_eq!(sets["datasets"][0]["values"], json!({"Name": {"text": "Second job"}, "Note": {"text": "First note"}}), "{sets}");
}
/// Renaming carries the variable's bindings and its values in every row with it — a name is
/// how a variable is referred to, so leaving the old one behind would orphan both.
#[test]
fn renaming_carries_the_bindings_and_values_with_it() {
let mut s = session();
let t = text(&mut s, "Alice");
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).unwrap();
s.execute("dataset.new", &json!({"name": "D", "values": {"Name": "Bob"}})).unwrap();
s.execute("dataset.select", &json!({"name": "D"})).unwrap();
s.execute("variable.rename", &json!({"name": "Name", "newName": "Title"})).unwrap();
assert_eq!(plain(&s, t), "Bob", "the row still applied");
assert_eq!(s.execute("dataset.list", &json!({})).unwrap()["datasets"][0]["values"]["Title"], json!({"text": "Bob"}));
let vars = s.execute("variable.list", &json!({})).unwrap();
assert_eq!((vars["variables"][0]["name"].clone(), vars["variables"][0]["bindings"].clone()), (json!("Title"), json!(1)));
// Renaming a dataset keeps it active and keeps its values.
s.execute("dataset.rename", &json!({"name": "D", "newName": "Row 1"})).unwrap();
let sets = s.execute("dataset.list", &json!({})).unwrap();
assert_eq!((sets["active"].clone(), sets["datasets"][0]["name"].clone()), (json!("Row 1"), json!("Row 1")));
// Taken and missing names are errors.
s.execute("variable.define", &json!({"name": "Other", "kind": "text"})).unwrap();
assert!(s.execute("variable.rename", &json!({"name": "Title", "newName": "Other"})).is_err());
assert!(s.execute("variable.rename", &json!({"name": "Ghost", "newName": "X"})).is_err());
assert!(s.execute("dataset.rename", &json!({"name": "Ghost", "newName": "X"})).is_err());
// A dataset and a variable are separate namespaces: a dataset may take a variable's name,
// and a variable may take a dataset's.
s.execute("dataset.rename", &json!({"name": "Row 1", "newName": "Title"})).unwrap();
assert_eq!(s.execute("dataset.list", &json!({})).unwrap()["active"], "Title");
s.execute("dataset.new", &json!({"name": "Taken"})).unwrap();
assert!(s.execute("dataset.rename", &json!({"name": "Title", "newName": "Taken"})).is_err());
}
/// Renaming a name that does not exist onto one that does reports the missing name, the same
/// error a rename to a free name gives.
#[test]
fn renaming_a_missing_name_onto_a_taken_one_reports_the_missing_name() {
let mut s = session();
s.execute("variable.define", &json!({"name": "A", "kind": "text"})).unwrap();
s.execute("dataset.new", &json!({"name": "A"})).unwrap();
let e = s.execute("variable.rename", &json!({"name": "Nope", "newName": "A"})).unwrap_err().to_string();
assert!(e.contains("no variable named `Nope`"), "{e}");
let e = s.execute("dataset.rename", &json!({"name": "Nope", "newName": "A"})).unwrap_err().to_string();
assert!(e.contains("no dataset named `Nope`"), "{e}");
}
/// `dataset.set` is how a value changes after the row exists: replacing it must not need the
/// dataset to be deleted and rebuilt (which loses its place in the list and the active one).
#[test]
fn dataset_set_replaces_the_row_and_reports_what_it_dropped() {
let mut s = session();
let t = text(&mut s, "x");
let r = rect(&mut s);
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("variable.define", &json!({"name": "Show", "kind": "visibility"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).unwrap();
s.execute("variable.bind", &json!({"variable": "Show", "ids": [r.0]})).unwrap();
s.execute("dataset.new", &json!({"name": "D", "values": {"Name": "y", "Show": false}})).unwrap();
// The dataset keeps its place and stays active while its values change.
let v = s.execute("dataset.set", &json!({"name": "D", "values": {"Name": "z"}})).unwrap();
assert_eq!((v["values"].clone(), v["removed"].clone()), (json!(1), json!(["Show"])));
let sets = s.execute("dataset.list", &json!({})).unwrap();
assert_eq!(sets["datasets"].as_array().map(Vec::len), Some(1));
assert_eq!(sets["datasets"][0]["values"], json!({"Name": {"text": "z"}}));
s.execute("dataset.select", &json!({"name": "D"})).unwrap();
assert_eq!(plain(&s, t), "z");
assert!(s.doc().unwrap().doc.node(r).unwrap().visible, "`Show` has no value in this row now");
// Errors are errors, not a silent rewrite of some other row.
assert!(s.execute("dataset.set", &json!({"name": "Missing"})).is_err());
assert!(s.execute("dataset.set", &json!({"name": "D", "values": {"Ghost": "z"}})).is_err());
assert!(s.execute("dataset.set", &json!({"name": "D", "values": {"Show": "yes"}})).is_err());
assert!(s.execute("dataset.set", &json!({"name": "D", "values": []})).is_err());
// One step, so one undo puts the row back as it was.
s.execute("dataset.set", &json!({"name": "D", "values": {"Name": "w"}})).unwrap();
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(plain(&s, t), "z");
}
/// `newName` renames the row in the same step as its values change, and keeps it active.
#[test]
fn dataset_set_with_a_new_name_renames_the_row() {
let mut s = session();
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("dataset.new", &json!({"name": "D", "values": {"Name": "y"}})).unwrap();
s.execute("dataset.new", &json!({"name": "E", "values": {"Name": "e"}})).unwrap();
s.execute("dataset.select", &json!({"name": "D"})).unwrap();
let undo = s.doc().unwrap().history.undo.len();
let v = s.execute("dataset.set", &json!({"name": "D", "newName": "Spring", "values": {"Name": "z"}})).unwrap();
assert_eq!(v["name"], "Spring");
let sets = s.execute("dataset.list", &json!({})).unwrap();
assert_eq!((sets["datasets"][0]["name"].clone(), sets["active"].clone()), (json!("Spring"), json!("Spring")));
assert_eq!(sets["datasets"][0]["values"], json!({"Name": {"text": "z"}}));
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1, "one undo step");
// Its own name is no rename; another row's name, or an empty one, is refused and changes nothing.
s.execute("dataset.set", &json!({"name": "Spring", "newName": "Spring", "values": {"Name": "z"}})).unwrap();
for bad in [json!("E"), json!(""), json!(3)] {
assert!(s.execute("dataset.set", &json!({"name": "Spring", "newName": bad, "values": {"Name": "x"}})).is_err(), "{bad}");
}
let sets = s.execute("dataset.list", &json!({})).unwrap();
assert_eq!(sets["datasets"][0]["values"], json!({"Name": {"text": "z"}}));
assert_eq!(sets["datasets"][1]["values"], json!({"Name": {"text": "e"}}));
// The set that changed nothing added no step: one undo takes the rename back.
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(s.execute("dataset.list", &json!({})).unwrap()["active"], "D", "undo puts the name back");
}
#[test]
fn bad_variables_requests_are_errors_not_panics() {
let mut s = session();
s.execute("variable.define", &json!({"name": "N", "kind": "text"})).unwrap();
for (cmd, params) in [
("variable.define", json!({})),
("variable.define", json!({"name": "", "kind": "text"})),
("variable.define", json!({"name": "N", "kind": "colour"})),
("variable.define", json!({"name": "N", "kind": "text"})),
("variable.delete", json!({})),
("variable.bind", json!({})),
("variable.bind", json!({"variable": "N"})),
("variable.bind", json!({"variable": "Missing", "ids": [1]})),
("dataset.new", json!({})),
("dataset.new", json!({"name": "D", "values": []})),
("dataset.new", json!({"name": "D", "values": {"N": 5}})),
("dataset.select", json!({"name": "Missing"})),
("dataset.next", json!({})),
] {
assert!(s.execute(cmd, &params).is_err(), "{cmd} {params}");
}
}
#[test]
fn highlight_selects_without_unbinding_and_deletes_leave_the_rest() {
// The reviewer's regression case: two variables, two rows.
let mut s = session();
let a = text(&mut s, "Alice");
let b = text(&mut s, "Bob");
for (name, id) in [("Name", a), ("Other", b)] {
s.execute("variable.define", &json!({"name": name, "kind": "text"})).unwrap();
s.execute("variable.bind", &json!({"variable": name, "ids": [id.0]})).unwrap();
}
s.execute("dataset.new", &json!({"name": "One", "values": {"Name": "Alice", "Other": "Bob"}})).unwrap();
s.execute("dataset.new", &json!({"name": "Two", "values": {"Name": "A2", "Other": "B2"}})).unwrap();
s.execute("dataset.select", &json!({"name": "One"})).unwrap();
// What a row click runs (highlight), then what Select Bound Object runs (select).
s.execute("variable.highlight", &json!({"variable": "Name"})).unwrap();
let bound: Vec<u64> = s.doc().unwrap().doc.variables.objects_of("Name").iter().map(|i| i.0).collect();
s.execute("select.set", &json!({"ids": bound})).unwrap();
assert_eq!(s.doc().unwrap().selection.objects, vec![a]);
// …and the binding is still there: selecting never unbinds.
assert_eq!(s.doc().unwrap().doc.variables.objects_of("Name"), vec![a]);
// Deleting one variable leaves the other; deleting the current dataset leaves the other.
s.execute("variable.delete", &json!({"names": ["Name"]})).unwrap();
let vars = s.execute("variable.list", &json!({})).unwrap();
assert_eq!(vars["variables"].as_array().unwrap().len(), 1);
assert_eq!(vars["variables"][0]["name"], "Other");
s.execute("dataset.delete", &json!({"names": ["One"]})).unwrap();
let sets = s.execute("dataset.list", &json!({})).unwrap();
assert_eq!(sets["datasets"].as_array().unwrap().len(), 1);
assert_eq!(sets["datasets"][0]["name"], "Two");
}
#[test]
fn highlight_follows_rename_and_clears_with_delete() {
let mut s = session();
let t = text(&mut s, "Alice");
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).unwrap();
assert!(s.execute("variable.highlight", &json!({"variable": "Missing"})).is_err(), "unknown names stay errors");
let v = s.execute("variable.highlight", &json!({"variable": "Name"})).unwrap();
assert_eq!(v["variable"], "Name");
// The highlight names a variable, so the rename carries it along.
s.execute("variable.rename", &json!({"name": "Name", "newName": "Title"})).unwrap();
assert_eq!(s.doc().unwrap().variables_highlight.as_deref(), Some("Title"));
// Empty clears it, and deleting the highlighted variable clears it too.
s.execute("variable.highlight", &json!({})).unwrap();
assert!(s.doc().unwrap().variables_highlight.is_none());
s.execute("variable.highlight", &json!({"variable": "Title"})).unwrap();
s.execute("variable.delete", &json!({"names": ["Title"]})).unwrap();
assert!(s.doc().unwrap().variables_highlight.is_none(), "a highlight of a deleted variable highlights nothing");
}
/// Undo of a rename takes the new name away, so the highlight that followed the rename goes
/// too: a variable defined later under that name is not highlighted, and Delete without names
/// does not delete it.
#[test]
fn undoing_a_rename_clears_the_highlight_of_the_new_name() {
let mut s = session();
s.execute("variable.define", &json!({"name": "A", "kind": "text"})).unwrap();
s.execute("variable.highlight", &json!({"variable": "A"})).unwrap();
s.execute("variable.rename", &json!({"name": "A", "newName": "B"})).unwrap();
s.execute("edit.undo", &json!({})).unwrap();
assert!(s.doc().unwrap().variables_highlight.is_none());
s.execute("variable.define", &json!({"name": "B", "kind": "visibility"})).unwrap();
assert!(s.doc().unwrap().variables_highlight.is_none(), "B was never clicked");
assert!(s.execute("variable.delete", &json!({})).is_err(), "no variable highlighted");
let vars = s.execute("variable.list", &json!({})).unwrap();
assert_eq!(vars["variables"].as_array().unwrap().len(), 2);
}
/// Undo of New Variable, Redo of Delete Variables and a canceled undo group each leave the
/// highlighted name undefined, and the highlight goes with it.
#[test]
fn undo_redo_and_a_canceled_group_clear_a_highlight_of_a_variable_they_remove() {
let mut s = session();
s.execute("variable.define", &json!({"name": "N", "kind": "text"})).unwrap();
s.execute("variable.highlight", &json!({"variable": "N"})).unwrap();
s.execute("edit.undo", &json!({})).unwrap();
assert!(s.doc().unwrap().variables_highlight.is_none(), "undo of New Variable");
s.execute("edit.redo", &json!({})).unwrap();
s.execute("variable.delete", &json!({"names": ["N"]})).unwrap();
s.execute("edit.undo", &json!({})).unwrap();
s.execute("variable.highlight", &json!({"variable": "N"})).unwrap();
s.execute("edit.redo", &json!({})).unwrap();
assert!(s.doc().unwrap().variables_highlight.is_none(), "redo of Delete Variables");
s.execute("edit.undoGroup.begin", &json!({})).unwrap();
s.execute("variable.define", &json!({"name": "G", "kind": "text"})).unwrap();
s.execute("variable.highlight", &json!({"variable": "G"})).unwrap();
s.execute("edit.undoGroup.end", &json!({"cancel": true})).unwrap();
assert!(s.doc().unwrap().doc.variables.variable("G").is_none(), "the cancel took G back");
assert!(s.doc().unwrap().variables_highlight.is_none(), "a canceled undo group");
}
/// A row that hides an object must be able to show it again, and hiding it on the artboard
/// (as the Layers panel does) is what a capture records: a visibility variable reads and
/// writes its object's own visibility, which therefore never makes the object off limits.
#[test]
fn a_hidden_object_comes_back_and_is_captured_hidden() {
let mut s = session();
let t = text(&mut s, "Alice");
let r = rect(&mut s);
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("variable.define", &json!({"name": "Show", "kind": "visibility"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).unwrap();
s.execute("variable.bind", &json!({"variable": "Show", "ids": [r.0, t.0]})).unwrap();
s.execute("dataset.new", &json!({"name": "Shown", "values": {"Name": "Alice", "Show": true}})).unwrap();
s.execute("dataset.new", &json!({"name": "Hidden", "values": {"Name": "Bob", "Show": false}})).unwrap();
s.execute("dataset.select", &json!({"name": "Hidden"})).unwrap();
assert!(!s.doc().unwrap().doc.node(r).unwrap().visible);
let v = s.execute("dataset.select", &json!({"name": "Shown"})).unwrap();
assert_eq!(v["skipped"], json!(0), "{v}");
assert!(s.doc().unwrap().doc.node(r).unwrap().visible, "the row shows it again");
assert_eq!(plain(&s, t), "Alice", "and rewrites the text of the object it had hidden");
// Hidden on the artboard, then captured: the row says hidden, and it matches the art.
s.execute("select.set", &json!({"ids": [t.0, r.0]})).unwrap();
s.execute("object.hide", &json!({})).unwrap();
s.execute("dataset.capture", &json!({"name": "Captured"})).unwrap();
let sets = s.execute("dataset.list", &json!({})).unwrap();
let captured = sets["datasets"].as_array().unwrap().iter().find(|d| d["name"] == "Captured").unwrap().clone();
assert_eq!(captured["values"]["Show"], json!({"visible": false}), "{captured}");
assert_eq!(captured["matches"], json!(true));
// A hidden object can still be bound by id.
s.execute("variable.define", &json!({"name": "More", "kind": "visibility"})).unwrap();
s.execute("variable.bind", &json!({"variable": "More", "ids": [r.0]})).unwrap();
}
/// A row selected while a layer is hidden rewrites the type on it and applies its other values,
/// and leaves the layer hidden: showing the layer shows that row's text. A layer that is locked
/// as well as hidden is still left alone.
#[test]
fn a_row_rewrites_type_on_a_hidden_layer_but_not_on_a_locked_one() {
let mut s = session();
let layer = NodeId(s.execute("layer.new", &json!({})).unwrap()["id"].as_u64().unwrap());
let t = text(&mut s, "Alice");
let r = rect(&mut s);
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("variable.define", &json!({"name": "Show", "kind": "visibility"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).unwrap();
s.execute("variable.bind", &json!({"variable": "Show", "ids": [r.0]})).unwrap();
s.execute("dataset.new", &json!({"name": "A", "values": {"Name": "Alice", "Show": true}})).unwrap();
s.execute("dataset.new", &json!({"name": "B", "values": {"Name": "Bob", "Show": false}})).unwrap();
s.execute("dataset.select", &json!({"name": "A"})).unwrap();
s.execute("layer.setProps", &json!({"id": layer.0, "visible": false})).unwrap();
let v = s.execute("dataset.select", &json!({"name": "B"})).unwrap();
assert_eq!((v["applied"].as_u64(), v["skipped"].as_u64()), (Some(2), Some(0)), "{v}");
let doc = &s.doc().unwrap().doc;
assert_eq!(plain(&s, t), "Bob", "the type on the hidden layer holds the row's text");
assert!(!doc.node(r).unwrap().visible, "the row's visibility value applies on the hidden layer");
assert!(!doc.node(layer).unwrap().visible, "the row leaves the layer hidden");
let sets = s.execute("dataset.list", &json!({})).unwrap();
let matching: Vec<&Value> = sets["datasets"].as_array().unwrap().iter().filter(|d| d["matches"] == json!(true)).map(|d| &d["name"]).collect();
assert_eq!(matching, [&json!("B")], "only the row the hidden art holds matches: {sets}");
s.execute("layer.setProps", &json!({"id": layer.0, "visible": true})).unwrap();
assert_eq!(plain(&s, t), "Bob", "showing the layer shows the row's text");
// Locked and hidden: the row skips both objects and the type keeps its text.
s.execute("layer.setProps", &json!({"id": layer.0, "visible": false, "locked": true})).unwrap();
let v = s.execute("dataset.select", &json!({"name": "A"})).unwrap();
assert_eq!((v["applied"].as_u64(), v["skipped"].as_u64()), (Some(0), Some(2)), "{v}");
assert_eq!(plain(&s, t), "Bob", "the type on the locked layer keeps its text");
assert!(!s.doc().unwrap().doc.node(r).unwrap().visible);
}
/// Type inside a hidden group is rewritten too, and List and Capture Data Set read it.
#[test]
fn a_row_rewrites_type_in_a_hidden_group_and_capture_reads_it() {
let mut s = session();
let t = text(&mut s, "Alice");
let other = rect(&mut s);
let g = group(&mut s, &[t, other]);
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).unwrap();
s.execute("dataset.new", &json!({"name": "A", "values": {"Name": "Alice"}})).unwrap();
s.execute("dataset.new", &json!({"name": "B", "values": {"Name": "Bob"}})).unwrap();
s.execute("object.setProps", &json!({"ids": [g.0], "visible": false})).unwrap();
let v = s.execute("dataset.select", &json!({"name": "B"})).unwrap();
assert_eq!((v["applied"].as_u64(), v["skipped"].as_u64()), (Some(1), Some(0)), "{v}");
assert_eq!(plain(&s, t), "Bob");
assert!(!s.doc().unwrap().doc.node(g).unwrap().visible, "the group stays hidden");
let sets = s.execute("dataset.list", &json!({})).unwrap();
let matches: Vec<&Value> = sets["datasets"].as_array().unwrap().iter().map(|d| &d["matches"]).collect();
assert_eq!(matches, [&json!(false), &json!(true)], "the art holds B's text, not A's: {sets}");
let v = s.execute("dataset.capture", &json!({"name": "C"})).unwrap();
let sets = s.execute("dataset.list", &json!({})).unwrap();
assert_eq!(sets["datasets"][2]["values"], json!({"Name": {"text": "Bob"}}), "{v}: {sets}");
}
/// A row that hides selected objects deselects them, the key object included. Undo shows them
/// and selects them again with the key object.
#[test]
fn a_row_that_hides_selected_objects_deselects_them() {
let mut s = session();
let (r, q) = (rect(&mut s), rect(&mut s));
s.execute("variable.define", &json!({"name": "Show", "kind": "visibility"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Show", "ids": [r.0]})).unwrap();
s.execute("dataset.new", &json!({"name": "Hidden", "values": {"Show": false}})).unwrap();
s.execute("select.set", &json!({"ids": [r.0, q.0]})).unwrap();
s.execute("select.key", &json!({"id": r.0})).unwrap();
s.execute("dataset.select", &json!({"name": "Hidden"})).unwrap();
assert!(!s.doc().unwrap().doc.node(r).unwrap().visible);
let sel = s.doc().unwrap().selection.clone();
assert_eq!((sel.objects, sel.key), (vec![q], None), "r, the key object, is hidden");
s.execute("edit.undo", &json!({})).unwrap();
assert!(s.doc().unwrap().doc.node(r).unwrap().visible);
let sel = s.doc().unwrap().selection.clone();
assert_eq!((sel.objects, sel.key), (vec![r, q], Some(r)), "undo selects r again as the key object");
}
/// A row that shows or hides a group also sets the objects inside it in the same selection,
/// whichever variable was defined first.
#[test]
fn a_row_sets_the_objects_inside_a_group_it_shows_or_hides() {
let mut s = session();
let (inner, other) = (rect(&mut s), rect(&mut s));
let g = group(&mut s, &[inner, other]);
s.execute("variable.define", &json!({"name": "Inner", "kind": "visibility"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Inner", "ids": [inner.0]})).unwrap();
s.execute("variable.define", &json!({"name": "Set", "kind": "visibility"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Set", "ids": [g.0]})).unwrap();
s.execute("dataset.new", &json!({"name": "Hidden", "values": {"Inner": false, "Set": false}})).unwrap();
s.execute("dataset.new", &json!({"name": "Shown", "values": {"Inner": true, "Set": true}})).unwrap();
let v = s.execute("dataset.select", &json!({"name": "Hidden"})).unwrap();
assert_eq!((v["applied"].as_u64(), v["skipped"].as_u64()), (Some(2), Some(0)), "{v}");
let v = s.execute("dataset.select", &json!({"name": "Shown"})).unwrap();
assert_eq!((v["applied"].as_u64(), v["skipped"].as_u64()), (Some(2), Some(0)), "{v}");
let doc = &s.doc().unwrap().doc;
assert!(doc.node(g).unwrap().visible && doc.node(inner).unwrap().visible, "the group and the object inside it show");
let sets = s.execute("dataset.list", &json!({})).unwrap();
let matching: Vec<&Value> = sets["datasets"].as_array().unwrap().iter().filter(|d| d["matches"] == json!(true)).map(|d| &d["name"]).collect();
assert_eq!(matching, [&json!("Shown")], "{sets}");
// With the inner object's variable defined last, a row that hides both still hides it.
s.execute("variable.delete", &json!({"names": ["Inner"]})).unwrap();
s.execute("variable.define", &json!({"name": "Inner", "kind": "visibility"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Inner", "ids": [inner.0]})).unwrap();
s.execute("dataset.set", &json!({"name": "Hidden", "values": {"Inner": false, "Set": false}})).unwrap();
let v = s.execute("dataset.select", &json!({"name": "Hidden"})).unwrap();
assert_eq!((v["applied"].as_u64(), v["skipped"].as_u64()), (Some(2), Some(0)), "{v}");
let doc = &s.doc().unwrap().doc;
assert!(!doc.node(g).unwrap().visible && !doc.node(inner).unwrap().visible, "both are hidden");
}
/// A row writes the text of type inside a group that it shows or hides, whichever variable was
/// defined first.
#[test]
fn a_row_writes_type_inside_a_group_it_shows_or_hides() {
let mut s = session();
let t = text(&mut s, "Alice");
let other = rect(&mut s);
let g = group(&mut s, &[t, other]);
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).unwrap();
s.execute("variable.define", &json!({"name": "Set", "kind": "visibility"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Set", "ids": [g.0]})).unwrap();
s.execute("dataset.new", &json!({"name": "Hidden", "values": {"Name": "Bob", "Set": false}})).unwrap();
s.execute("dataset.new", &json!({"name": "Shown", "values": {"Name": "Carol", "Set": true}})).unwrap();
s.execute("dataset.select", &json!({"name": "Hidden"})).unwrap();
assert_eq!(plain(&s, t), "Bob");
let v = s.execute("dataset.select", &json!({"name": "Shown"})).unwrap();
assert_eq!((v["applied"].as_u64(), v["skipped"].as_u64()), (Some(2), Some(0)), "{v}");
assert_eq!(plain(&s, t), "Carol", "the text is written although the group started hidden");
// With the text variable defined last, a row that hides the group still writes the text.
s.execute("variable.delete", &json!({"names": ["Name"]})).unwrap();
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).unwrap();
s.execute("dataset.set", &json!({"name": "Hidden", "values": {"Name": "Bob", "Set": false}})).unwrap();
let v = s.execute("dataset.select", &json!({"name": "Hidden"})).unwrap();
assert_eq!((v["applied"].as_u64(), v["skipped"].as_u64()), (Some(2), Some(0)), "{v}");
assert_eq!(plain(&s, t), "Bob", "the text is written before the group is hidden");
assert!(!s.doc().unwrap().doc.node(g).unwrap().visible);
}
/// Of two visibility variables on one object, the one defined later decides whether it shows.
#[test]
fn the_later_visibility_variable_on_an_object_wins() {
let mut s = session();
let r = rect(&mut s);
for name in ["First", "Second"] {
s.execute("variable.define", &json!({"name": name, "kind": "visibility"})).unwrap();
s.execute("variable.bind", &json!({"variable": name, "ids": [r.0]})).unwrap();
}
s.execute("dataset.new", &json!({"name": "Hide", "values": {"First": true, "Second": false}})).unwrap();
s.execute("dataset.new", &json!({"name": "Show", "values": {"First": false, "Second": true}})).unwrap();
let v = s.execute("dataset.select", &json!({"name": "Hide"})).unwrap();
assert_eq!(v["applied"], json!(2), "{v}");
assert!(!s.doc().unwrap().doc.node(r).unwrap().visible);
s.execute("dataset.select", &json!({"name": "Show"})).unwrap();
assert!(s.doc().unwrap().doc.node(r).unwrap().visible);
}
/// Variables and datasets are separate namespaces: renaming a variable leaves a dataset of
/// the same name, and which dataset is active, alone.
#[test]
fn renaming_a_variable_leaves_a_dataset_of_the_same_name_active() {
let mut s = session();
let t = text(&mut s, "Alice");
s.execute("variable.define", &json!({"name": "Title", "kind": "text"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Title", "ids": [t.0]})).unwrap();
s.execute("dataset.capture", &json!({"name": "Title"})).unwrap();
s.execute("variable.rename", &json!({"name": "Title", "newName": "Heading"})).unwrap();
let sets = s.execute("dataset.list", &json!({})).unwrap();
assert_eq!((sets["active"].clone(), sets["datasets"][0]["name"].clone()), (json!("Title"), json!("Title")));
s.execute("dataset.update", &json!({})).unwrap();
}
/// Without `names`, the deletes act on what the panel shows as current: the highlighted
/// variable and the active dataset (Window › Variables runs them with no params).
#[test]
fn deletes_without_names_take_the_highlight_and_the_active_dataset() {
let mut s = session();
let t = text(&mut s, "Alice");
for name in ["A", "B"] {
s.execute("variable.define", &json!({"name": name, "kind": "text"})).unwrap();
}
s.execute("variable.bind", &json!({"variable": "A", "ids": [t.0]})).unwrap();
s.execute("dataset.capture", &json!({})).unwrap();
s.execute("dataset.capture", &json!({})).unwrap();
assert!(s.execute("variable.delete", &json!({})).is_err(), "nothing highlighted");
s.execute("variable.highlight", &json!({"variable": "B"})).unwrap();
assert_eq!(s.execute("variable.delete", &json!({})).unwrap()["deleted"], json!(1));
let vars = s.execute("variable.list", &json!({})).unwrap();
assert_eq!(vars["variables"].as_array().map(Vec::len), Some(1));
assert_eq!(vars["variables"][0]["name"], "A");
assert_eq!(s.execute("dataset.delete", &json!({})).unwrap()["deleted"], json!(1));
let sets = s.execute("dataset.list", &json!({})).unwrap();
assert_eq!(sets["datasets"].as_array().map(Vec::len), Some(1));
assert_eq!(sets["datasets"][0]["name"], "Data Set 1");
assert!(s.execute("dataset.delete", &json!({})).is_err(), "no active dataset left");
}
/// Ids and names from an agent are checked, never reinterpreted: an `ids` that isn't a list
/// of objects is an error rather than the selection, and names are trimmed and capped.
#[test]
fn ids_and_names_from_an_agent_are_checked() {
let mut s = session();
let t = text(&mut s, "Alice");
s.execute("variable.define", &json!({"name": " Name ", "kind": "text"})).unwrap();
assert_eq!(s.execute("variable.list", &json!({})).unwrap()["variables"][0]["name"], "Name");
s.execute("select.set", &json!({"ids": [t.0]})).unwrap();
for params in
[json!({"variable": "Name", "ids": 5}), json!({"variable": "Name", "ids": [t.0, "x"]}), json!({"variable": "Name", "ids": [999_999]})]
{
assert!(s.execute("variable.bind", &params).is_err(), "{params}");
}
assert!(s.doc().unwrap().doc.variables.objects_of("Name").is_empty(), "nothing was bound");
assert!(s.execute("variable.unbind", &json!({"ids": "all"})).is_err());
assert!(s.execute("variable.define", &json!({"name": "x".repeat(256), "kind": "text"})).is_err());
s.execute("variable.define", &json!({"name": "x".repeat(255), "kind": "text"})).unwrap();
assert!(s.execute("variable.delete", &json!({"names": []})).is_err());
assert!(s.execute("variable.delete", &json!({"names": [5]})).is_err());
}
/// Image variables swap the linked file and keep the object's bounds; capture records the path.
#[test]
fn image_datasets_swap_pixels_and_keep_bounds() {
let dir = Folder::new("vars-image");
let red_path = dir.file("red.png");
let blue_path = dir.file("blue.png");
write(&red_path, &png(40, 30, RED));
write(&blue_path, &png(80, 60, BLUE));
let mut s = session();
let id = place(&mut s, &red_path);
let before = bounds(&s, id);
s.execute("variable.define", &json!({"name": "Photo", "kind": "image"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Photo", "ids": [id.0]})).unwrap();
// A text object can't take an image variable; a path must be a non-empty string.
let t = text(&mut s, "x");
assert!(s.execute("variable.bind", &json!({"variable": "Photo", "ids": [t.0]})).is_err());
assert!(s.execute("dataset.new", &json!({"name": "B", "values": {"Photo": ""}})).is_err());
assert!(s.execute("dataset.new", &json!({"name": "B", "values": {"Photo": true}})).is_err());
s.execute("dataset.new", &json!({"name": "Blue", "values": {"Photo": blue_path}})).unwrap();
let v = s.execute("dataset.select", &json!({"name": "Blue"})).unwrap();
assert_eq!((v["applied"].as_u64(), v["skipped"].as_u64()), (Some(1), Some(0)));
assert_eq!(image(&s, id).link.as_ref().map(|l| l.path.as_str()), Some(blue_path.as_str()));
assert_eq!(bounds(&s, id), before, "bounds stay when the file swaps");
// Capture records the path the art holds now; a missing file is skipped, not an abort.
s.execute("dataset.capture", &json!({"name": "Now"})).unwrap();
let sets = s.execute("dataset.list", &json!({})).unwrap();
let now = sets["datasets"].as_array().unwrap().iter().find(|d| d["name"] == "Now").unwrap();
assert_eq!(now["values"]["Photo"], json!({"image": blue_path}));
let gone = dir.file("gone.png");
s.execute("dataset.new", &json!({"name": "Missing", "values": {"Photo": gone}})).unwrap();
let skip = s.execute("dataset.select", &json!({"name": "Missing"})).unwrap();
assert_eq!((skip["applied"].as_u64(), skip["skipped"].as_u64()), (Some(0), Some(1)));
assert_eq!(image(&s, id).link.as_ref().map(|l| l.path.as_str()), Some(blue_path.as_str()));
// Make Image Dynamic: one step from a selection to a bound image variable.
let id2 = place(&mut s, &red_path);
s.execute("select.set", &json!({"ids": [id2.0]})).unwrap();
let made = s.execute("variable.makeImageDynamic", &json!({})).unwrap();
assert_eq!(made["kind"], "image");
assert_eq!(made["ids"], json!([id2.0]));
s.execute("select.set", &json!({"ids": [t.0]})).unwrap();
assert!(s.execute("variable.makeImageDynamic", &json!({})).is_err(), "type is not an image");
}
/// A text value with commas, quotes and line breaks comes back from the exported CSV as it went out.
#[test]
fn datasets_csv_keeps_line_breaks_in_quoted_cells() {
let mut s = session();
let t = text(&mut s, "One");
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).unwrap();
let value = "Line one, \"quoted\"\nLine two";
s.execute("dataset.new", &json!({"name": "A", "values": {"Name": value}})).unwrap();
let csv = s.execute("dataset.export", &json!({})).unwrap()["csv"].as_str().unwrap().to_string();
s.execute("dataset.import", &json!({"csv": csv})).unwrap();
let sets = &s.doc().unwrap().doc.variables.datasets;
assert_eq!(sets.len(), 1);
assert_eq!(sets[0].values.get("Name"), Some(&vectorcraft_doc::DataValue::Text(value.into())));
assert!(s.execute("dataset.import", &json!({"csv": "Name\n\"open"})).is_err(), "unclosed quote");
}
/// An empty text value is not a missing value: it comes back from the exported CSV as an empty
/// text, so selecting the data set still clears the bound text.
#[test]
fn datasets_csv_keeps_an_empty_text_value() {
let mut s = session();
let t = text(&mut s, "One");
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).unwrap();
s.execute("dataset.new", &json!({"name": "A", "values": {"Name": ""}})).unwrap();
let csv = s.execute("dataset.export", &json!({})).unwrap()["csv"].as_str().unwrap().to_string();
s.execute("dataset.import", &json!({"csv": csv})).unwrap();
let value = |s: &Session, name: &str| s.doc().unwrap().doc.variables.dataset(name).and_then(|d| d.values.get("Name")).cloned();
assert_eq!(value(&s, "Data Set 1"), Some(vectorcraft_doc::DataValue::Text(String::new())));
s.execute("variable.define", &json!({"name": "Other", "kind": "text"})).unwrap();
s.execute("dataset.import", &json!({"csv": "Name,Other\n,x\n\"\",y\n"})).unwrap();
assert_eq!(value(&s, "Data Set 2"), Some(vectorcraft_doc::DataValue::Text(String::new())));
// Compatibility rule for files written before empty text was quoted: a bare empty cell stays "no value".
assert_eq!(value(&s, "Data Set 1"), None);
// A data set with no value for the variable exports a bare cell and stays without a value.
let csv = s.execute("dataset.export", &json!({})).unwrap()["csv"].as_str().unwrap().to_string();
assert_eq!(csv, "Name,Other\n,x\n\"\",y\n");
s.execute("dataset.import", &json!({"csv": csv})).unwrap();
assert_eq!(value(&s, "Data Set 1"), None);
}
/// A data set with no value for any variable exports as a row that imports back: a quoted empty
/// cell in the first column that isn't a text variable reads back as no value, and with only text
/// variables the first column reads back as an empty text.
#[test]
fn datasets_csv_keeps_a_data_set_without_values() {
let mut s = session();
s.execute("variable.define", &json!({"name": "Title", "kind": "text"})).unwrap();
s.execute("dataset.new", &json!({"name": "A", "values": {}})).unwrap();
s.execute("dataset.new", &json!({"name": "B", "values": {"Title": "x"}})).unwrap();
let export = |s: &mut Session| s.execute("dataset.export", &json!({})).unwrap()["csv"].as_str().unwrap().to_string();
let csv = export(&mut s);
assert_eq!(csv, "Title\n\"\"\nx\n");
let imp = s.execute("dataset.import", &json!({"csv": csv})).unwrap();
assert_eq!(imp["names"], json!(["Data Set 1", "Data Set 2"]));
let values = |s: &Session, name: &str| s.doc().unwrap().doc.variables.dataset(name).map(|d| d.values.clone()).unwrap();
assert_eq!(values(&s, "Data Set 1").get("Title"), Some(&vectorcraft_doc::DataValue::Text(String::new())));
s.execute("variable.define", &json!({"name": "Show", "kind": "visibility"})).unwrap();
s.execute("dataset.new", &json!({"name": "C", "values": {}})).unwrap();
let csv = export(&mut s);
assert!(csv.ends_with("\n,\"\"\n"), "{csv:?}");
s.execute("dataset.import", &json!({"csv": csv})).unwrap();
assert_eq!(s.doc().unwrap().doc.variables.datasets.len(), 3);
assert!(values(&s, "Data Set 3").is_empty());
}
/// A quoted text cell keeps its leading and trailing spaces, so a padded value comes back from
/// the exported CSV as it went out; an unquoted cell, and spaces after a closing quote, are trimmed.
#[test]
fn datasets_csv_keeps_spaces_in_quoted_cells() {
let mut s = session();
let t = text(&mut s, "One");
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).unwrap();
s.execute("dataset.new", &json!({"name": "A", "values": {"Name": " padded "}})).unwrap();
let csv = s.execute("dataset.export", &json!({})).unwrap()["csv"].as_str().unwrap().to_string();
s.execute("dataset.import", &json!({"csv": csv})).unwrap();
let value = |s: &Session, name: &str| s.doc().unwrap().doc.variables.dataset(name).and_then(|d| d.values.get("Name")).cloned();
let text_value = |v: &str| Some(vectorcraft_doc::DataValue::Text(v.into()));
assert_eq!(value(&s, "Data Set 1"), text_value(" padded "));
s.execute("dataset.import", &json!({"csv": "Name\n typed \n\" quoted \" \n"})).unwrap();
assert_eq!(value(&s, "Data Set 1"), text_value("typed"));
assert_eq!(value(&s, "Data Set 2"), text_value(" quoted "));
}
/// Export refuses a document with more data sets or variables than `dataset.import` reads, so
/// every file it writes imports back: 1,000 data sets and 256 variables export and import, and
/// one more of either is refused. The import refuses a line of 257 columns, the last one included.
#[test]
fn datasets_export_refuses_more_than_import_reads() {
let mut s = session();
let header: Vec<String> = (1..=256).map(|i| format!("V{i}")).collect();
for wide in [format!("{},V257\nx\n", header.join(",")), format!("{}\nx{}\n", header.join(","), ",".repeat(256))] {
let err = s.execute("dataset.import", &json!({"csv": wide})).unwrap_err().to_string();
assert!(err.contains("more than 256 columns"), "{err}");
}
let rows: String = (1..=1000).map(|i| format!("Set {i},x\n")).collect();
s.execute("dataset.import", &json!({"csv": format!("{}\n{rows}", header.join(","))})).unwrap();
let csv = s.execute("dataset.export", &json!({})).unwrap()["csv"].as_str().unwrap().to_string();
s.execute("dataset.import", &json!({"csv": csv})).unwrap();
s.execute("dataset.new", &json!({"name": "Set 1001"})).unwrap();
let err = s.execute("dataset.export", &json!({})).unwrap_err().to_string();
assert!(err.contains("more than 1000 data sets"), "{err}");
s.execute("dataset.delete", &json!({"names": ["Set 1001"]})).unwrap();
s.execute("dataset.export", &json!({})).unwrap();
s.execute("variable.define", &json!({"name": "V257", "kind": "text"})).unwrap();
let err = s.execute("dataset.export", &json!({})).unwrap_err().to_string();
assert!(err.contains("more than 256 variables"), "{err}");
let dir = Folder::new("vars-csv-caps");
let out = dir.file("sets.csv");
assert!(s.execute("dataset.export", &json!({"path": out})).is_err());
assert!(!std::path::Path::new(&out).exists(), "no file is written");
}
/// A header row with a tab and no comma outside quotes makes tabs the separator; a quoted cell
/// may then hold tabs and commas. A header row with a comma outside quotes keeps commas, even
/// when a cell holds a tab. A tab-separated header with an unclosed quote fails with the reason.
#[test]
fn datasets_csv_reads_tab_separated_files() {
let mut s = session();
s.execute("variable.define", &json!({"name": "Title", "kind": "text"})).unwrap();
s.execute("variable.define", &json!({"name": "Show", "kind": "visibility"})).unwrap();
s.execute("variable.define", &json!({"name": "Last, First", "kind": "text"})).unwrap();
let tsv = "Title\tShow\r\nTwo words\ttrue\r\n\"a, b\tc\"\tfalse\r\n";
let imp = s.execute("dataset.import", &json!({"csv": tsv})).unwrap();
assert_eq!(imp["names"], json!(["Data Set 1", "Data Set 2"]));
let value = |s: &Session, name: &str, var: &str| s.doc().unwrap().doc.variables.dataset(name).and_then(|d| d.values.get(var)).cloned();
let text_value = |v: &str| Some(vectorcraft_doc::DataValue::Text(v.into()));
assert_eq!(value(&s, "Data Set 1", "Title"), text_value("Two words"));
assert_eq!(value(&s, "Data Set 1", "Show"), Some(vectorcraft_doc::DataValue::Visible(true)));
assert_eq!(value(&s, "Data Set 2", "Title"), text_value("a, b\tc"));
assert_eq!(value(&s, "Data Set 2", "Show"), Some(vectorcraft_doc::DataValue::Visible(false)));
s.execute("dataset.import", &json!({"csv": "Title,Show\none\ttwo,true"})).unwrap();
assert_eq!(value(&s, "Data Set 1", "Title"), text_value("one\ttwo"));
s.execute("dataset.import", &json!({"csv": "\"Last, First\"\tShow\nSmith, Jo\ttrue"})).unwrap();
assert_eq!(value(&s, "Data Set 1", "Last, First"), text_value("Smith, Jo"));
// A file of one variable whose name holds a tab is exported with the name quoted, and reads back as one column.
let mut one = session();
one.execute("variable.define", &json!({"name": "Tab\tName", "kind": "text"})).unwrap();
one.execute("dataset.new", &json!({"name": "A", "values": {"Tab\tName": "v"}})).unwrap();
let csv = one.execute("dataset.export", &json!({})).unwrap()["csv"].as_str().unwrap().to_string();
assert_eq!(csv, "\"Tab\tName\"\nv\n");
let imp = one.execute("dataset.import", &json!({"csv": csv})).unwrap();
assert_eq!(imp["defined"], json!([]), "{imp}");
assert_eq!(value(&one, "Data Set 1", "Tab\tName"), text_value("v"));
let unclosed = s.execute("dataset.import", &json!({"csv": "Title\t\"Show\nx\ttrue"})).unwrap_err().to_string();
assert!(unclosed.contains("unclosed quote"), "{unclosed}");
}
/// Every header cell is a variable, whatever its case: a file laid out as the set-up page shows
/// (`Name,Age`) defines and fills `Name`, and `name` and `NAME` are variables too. The rows are
/// named Data Set 1, 2 and so on, in file order.
#[test]
fn datasets_csv_reads_every_header_cell_as_a_variable() {
let mut s = session();
let t = text(&mut s, "x");
let imp = s.execute("dataset.import", &json!({"csv": "Name,Age\nBill Tucker,36\nJo Lee,41\n"})).unwrap();
assert_eq!((imp["imported"].as_u64(), &imp["defined"]), (Some(2), &json!(["Name", "Age"])), "{imp}");
assert_eq!(imp["names"], json!(["Data Set 1", "Data Set 2"]));
s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).unwrap();
s.execute("dataset.select", &json!({"name": "Data Set 2"})).unwrap();
assert_eq!(plain(&s, t), "Jo Lee");
for header in ["name", "NAME"] {
let imp = s.execute("dataset.import", &json!({"csv": format!("{header},Age\nx,1\n")})).unwrap();
assert_eq!((&imp["defined"], &imp["names"]), (&json!([header]), &json!(["Data Set 1"])), "{header}: {imp}");
}
let value = |s: &Session, var: &str| s.doc().unwrap().doc.variables.dataset("Data Set 1").and_then(|d| d.values.get(var)).cloned();
assert_eq!(value(&s, "NAME"), Some(vectorcraft_doc::DataValue::Text("x".into())));
}
/// An import replaces every data set of the document, as Load Variable Library does, and clears
/// the active data set. The document's variables stay with their bindings, including those the
/// file has no column for, which then have no value in any data set. Undo brings the data sets back.
#[test]
fn datasets_csv_import_replaces_the_data_sets() {
let mut s = session();
let t = text(&mut s, "x");
let r = rect(&mut s);
s.execute("variable.define", &json!({"name": "Title", "kind": "text"})).unwrap();
s.execute("variable.define", &json!({"name": "Show", "kind": "visibility"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Title", "ids": [t.0]})).unwrap();
s.execute("variable.bind", &json!({"variable": "Show", "ids": [r.0]})).unwrap();
for (name, title) in [("First", "a"), ("Data Set 2", "b"), ("Data Set 3", "c")] {
s.execute("dataset.new", &json!({"name": name, "values": {"Title": title, "Show": true}})).unwrap();
}
s.execute("dataset.select", &json!({"name": "First"})).unwrap();
let imp = s.execute("dataset.import", &json!({"csv": "Title\ny\nz\n"})).unwrap();
assert_eq!(imp["names"], json!(["Data Set 1", "Data Set 2"]));
assert_eq!(imp["deletedDataSets"], json!(["First", "Data Set 2", "Data Set 3"]));
let vars = &s.doc().unwrap().doc.variables;
let sets: Vec<_> = vars.datasets.iter().map(|d| (d.name.as_str(), d.values.get("Title").cloned(), d.values.get("Show").cloned())).collect();
let text_value = |v: &str| Some(vectorcraft_doc::DataValue::Text(v.into()));
assert_eq!(sets, [("Data Set 1", text_value("y"), None), ("Data Set 2", text_value("z"), None)]);
assert_eq!(vars.active_dataset, None);
let listed = s.execute("variable.list", &json!({})).unwrap();
assert_eq!(listed["variables"], json!([{"name": "Title", "kind": "text", "bindings": 1}, {"name": "Show", "kind": "visibility", "bindings": 1}]));
s.execute("dataset.select", &json!({"name": "Data Set 2"})).unwrap();
assert_eq!(plain(&s, t), "z");
// The first undo takes back the selection, the second the import.
s.execute("edit.undo", &json!({})).unwrap();
s.execute("edit.undo", &json!({})).unwrap();
let vars = &s.doc().unwrap().doc.variables;
let names: Vec<&str> = vars.datasets.iter().map(|d| d.name.as_str()).collect();
assert_eq!((names, vars.active_dataset.as_deref()), (vec!["First", "Data Set 2", "Data Set 3"], Some("First")));
}
/// An error about a row's values gives the file line the row starts on, counting blank lines and
/// line breaks inside quoted cells.
#[test]
fn datasets_csv_row_errors_name_the_file_line() {
let mut s = session();
s.execute("variable.define", &json!({"name": "Title", "kind": "text"})).unwrap();
s.execute("variable.define", &json!({"name": "Show", "kind": "visibility"})).unwrap();
let error = |s: &mut Session, csv: &str| s.execute("dataset.import", &json!({"csv": csv})).unwrap_err().to_string();
let blank_line = error(&mut s, "Show\n\nmaybe");
assert!(blank_line.contains("line 3:"), "{blank_line}");
let line_break = error(&mut s, "Title,Show\n\"one\ntwo\",true\nx,maybe");
assert!(line_break.contains("line 4:"), "{line_break}");
}
/// A refused cell's error gives its line and why the value was refused: a visibility value that
/// isn't true or false, or a graph value longer than the graph data limit.
#[test]
fn datasets_csv_cell_errors_say_why() {
let mut s = session();
s.execute("variable.define", &json!({"name": "Show", "kind": "visibility"})).unwrap();
s.execute("variable.define", &json!({"name": "Sales", "kind": "graph"})).unwrap();
let error = |s: &mut Session, csv: &str| s.execute("dataset.import", &json!({"csv": csv})).unwrap_err();
let maybe = error(&mut s, "Show\nmaybe");
let want = "line 2: value for `Show` must be true or false";
assert!(matches!(&maybe, EngineError::BadParams { msg, .. } if msg == want), "{maybe}");
let long = error(&mut s, &format!("Sales\n\"{}\"", "1,".repeat(140_000)));
let want = "line 2: value for `Sales` is longer than 262144 bytes";
assert!(matches!(&long, EngineError::BadParams { msg, .. } if msg == want), "{long}");
assert!(s.doc().unwrap().doc.variables.datasets.is_empty());
}
/// A header cell that names no variable defines one, unbound, in the import's undo step: `#`
/// defines a visibility variable, `@` defines an image variable, and a cell without a prefix
/// defines a text variable. Importing the file again finds them; a prefix of the other kind, a
/// bare prefix and a long name fail and define nothing.
#[test]
fn datasets_csv_header_prefixes_define_variables() {
let mut s = session();
let csv = "#Shown,@Photo,Title\ntrue,a.png,x";
let imp = s.execute("dataset.import", &json!({"csv": csv})).unwrap();
assert_eq!(imp["defined"], json!(["Shown", "Photo", "Title"]));
assert_eq!((imp["imported"].as_u64(), imp["names"].clone()), (Some(1), json!(["Data Set 1"])));
let listed = s.execute("variable.list", &json!({})).unwrap();
assert_eq!(
listed["variables"],
json!([
{"name": "Shown", "kind": "visibility", "bindings": 0},
{"name": "Photo", "kind": "image", "bindings": 0},
{"name": "Title", "kind": "text", "bindings": 0},
])
);
let value = |s: &Session, var: &str| s.doc().unwrap().doc.variables.dataset("Data Set 1").and_then(|d| d.values.get(var)).cloned();
assert_eq!(value(&s, "Shown"), Some(vectorcraft_doc::DataValue::Visible(true)));
assert_eq!(value(&s, "Photo"), Some(vectorcraft_doc::DataValue::Image("a.png".into())));
assert_eq!(value(&s, "Title"), Some(vectorcraft_doc::DataValue::Text("x".into())));
let again = s.execute("dataset.import", &json!({"csv": "#Shown\t@Photo\tTitle\nfalse\tb.png\ty"})).unwrap();
assert_eq!((again["imported"].as_u64(), again["defined"].clone()), (Some(1), json!([])));
assert_eq!(value(&s, "Shown"), Some(vectorcraft_doc::DataValue::Visible(false)));
let error = |s: &mut Session, csv: &str| s.execute("dataset.import", &json!({"csv": csv})).unwrap_err().to_string();
let other_kind = error(&mut s, "Shown,New,#Title\ntrue,n,true");
assert!(other_kind.contains("column 3:") && other_kind.contains("text variable"), "{other_kind}");
let twice = error(&mut s, "Fresh,@Fresh\nn,f.png");
assert!(twice.contains("column 2:") && twice.contains("text variable"), "{twice}");
let bare = error(&mut s, "Title,#\nx,true");
assert!(bare.contains("column 2:") && bare.contains("names no variable"), "{bare}");
let long = error(&mut s, &format!("Title,@{}\nx,b.png", "n".repeat(256)));
assert!(long.contains("column 2:") && long.contains("longer than 255"), "{long}");
assert_eq!(s.doc().unwrap().doc.variables.variables.len(), 3, "a failed import defines nothing");
// Each import is one undo step, which takes back the variables it defined with its rows.
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(value(&s, "Shown"), Some(vectorcraft_doc::DataValue::Visible(true)));
assert_eq!(s.doc().unwrap().doc.variables.variables.len(), 3);
s.execute("edit.undo", &json!({})).unwrap();
let vars = &s.doc().unwrap().doc.variables;
assert!(vars.variables.is_empty() && vars.datasets.is_empty());
}
/// A header cell that is a variable's exact name names it, even when the name starts with `#`,
/// `@` or `%`, so a file `dataset.export` wrote imports back without defining anything.
#[test]
fn datasets_csv_exact_names_win_over_prefixes() {
let mut s = session();
for (name, kind) in [("#Shown", "text"), ("@Photo", "visibility"), ("%Chart", "graph"), ("Shown", "image")] {
s.execute("variable.define", &json!({"name": name, "kind": kind})).unwrap();
}
let values = json!({"#Shown": "hello", "@Photo": true, "%Chart": ",Q\nX,3", "Shown": "s.png"});
s.execute("dataset.new", &json!({"name": "A", "values": values})).unwrap();
let csv = s.execute("dataset.export", &json!({})).unwrap()["csv"].as_str().unwrap().to_string();
assert!(csv.starts_with("#Shown,#@Photo,%Chart,@Shown\n"), "{csv}");
let imp = s.execute("dataset.import", &json!({"csv": csv})).unwrap();
assert_eq!((imp["imported"].as_u64(), imp["defined"].clone()), (Some(1), json!([])));
let value = |var: &str| s.doc().unwrap().doc.variables.dataset("Data Set 1").and_then(|d| d.values.get(var)).cloned();
assert_eq!(value("#Shown"), Some(vectorcraft_doc::DataValue::Text("hello".into())));
assert_eq!(value("@Photo"), Some(vectorcraft_doc::DataValue::Visible(true)));
assert_eq!(value("%Chart"), Some(vectorcraft_doc::DataValue::Graph(",Q\nX,3".into())));
assert_eq!(value("Shown"), Some(vectorcraft_doc::DataValue::Image("s.png".into())));
assert_eq!(s.doc().unwrap().doc.variables.variables.len(), 4);
}
/// `dataset.export` writes `#` before a visibility variable's header and `@` before an image
/// variable's, so a document without the variables reads the file back with their kinds. The
/// reply's `warning` names the variables it doesn't: a text variable whose name starts with `#`,
/// `@` or `%`, and a graph variable.
#[test]
fn datasets_csv_export_marks_visibility_and_image_columns() {
let mut s = session();
for (name, kind) in [("Name", "text"), ("Show", "visibility"), ("Photo", "image")] {
s.execute("variable.define", &json!({"name": name, "kind": kind})).unwrap();
}
s.execute("dataset.new", &json!({"name": "A", "values": {"Name": "Ann", "Show": false, "Photo": "a.png"}})).unwrap();
let out = s.execute("dataset.export", &json!({})).unwrap();
assert_eq!(out["csv"], "Name,#Show,@Photo\nAnn,false,a.png\n");
assert!(out.get("warning").is_none(), "{out}");
let mut fresh = session();
let imp = fresh.execute("dataset.import", &json!({"csv": out["csv"]})).unwrap();
assert_eq!(imp["defined"], json!(["Name", "Show", "Photo"]));
let listed = fresh.execute("variable.list", &json!({})).unwrap();
let kinds: Vec<_> = listed["variables"].as_array().unwrap().iter().map(|v| v["kind"].clone()).collect();
assert_eq!(kinds, [json!("text"), json!("visibility"), json!("image")]);
let value = |var: &str| fresh.doc().unwrap().doc.variables.dataset("Data Set 1").and_then(|d| d.values.get(var)).cloned();
assert_eq!(value("Name"), Some(vectorcraft_doc::DataValue::Text("Ann".into())));
assert_eq!(value("Show"), Some(vectorcraft_doc::DataValue::Visible(false)));
assert_eq!(value("Photo"), Some(vectorcraft_doc::DataValue::Image("a.png".into())));
for (name, kind) in [("#Tag", "text"), ("Chart", "graph"), ("@Note", "text")] {
s.execute("variable.define", &json!({"name": name, "kind": kind})).unwrap();
}
let out = s.execute("dataset.export", &json!({})).unwrap();
assert_eq!(out["warning"], "a new document won't read these variables back with their kind: `#Tag`, `Chart`, `@Note`");
assert!(out["csv"].as_str().unwrap().starts_with("Name,#Show,@Photo,#Tag,Chart,@Note\n"), "{out}");
}
/// A file `dataset.export` writes has no column of data set names, and `dataset.import` reads
/// every column as a variable, so the file imports back to the same data sets: the same variables
/// with their kinds, and each data set's values in order, a data set without values included, named
/// Data Set 1, 2 and so on. In a document whose data sets already have those names and values, the
/// import changes nothing.
#[test]
fn datasets_csv_export_imports_back_to_the_same_data_sets() {
let mut s = session();
for (name, kind) in [("Name", "text"), ("Show", "visibility"), ("Photo", "image")] {
s.execute("variable.define", &json!({"name": name, "kind": kind})).unwrap();
}
s.execute("dataset.new", &json!({"name": "A", "values": {"Name": "Ann, Jr.", "Show": true, "Photo": "a.png"}})).unwrap();
s.execute("dataset.new", &json!({"name": "B", "values": {"Show": false}})).unwrap();
s.execute("dataset.new", &json!({"name": "C", "values": {}})).unwrap();
s.execute("dataset.new", &json!({"name": "D", "values": {"Name": "", "Photo": "d.png"}})).unwrap();
let csv = s.execute("dataset.export", &json!({})).unwrap()["csv"].as_str().unwrap().to_string();
assert!(csv.starts_with("Name,#Show,@Photo\n"), "{csv}");
let before = s.doc().unwrap().doc.variables.clone();
let mut fresh = session();
fresh.execute("dataset.import", &json!({"csv": csv})).unwrap();
let vars = fresh.doc().unwrap().doc.variables.clone();
assert_eq!(vars.variables, before.variables, "the same variables, with their kinds");
let values = |v: &vectorcraft_doc::Variables| v.datasets.iter().map(|d| d.values.clone()).collect::<Vec<_>>();
assert_eq!(values(&vars), values(&before), "the same values in every data set");
let names: Vec<&str> = vars.datasets.iter().map(|d| d.name.as_str()).collect();
assert_eq!(names, ["Data Set 1", "Data Set 2", "Data Set 3", "Data Set 4"]);
// Exporting the imported data sets writes the same file, and importing it changes nothing: no
// undo step, and the active data set stays.
let again = fresh.execute("dataset.export", &json!({})).unwrap()["csv"].as_str().unwrap().to_string();
assert_eq!(again, csv);
fresh.execute("dataset.select", &json!({"name": "Data Set 1"})).unwrap();
let vars = fresh.doc().unwrap().doc.variables.clone();
assert!(vars.active_dataset.is_some());
let undo = fresh.doc().unwrap().history.undo.len();
fresh.execute("dataset.import", &json!({"csv": again})).unwrap();
assert_eq!(fresh.doc().unwrap().history.undo.len(), undo, "no undo step");
assert_eq!(fresh.doc().unwrap().doc.variables, vars);
}
/// The export's `warning` names the variables whose column a document without the variables
/// refuses, so it doesn't import the file: a `%` header, a `#` header whose values aren't true or
/// false, and a header that names a variable an earlier column defined with another kind. A
/// header that names such a variable with its kind is read with that kind, and is listed as one
/// read back with another kind.
#[test]
fn datasets_csv_export_warns_of_headers_a_new_document_refuses() {
let export = |vars: &[(&str, &str)], values: Value| {
let mut s = session();
for (name, kind) in vars {
s.execute("variable.define", &json!({"name": name, "kind": kind})).unwrap();
}
s.execute("dataset.new", &json!({"name": "A", "values": values})).unwrap();
let out = s.execute("dataset.export", &json!({})).unwrap();
let refused = session().execute("dataset.import", &json!({"csv": out["csv"]})).is_err();
(out["warning"].as_str().unwrap_or("").to_string(), refused)
};
let refuses = |names: &str| format!("a new document refuses this file because of these variables: {names}");
let loses = |names: &str| format!("a new document won't read these variables back with their kind: {names}");
assert_eq!(export(&[("%Rate", "text"), ("Chart", "graph")], json!({})), (refuses("`%Rate`"), true));
assert_eq!(export(&[("#Tag", "text")], json!({"#Tag": "hello"})), (refuses("`#Tag`"), true));
assert_eq!(export(&[("#Tag", "text")], json!({"#Tag": "yes"})), (loses("`#Tag`"), false));
assert_eq!(export(&[("#Seen", "text"), ("Seen", "image")], json!({})), (refuses("`Seen`"), true));
assert_eq!(export(&[("#Seen", "text"), ("Seen", "text")], json!({})), (refuses("`Seen`"), true));
}
/// `dataset.export` is refused, naming both variables, when a variable's header is another
/// variable's name, which an import reads as that variable; with `path`, no file is written.
#[test]
fn datasets_csv_export_refuses_a_header_that_names_another_variable() {
let dir = Folder::new("vars-csv-header");
for (first, second) in
[(("Show", "visibility"), ("#Show", "text")), (("Photo", "image"), ("@Photo", "graph")), (("X", "visibility"), ("#X", "image"))]
{
let mut s = session();
for (name, kind) in [first, second] {
s.execute("variable.define", &json!({"name": name, "kind": kind})).unwrap();
}
let e = s.execute("dataset.export", &json!({})).unwrap_err().to_string();
let (name, other) = (first.0, second.0);
assert!(e.contains(&format!("for `{name}` would read back as the variable `{other}`")), "{e}");
let out = dir.file("sets.csv");
assert!(s.execute("dataset.export", &json!({"path": out})).is_err());
assert!(!std::path::Path::new(&out).exists(), "{out}");
}
}
/// A `%` header cell that isn't a variable's name (graph data files) is refused with its column,
/// and the import defines nothing.
#[test]
fn datasets_csv_refuses_graph_data_file_columns() {
let mut s = session();
let error = |s: &mut Session, csv: &str| s.execute("dataset.import", &json!({"csv": csv})).unwrap_err().to_string();
let named = error(&mut s, "Name,Title,%Sales\nA,x,sales.txt");
assert!(named.contains("column 3:") && named.contains("`%Sales`"), "{named}");
let unnamed = error(&mut s, "%Sales\tTitle\nsales.txt\tx");
assert!(unnamed.contains("column 1:") && unnamed.contains("`%Sales`"), "{unnamed}");
let vars = &s.doc().unwrap().doc.variables;
assert!(vars.variables.is_empty() && vars.datasets.is_empty());
}
/// Two header cells that name one variable, by the same name or by a name and its prefix, are
/// refused with both columns, and the import changes nothing.
#[test]
fn datasets_csv_refuses_two_columns_for_one_variable() {
let mut s = session();
s.execute("variable.define", &json!({"name": "Shown", "kind": "visibility"})).unwrap();
let error = |s: &mut Session, csv: &str| s.execute("dataset.import", &json!({"csv": csv})).unwrap_err().to_string();
for (csv, first, second, var) in [
("Title,City,City\nR,Paris,Rome", 2, 3, "City"),
("Title,City,City\nR,Paris,", 2, 3, "City"),
("City,Title,City\nParis,x,Rome", 1, 3, "City"),
("Title,Shown,#Shown\nR,true,false", 2, 3, "Shown"),
("Title\t@Photo\t@ Photo\nx\ta.png\tb.png", 2, 3, "Photo"),
] {
let err = error(&mut s, csv);
assert!(err.contains(&format!("columns {first} and {second} both name the variable `{var}`")), "{csv}: {err}");
}
let vars = &s.doc().unwrap().doc.variables;
assert_eq!(vars.variables.len(), 1);
assert!(vars.datasets.is_empty());
}
/// While the variables are locked, an import is refused when a header cell would define a
/// variable, as New Variable is, and a file whose headers all name variables imports.
#[test]
fn locked_variables_refuse_header_cells_that_define_variables() {
let mut s = session();
s.execute("variable.define", &json!({"name": "Shown", "kind": "visibility"})).unwrap();
s.execute("variable.define", &json!({"name": "Title", "kind": "text"})).unwrap();
s.execute("variable.lock", &json!({"locked": true})).unwrap();
for csv in ["#Shown,Title,New\ntrue,x,n", "@Photo\nb.png", "Title,#Fresh\nx,true"] {
let e = s.execute("dataset.import", &json!({"csv": csv})).unwrap_err();
assert!(matches!(&e, EngineError::Disabled(cmd, why) if cmd == "dataset.import" && why == "variables are locked"), "{csv}: {e}");
}
let vars = &s.doc().unwrap().doc.variables;
assert!(vars.variables.len() == 2 && vars.datasets.is_empty(), "a refused import defines and imports nothing");
let imp = s.execute("dataset.import", &json!({"csv": "#Shown,Title\ntrue,x"})).unwrap();
assert_eq!((imp["imported"].as_u64(), imp["defined"].clone()), (Some(1), json!([])));
s.execute("variable.lock", &json!({"locked": false})).unwrap();
let imp = s.execute("dataset.import", &json!({"csv": "New\nn"})).unwrap();
assert_eq!(imp["defined"], json!(["New"]));
}
/// Import / export datasets as CSV: one column per variable and no dataset names; round-trip keeps
/// the values in order, and the rows come back as Data Set 1, 2.
#[test]
fn datasets_csv_round_trips_and_rejects_bad_input() {
let dir = Folder::new("vars-csv");
let red_path = dir.file("red.png");
let blue_path = dir.file("blue.png");
write(&red_path, &png(8, 8, RED));
write(&blue_path, &png(8, 8, BLUE));
let mut s = session();
let t = text(&mut s, "Alice");
let r = rect(&mut s);
let img = place(&mut s, &red_path);
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("variable.define", &json!({"name": "Show", "kind": "visibility"})).unwrap();
s.execute("variable.define", &json!({"name": "Photo", "kind": "image"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).unwrap();
s.execute("variable.bind", &json!({"variable": "Show", "ids": [r.0]})).unwrap();
s.execute("variable.bind", &json!({"variable": "Photo", "ids": [img.0]})).unwrap();
s.execute("dataset.new", &json!({"name": "A", "values": {"Name": "Alice", "Show": true, "Photo": red_path}})).unwrap();
s.execute("dataset.new", &json!({"name": "B", "values": {"Name": "Bob", "Show": false, "Photo": blue_path}})).unwrap();
let exported = s.execute("dataset.export", &json!({})).unwrap();
let csv = exported["csv"].as_str().unwrap().to_string();
assert!(csv.starts_with("Name,#Show,@Photo\n"), "{csv}");
assert!(csv.contains("Alice") && csv.contains("Bob") && csv.contains("false"), "{csv}");
// Import again, which replaces A and B, and apply the second row.
let imp = s.execute("dataset.import", &json!({"csv": csv})).unwrap();
assert_eq!((imp["imported"].as_u64(), &imp["deletedDataSets"]), (Some(2), &json!(["A", "B"])));
s.execute("dataset.select", &json!({"name": "Data Set 2"})).unwrap();
assert_eq!(plain(&s, t), "Bob");
assert!(!s.doc().unwrap().doc.node(r).unwrap().visible);
assert_eq!(image(&s, img).link.as_ref().map(|l| l.path.as_str()), Some(blue_path.as_str()));
// Re-import replaces the rows and defines nothing; a bad visibility value is an error.
let again = s.execute("dataset.import", &json!({"csv": csv})).unwrap();
assert_eq!((again["imported"].as_u64(), &again["deletedDataSets"]), (Some(2), &json!(["Data Set 1", "Data Set 2"])));
assert_eq!(again["defined"], json!([]));
assert!(s.execute("dataset.import", &json!({"csv": "Show\nmaybe"})).is_err());
let out = dir.file("sets.csv");
s.execute("dataset.export", &json!({"path": out})).unwrap();
let from_file = s.execute("dataset.import", &json!({"path": out})).unwrap();
assert_eq!(from_file["names"], json!(["Data Set 1", "Data Set 2"]));
// One undo undoes the last import's replacements.
s.execute("edit.undo", &json!({})).unwrap();
}
/// A CSV that isn't UTF-8 is read as Windows-1252. UTF-8 files, with or without a byte-order mark,
/// read as before, and a file that starts with the UTF-8 byte-order mark but isn't UTF-8 is refused.
#[test]
fn datasets_csv_reads_windows_1252_files() {
let mut s = session();
s.execute("variable.define", &json!({"name": "City", "kind": "text"})).unwrap();
let import = |s: &mut Session, bytes: &[u8]| s.execute("dataset.import", &json!({"dataBase64": vectorcraft_format::base64_encode(bytes)}));
let value = |s: &Session, name: &str| s.doc().unwrap().doc.variables.dataset(name).and_then(|d| d.values.get("City")).cloned();
let text_value = |v: &str| Some(vectorcraft_doc::DataValue::Text(v.into()));
let imp = import(&mut s, b"City\r\nJos\xe9\r\n\x80 5 \x96 \x93Caf\xe9\x94\r\n").unwrap();
assert_eq!(imp["names"], json!(["Data Set 1", "Data Set 2"]));
assert_eq!(value(&s, "Data Set 1"), text_value("José"));
assert_eq!(value(&s, "Data Set 2"), text_value("€ 5 – “Café”"));
import(&mut s, "City\nJosé\n".as_bytes()).unwrap();
assert_eq!(value(&s, "Data Set 1"), text_value("José"));
import(&mut s, "\u{feff}City\nZoë\n".as_bytes()).unwrap();
assert_eq!(value(&s, "Data Set 1"), text_value("Zoë"));
let e = import(&mut s, b"\xef\xbb\xbfCity\nJos\xe9\n").unwrap_err().to_string();
assert!(e.contains("CSV is not UTF-8"), "{e}");
assert_eq!(value(&s, "Data Set 1"), text_value("Zoë"));
}
/// A CSV that starts with a UTF-16 byte-order mark is read as UTF-16, and
/// one that isn't valid UTF-16 is refused. A file with NUL bytes and no byte-order mark is refused.
/// A text value holding NUL imports back from the `csv` that `dataset.export` returns.
#[test]
fn datasets_csv_reads_utf16_files() {
let mut s = session();
s.execute("variable.define", &json!({"name": "City", "kind": "text"})).unwrap();
let import = |s: &mut Session, bytes: &[u8]| s.execute("dataset.import", &json!({"dataBase64": vectorcraft_format::base64_encode(bytes)}));
let value = |s: &Session, name: &str| s.doc().unwrap().doc.variables.dataset(name).and_then(|d| d.values.get("City")).cloned();
let text_value = |v: &str| Some(vectorcraft_doc::DataValue::Text(v.into()));
let utf16 = |bom: &[u8], text: &str, le: bool| -> Vec<u8> {
let units = text.encode_utf16().flat_map(|u| if le { u.to_le_bytes() } else { u.to_be_bytes() });
bom.iter().copied().chain(units).collect()
};
import(&mut s, &utf16(b"\xff\xfe", "City\r\nZoë 😀\r\n", true)).unwrap();
assert_eq!(value(&s, "Data Set 1"), text_value("Zoë 😀"));
import(&mut s, &utf16(b"\xfe\xff", "City\nJosé\n", false)).unwrap();
assert_eq!(value(&s, "Data Set 1"), text_value("José"));
let mut odd = utf16(b"\xff\xfe", "City\nx\n", true);
odd.push(b'y');
let e = import(&mut s, &odd).unwrap_err().to_string();
assert!(e.contains("CSV is not UTF-16"), "{e}");
let e = import(&mut s, &[b"\xff\xfe".as_slice(), "City\nAB,".as_bytes(), b"\x00\xd8"].concat()).unwrap_err().to_string();
assert!(e.contains("CSV is not UTF-16"), "{e}");
let e = import(&mut s, &utf16(b"", "City\nx\n", true)).unwrap_err().to_string();
assert!(e.contains("CSV holds NUL bytes"), "{e}");
assert_eq!(value(&s, "Data Set 1"), text_value("José"), "a refused file changes nothing");
s.execute("dataset.set", &json!({"name": "Data Set 1", "values": {"City": "a\u{0}b"}})).unwrap();
let csv = s.execute("dataset.export", &json!({})).unwrap()["csv"].as_str().unwrap().to_string();
s.execute("dataset.delete", &json!({"names": ["Data Set 1"]})).unwrap();
s.execute("dataset.import", &json!({"csv": csv})).unwrap();
assert_eq!(value(&s, "Data Set 1"), text_value("a\u{0}b"));
}
/// A data set of [`library`]: its name, and each variable's name with its paragraphs.
type LibrarySet<'a> = (&'a str, &'a [(&'a str, &'a [&'a str])]);
/// A variable library written from its public grammar: a document type declaration with the
/// namespace entities, `variableSets` › `variableSet` › `variables` with one `variable` element per
/// `(varName, trait)`, then `sampleDataSets` with one `sampleDataSet` per data set, holding an
/// element per variable with a `p` element per paragraph.
fn library(vars: &[(&str, &str)], sets: &[LibrarySet]) -> String {
let mut x = String::from(
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\
<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\" [\n\
\t<!ENTITY ns_vars \"http://ns.adobe.com/Variables/1.0/\">\n\
\t<!ENTITY ns_custom \"http://ns.adobe.com/GenericCustomNamespace/1.0/\">\n\
\t<!ENTITY ns_flows \"http://ns.adobe.com/Flows/1.0/\">\n\
]>\n<svg>\n<variableSets xmlns=\"&ns_vars;\">\n<variableSet varSetName=\"binding1\" locked=\"none\">\n<variables>\n",
);
for (name, kind) in vars {
x += &format!("<variable varName=\"{name}\" trait=\"{kind}\" category=\"&ns_flows;\"></variable>\n");
}
x += "</variables>\n<v:sampleDataSets xmlns:v=\"&ns_vars;\" xmlns=\"&ns_custom;\">\n";
for (set, values) in sets {
x += &format!("<v:sampleDataSet dataSetName=\"{set}\">\n");
for (var, paras) in *values {
let paras: String = paras.iter().map(|p| format!("<p>{p}</p>")).collect();
x += &format!("<{var}>{paras}</{var}>\n");
}
x += "</v:sampleDataSet>\n";
}
x + "</v:sampleDataSets>\n</variableSet>\n</variableSets>\n</svg>\n"
}
/// Load Variable Library defines the library's text variables, unbound, and adds its data sets,
/// in one undo step. A value is the text of the `p` elements, one paragraph each; an element
/// without a `p` gives no value. A file, and base64 with a UTF-8 byte-order mark, load the same.
#[test]
fn a_variable_library_defines_text_variables_and_data_sets() {
let mut s = session();
let t = text(&mut s, "Alice");
let xml = library(
&[("Name", "textcontent"), ("Note", "textcontent")],
&[("Row 1", &[("Name", &["Bravo"]), ("Note", &["One", "Two &amp; three"])]), ("Row 2", &[("Name", &["Charlie"]), ("Note", &[])])],
);
let r = s.execute("variable.loadLibrary", &json!({"xml": xml})).unwrap();
assert_eq!(r["variables"], json!(["Name", "Note"]));
assert_eq!((r["deleted"].clone(), r["deletedDataSets"].clone()), (json!([]), json!([])));
assert_eq!(r["names"], json!(["Row 1", "Row 2"]));
assert_eq!((r["skipped"].clone(), r.get("warning")), (json!([]), None));
let listed = s.execute("variable.list", &json!({})).unwrap();
assert_eq!(listed["variables"], json!([{"name": "Name", "kind": "text", "bindings": 0}, {"name": "Note", "kind": "text", "bindings": 0}]));
let sets = s.execute("dataset.list", &json!({})).unwrap();
assert_eq!(sets["datasets"][0]["values"], json!({"Name": {"text": "Bravo"}, "Note": {"text": "One\nTwo & three"}}));
assert_eq!(sets["datasets"][1]["values"], json!({"Name": {"text": "Charlie"}}));
assert_eq!(sets["active"], Value::Null);
assert_eq!(plain(&s, t), "Alice", "loading binds nothing and applies nothing");
s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).unwrap();
s.execute("dataset.select", &json!({"name": "Row 2"})).unwrap();
assert_eq!(plain(&s, t), "Charlie");
// One undo step each: the select, the bind, then the load.
for _ in 0..3 {
s.execute("edit.undo", &json!({})).unwrap();
}
let vars = &s.doc().unwrap().doc.variables;
assert!(vars.variables.is_empty() && vars.datasets.is_empty());
let dir = Folder::new("vars-library");
let path = dir.file("library.xml");
write(&path, xml.as_bytes());
let from_file = s.execute("variable.loadLibrary", &json!({"path": path})).unwrap();
assert_eq!(from_file["names"], json!(["Row 1", "Row 2"]));
let bom = [b"\xEF\xBB\xBF".as_slice(), xml.as_bytes()].concat();
let from_base64 = s.execute("variable.loadLibrary", &json!({"dataBase64": vectorcraft_format::base64_encode(&bom)})).unwrap();
// The library declares the document's text variables: they stay, and only the data sets go.
assert_eq!((from_base64["deleted"].clone(), from_base64["deletedDataSets"].clone()), (json!([]), json!(["Row 1", "Row 2"])));
assert_eq!(s.doc().unwrap().doc.variables.datasets.len(), 2);
}
/// Load Variable Library replaces the document's variables and data sets: every variable goes with
/// its bindings and values, a variable of another kind with a library variable's name included,
/// and every data set goes, the active one included. A text variable the library also declares
/// stays bound (#4136), and the library's data sets then change its text. The highlight of a
/// variable that went highlights nothing. Undo brings back the variables, bindings, data sets and
/// active data set as they were.
#[test]
fn a_variable_library_replaces_every_variable_and_data_set() {
let mut s = session();
let t = text(&mut s, "Alice");
let r = rect(&mut s);
for (name, kind, id) in [("Name", "text", t), ("Title", "visibility", r), ("Kept", "visibility", r)] {
s.execute("variable.define", &json!({"name": name, "kind": kind})).unwrap();
s.execute("variable.bind", &json!({"variable": name, "ids": [id.0]})).unwrap();
}
s.execute("dataset.new", &json!({"name": "A", "values": {"Name": "Old", "Title": true}})).unwrap();
s.execute("dataset.new", &json!({"name": "Other", "values": {"Name": "Stays", "Title": false, "Kept": true}})).unwrap();
s.execute("dataset.select", &json!({"name": "Other"})).unwrap();
s.execute("variable.highlight", &json!({"variable": "Kept"})).unwrap();
let before = s.doc().unwrap().doc.variables.clone();
let xml = library(&[("Name", "textcontent"), ("Title", "textcontent")], &[("A", &[("Name", &["New"]), ("Title", &["Heading"])])]);
let loaded = s.execute("variable.loadLibrary", &json!({"xml": xml})).unwrap();
assert_eq!(loaded["deleted"], json!(["Title", "Kept"]));
assert_eq!((loaded["deletedDataSets"].clone(), loaded["names"].clone()), (json!(["A", "Other"]), json!(["A"])));
let listed = s.execute("variable.list", &json!({})).unwrap();
assert_eq!(listed["variables"], json!([{"name": "Name", "kind": "text", "bindings": 1}, {"name": "Title", "kind": "text", "bindings": 0}]));
let sets = s.execute("dataset.list", &json!({})).unwrap();
assert_eq!(sets["datasets"].as_array().map(Vec::len), Some(1));
assert_eq!(sets["datasets"][0]["values"], json!({"Name": {"text": "New"}, "Title": {"text": "Heading"}}));
assert_eq!(sets["active"], Value::Null);
let bindings = &s.doc().unwrap().doc.variables.bindings;
assert_eq!(bindings.len(), 1, "the bindings went with the variables, but for Name's");
assert_eq!(bindings.get(&t).map(|v| v.len()), Some(1));
assert!(s.doc().unwrap().variables_highlight.is_none(), "the highlighted variable went");
assert_eq!(plain(&s, t), "Stays", "loading applies nothing");
s.execute("dataset.select", &json!({"name": "A"})).unwrap();
assert_eq!(plain(&s, t), "New", "the library's data set changes the text bound to Name");
s.execute("edit.undo", &json!({})).unwrap();
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(s.doc().unwrap().doc.variables, before, "one undo step brings everything back");
}
/// An import or a library load that leaves the variables and data sets as they were (the same CSV
/// imported again, the same rows tab-separated, the file Export Data Sets writes, the same library
/// loaded again) records no undo step: the step to redo stays, and the document stays unmodified.
/// An import or load that changes something is still one undo step.
#[test]
fn an_import_or_library_load_that_changes_nothing_records_no_undo_step() {
let mut s = session();
let t = text(&mut s, "Alice");
let csv = "Name,#Shown\nAnn,true\nBob,false\n";
s.execute("dataset.import", &json!({"csv": csv})).unwrap();
let exported = s.execute("dataset.export", &json!({})).unwrap()["csv"].as_str().unwrap().to_string();
for csv in [csv, "Name\t#Shown\nAnn\ttrue\nBob\tfalse\n", exported.as_str()] {
super::select_with_a_step_to_redo(&mut s, &[t]);
let before = super::history(&s);
assert_eq!((before.1, before.2), (1, false), "a step to redo, and nothing unsaved");
let r = s.execute("dataset.import", &json!({"csv": csv})).unwrap();
assert_eq!((super::history(&s), r["imported"].clone()), (before, json!(2)), "{csv}");
}
// A file with one of the two rows replaces both data sets with one, which is a change.
for csv in ["Name,#Shown\nAnn,true\n", "Name\nCy\n"] {
super::select_with_a_step_to_redo(&mut s, &[t]);
let undo = super::history(&s).0;
s.execute("dataset.import", &json!({"csv": csv})).unwrap();
assert_eq!(super::history(&s), (undo + 1, 0, true), "{csv}");
}
let xml = library(&[("Name", "textcontent")], &[("A", &[("Name", &["Ann"])]), ("B", &[("Name", &["Bob"])])]);
s.execute("variable.loadLibrary", &json!({"xml": xml})).unwrap();
super::select_with_a_step_to_redo(&mut s, &[t]);
let before = super::history(&s);
let r = s.execute("variable.loadLibrary", &json!({"xml": xml})).unwrap();
assert_eq!((super::history(&s), r["names"].clone()), (before, json!(["A", "B"])));
// A bound variable the library declares keeps its binding: loading it again changes nothing.
s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).unwrap();
super::select_with_a_step_to_redo(&mut s, &[t]);
let before = super::history(&s);
s.execute("variable.loadLibrary", &json!({"xml": xml})).unwrap();
assert_eq!(super::history(&s), before);
assert_eq!(s.execute("variable.list", &json!({})).unwrap()["variables"][0]["bindings"], json!(1));
// A library of other variables is a change: it deletes Name with its binding.
let other = library(&[("Note", "textcontent")], &[("A", &[("Note", &["Hi"])])]);
let undo = super::history(&s).0;
s.execute("variable.loadLibrary", &json!({"xml": other})).unwrap();
assert_eq!(super::history(&s), (undo + 1, 0, true));
// While the variables are locked, a library without text variables still loads its data sets,
// and loading it again changes nothing.
let mut s = session();
let t = text(&mut s, "Alice");
s.execute("variable.lock", &json!({"locked": true})).unwrap();
let xml = library(&[("Shown", "other")], &[("B", &[])]);
s.execute("variable.loadLibrary", &json!({"xml": xml})).unwrap();
super::select_with_a_step_to_redo(&mut s, &[t]);
let before = super::history(&s);
s.execute("variable.loadLibrary", &json!({"xml": xml})).unwrap();
assert_eq!(super::history(&s), before);
}
/// A library that is refused leaves the document's variables and data sets as they were.
#[test]
fn a_refused_variable_library_deletes_nothing() {
let mut s = session();
let t = text(&mut s, "Alice");
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).unwrap();
s.execute("dataset.new", &json!({"name": "A", "values": {"Name": "Old"}})).unwrap();
let before = s.doc().unwrap().doc.variables.clone();
let dup = library(&[("Name", "textcontent")], &[("B", &[]), ("B", &[])]);
assert!(s.execute("variable.loadLibrary", &json!({"xml": dup})).is_err());
assert_eq!(s.doc().unwrap().doc.variables, before);
}
/// A library's variables of other kinds are left out with their values, named in `skipped` and
/// in a warning that gives each kind once with its count; its text variables load.
#[test]
fn a_variable_library_leaves_out_variables_that_are_not_text() {
let mut s = session();
let xml = library(
&[("Shown", "kindB"), ("Name", "textcontent"), ("Photo", "kindC"), ("Other", "kindB")],
&[("A", &[("Shown", &["true"]), ("Name", &["Bravo"]), ("Photo", &["a.png"])])],
);
let r = s.execute("variable.loadLibrary", &json!({"xml": xml})).unwrap();
assert_eq!(r["variables"], json!(["Name"]));
assert_eq!(r["skipped"], json!([{"name": "Shown", "trait": "kindB"}, {"name": "Photo", "trait": "kindC"}, {"name": "Other", "trait": "kindB"}]));
assert_eq!(r["warning"], "variables that aren't text were left out: kindB (2), kindC (1)");
let vars = &s.doc().unwrap().doc.variables;
assert_eq!(vars.variables.len(), 1);
assert_eq!(vars.dataset("A").map(|d| d.values.len()), Some(1));
}
/// A library that isn't one, or that breaks its grammar or the caps, is refused, and the document
/// is left as it was.
#[test]
fn malformed_variable_libraries_are_refused() {
let mut s = session();
let ok = library(&[("Name", "textcontent")], &[("A", &[("Name", &["x"])])]);
let deep = format!("{}<variableSets/>{}", "<a>".repeat(300), "</a>".repeat(300));
let markup = "<!DOCTYPE svg [<!ENTITY deep \"<a><a></a></a>\">]><svg>&deep;<variableSets/></svg>".to_string();
// A document type declaration in a comment before the real one, and markup reached through
// another entity.
let hidden = format!("<!-- <!DOCTYPE svg> -->{markup}");
let nested = "<!DOCTYPE svg [<!ENTITY a \"&b;&b;\"><!ENTITY b 'x'>]><svg>&a;<variableSets/></svg>".to_string();
let parameter = "<!DOCTYPE svg [<!ENTITY % p \"x\">]><svg><variableSets/></svg>".to_string();
let element = "<!DOCTYPE svg [<!ELEMENT svg ANY>]><svg><variableSets/></svg>".to_string();
// A `>` and a comment opener inside a quoted system identifier, before elements nested too deep.
let quoted = format!("<!DOCTYPE svg SYSTEM \"x><!--\"><svg>{}<variableSets/>{}</svg>", "<a>".repeat(300), "</a>".repeat(300));
let entities: String = (0..65).map(|i| format!("<!ENTITY e{i} \"x\">")).collect();
let many_entities = format!("<!DOCTYPE svg [{entities}]><svg><variableSets/></svg>");
// 1 KiB of text referenced 9,000 times.
let expanding = format!("<!DOCTYPE svg [<!ENTITY big \"{}\">]><svg>{}<variableSets/></svg>", "x".repeat(1024), "&big;".repeat(9000));
// References that expand to just under the cap, then a comment that takes the text past it.
let then_literal = format!(
"<!DOCTYPE svg [<!ENTITY big \"{}\">]><svg>{}<!--{}--><variableSets/></svg>",
"x".repeat(1024),
"&big;".repeat(8000),
" ".repeat(300_000)
);
let cases = [
("not well-formed XML", "<variableSets><variableSet>".to_string()),
("no `variableSets` element", "<svg><variables/></svg>".to_string()),
("declares no variables", "<variableSets><variableSet><variables/></variableSet></variableSets>".to_string()),
("is declared twice", library(&[("Name", "textcontent"), ("Name", "textcontent")], &[])),
("has no `trait`", ok.replace(" trait=\"textcontent\"", "")),
("missing `varName`", ok.replace("varName=\"Name\"", "varName=\" \"")),
("missing `dataSetName`", ok.replace(" dataSetName=\"A\"", "")),
("data set `A` is declared twice", library(&[("Name", "textcontent")], &[("A", &[]), ("A", &[])])),
("which the library doesn't declare", library(&[("Name", "textcontent")], &[("A", &[("Nmae", &["x"])])])),
("holds two values for `Name`", library(&[("Name", "textcontent")], &[("A", &[("Name", &["x"]), ("Name", &["y"])])])),
("nests elements more than 256 deep", deep),
("something other than entities of plain text", markup),
("something other than entities of plain text", hidden),
("something other than entities of plain text", nested),
("something other than entities of plain text", parameter),
("something other than entities of plain text", element),
("nests elements more than 256 deep", quoted),
("more than 64 entities", many_entities),
("entities expand to more than", expanding),
("entities expand to more than", then_literal),
("larger than", format!("{ok}<!--{}-->", " ".repeat(8 << 20))),
];
for (why, xml) in cases {
let e = s.execute("variable.loadLibrary", &json!({"xml": xml})).unwrap_err().to_string();
assert!(e.contains(why), "{why}: {e}");
}
let names: Vec<String> = (0..257).map(|i| format!("V{i}")).collect();
let many: Vec<(&str, &str)> = names.iter().map(|n| (n.as_str(), "textcontent")).collect();
let e = s.execute("variable.loadLibrary", &json!({"xml": library(&many, &[])})).unwrap_err().to_string();
assert!(e.contains("more than 256 variables"), "{e}");
// A Latin-1 é in a comment.
let latin1 = [ok.as_bytes(), b"<!-- \xE9 -->"].concat();
let e = s.execute("variable.loadLibrary", &json!({"dataBase64": vectorcraft_format::base64_encode(&latin1)})).unwrap_err().to_string();
assert!(e.contains("not UTF-8"), "{e}");
let e = s.execute("variable.loadLibrary", &json!({})).unwrap_err().to_string();
assert!(e.contains("give `path`, `xml` or `dataBase64`"), "{e}");
let vars = &s.doc().unwrap().doc.variables;
assert!(vars.variables.is_empty() && vars.datasets.is_empty(), "a refused library loads nothing");
}
/// Entities whose values hold plain text expand to it, in attribute values too, and a reference in
/// a CDATA section stays as written.
#[test]
fn a_variable_library_expands_entities_of_plain_text() {
let mut s = session();
let xml = "<!DOCTYPE svg [<!ENTITY a \"x\"><!ENTITY b 'y \"z\"'>]><svg><variableSets><variableSet><variables>\
<variable varName=\"Name\" trait=\"textcontent\"/></variables><sampleDataSets><sampleDataSet dataSetName=\"&b;\">\
<Name><p>&a;-&b;-&a;<![CDATA[&a;]]></p></Name></sampleDataSet></sampleDataSets></variableSet></variableSets></svg>";
s.execute("variable.loadLibrary", &json!({"xml": xml})).unwrap();
let value = s.doc().unwrap().doc.variables.dataset("y \"z\"").and_then(|d| d.values.get("Name")).cloned();
assert_eq!(value, Some(vectorcraft_doc::DataValue::Text("x-y \"z\"-x&a;".into())));
}
/// Save Variable Library writes what Load Variable Library reads back: the text variables in their
/// order, every data set by name and in order, and each text value as it was, with several lines,
/// markup characters, quotes, a carriage return and an empty value. Variables of other kinds are
/// left out with their values, named in `skipped` and in the warning. The library is written while
/// the variables are locked too, to a file with `path`.
#[test]
fn a_saved_variable_library_loads_back_the_text_variables_and_data_sets() {
let mut s = session();
let t = text(&mut s, "Alice");
for (name, kind) in [("Name", "text"), ("Shown", "visibility"), ("Note", "text"), ("Photo", "image"), ("Badge", "visibility")] {
s.execute("variable.define", &json!({"name": name, "kind": kind})).unwrap();
}
s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).unwrap();
s.execute("dataset.new", &json!({"name": "First", "values": {"Name": "Bravo", "Note": "One\nTwo & <three>\r\n'four' \"five\"", "Shown": true}}))
.unwrap();
s.execute("dataset.new", &json!({"name": "Say \"hi\" & <go>\tnow", "values": {"Name": "", "Photo": "a.png"}})).unwrap();
s.execute("dataset.new", &json!({"name": "Empty"})).unwrap();
s.execute("variable.lock", &json!({"locked": true})).unwrap();
let before = s.doc().unwrap().doc.variables.clone();
let saved = s.execute("variable.saveLibrary", &json!({})).unwrap();
assert_eq!(saved["variables"], json!(["Name", "Note"]));
assert_eq!(saved["datasets"], json!(3));
assert_eq!(
saved["skipped"],
json!([{"name": "Shown", "kind": "visibility"}, {"name": "Photo", "kind": "image"}, {"name": "Badge", "kind": "visibility"}])
);
assert_eq!(saved["warning"], "variables that aren't text were left out: visibility (2), image (1)");
assert_eq!(s.doc().unwrap().doc.variables, before, "saving changes nothing");
let mut other = session();
let loaded = other.execute("variable.loadLibrary", &json!({"xml": saved["xml"]})).unwrap();
assert_eq!((loaded["skipped"].clone(), loaded.get("warning")), (json!([]), None));
let vars = &other.doc().unwrap().doc.variables;
assert_eq!(vars.variables.iter().map(|v| (v.name.as_str(), v.kind.label())).collect::<Vec<_>>(), [("Name", "text"), ("Note", "text")]);
let text_only: Vec<vectorcraft_doc::DataSet> = before
.datasets
.iter()
.map(|d| {
let mut d = d.clone();
d.values.retain(|k, _| k == "Name" || k == "Note");
d
})
.collect();
assert_eq!(vars.datasets, text_only);
let dir = Folder::new("vars-save-library");
let path = dir.file("library.xml");
let written = s.execute("variable.saveLibrary", &json!({"path": path})).unwrap();
assert_eq!(written["path"], json!(path));
assert!(written.get("xml").is_none());
assert_eq!(std::fs::read_to_string(&path).unwrap(), saved["xml"].as_str().unwrap());
let mut from_file = session();
from_file.execute("variable.loadLibrary", &json!({"path": path})).unwrap();
assert_eq!(from_file.doc().unwrap().doc.variables.datasets, text_only);
}
/// The library is the XML of the public grammar: a document type declaration with the namespace
/// entities, `variableSets` › `variableSet` › `variables` with a `textcontent` variable per text
/// variable, and `sampleDataSets` with a `sampleDataSet` per data set, holding an element per text
/// variable it has a value for, with a `p` element per line. Markup characters, a carriage return,
/// and a tab or quote in a data set name are escaped. A document with text variables and no data
/// sets writes an empty `sampleDataSets`.
#[test]
fn a_saved_variable_library_has_the_library_layout() {
let mut s = session();
for (name, kind) in [("Name", "text"), ("Shown", "visibility"), ("Note", "text")] {
s.execute("variable.define", &json!({"name": name, "kind": kind})).unwrap();
}
let head = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\
<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\" [\n\
\t<!ENTITY ns_vars \"http://ns.adobe.com/Variables/1.0/\">\n\
\t<!ENTITY ns_custom \"http://ns.adobe.com/GenericCustomNamespace/1.0/\">\n\
\t<!ENTITY ns_flows \"http://ns.adobe.com/Flows/1.0/\">\n\
]>\n\
<svg>\n\
<variableSets xmlns=\"&ns_vars;\">\n\
\t<variableSet varSetName=\"binding1\" locked=\"none\">\n\
\t\t<variables>\n\
\t\t\t<variable varName=\"Name\" trait=\"textcontent\" category=\"&ns_flows;\"></variable>\n\
\t\t\t<variable varName=\"Note\" trait=\"textcontent\" category=\"&ns_flows;\"></variable>\n\
\t\t</variables>\n\
\t\t<v:sampleDataSets xmlns:v=\"&ns_vars;\" xmlns=\"&ns_custom;\">\n";
let tail = "\t\t</v:sampleDataSets>\n\t</variableSet>\n</variableSets>\n</svg>\n";
let empty = s.execute("variable.saveLibrary", &json!({})).unwrap();
assert_eq!(empty["xml"].as_str().unwrap(), format!("{head}{tail}"));
assert_eq!(empty["datasets"], json!(0));
s.execute("dataset.new", &json!({"name": "A &\t\"B\"", "values": {"Name": "x\r\ny < z", "Shown": false}})).unwrap();
s.execute("dataset.new", &json!({"name": "C", "values": {"Note": ""}})).unwrap();
let xml = s.execute("variable.saveLibrary", &json!({})).unwrap()["xml"].as_str().unwrap().to_string();
let sets = "\t\t\t<v:sampleDataSet dataSetName=\"A &amp;&#9;&quot;B&quot;\">\n\
\t\t\t\t<Name>\n\
\t\t\t\t\t<p>x&#13;</p>\n\
\t\t\t\t\t<p>y &lt; z</p>\n\
\t\t\t\t</Name>\n\
\t\t\t</v:sampleDataSet>\n\
\t\t\t<v:sampleDataSet dataSetName=\"C\">\n\
\t\t\t\t<Note>\n\
\t\t\t\t\t<p></p>\n\
\t\t\t\t</Note>\n\
\t\t\t</v:sampleDataSet>\n";
assert_eq!(xml, format!("{head}{sets}{tail}"));
}
/// Save Variable Library refuses a document without variables or without text variables, a text
/// variable whose name can't name an element, a data set name or text value with a character XML
/// can't hold, a name that Load Variable Library would read differently or refuse, and a library
/// past the caps that Load Variable Library reads. A refused save writes no file.
#[test]
fn save_variable_library_refuses_what_a_library_cant_hold() {
let dir = Folder::new("vars-save-refused");
let path = dir.file("library.xml");
let refused = |s: &mut Session, why: &str| {
let e = s.execute("variable.saveLibrary", &json!({"path": path})).unwrap_err().to_string();
assert!(e.contains(why), "{why}: {e}");
assert!(!std::path::Path::new(&path).exists(), "{why}: a file was written");
};
let mut s = session();
refused(&mut s, "define a variable first");
s.execute("variable.define", &json!({"name": "Shown", "kind": "visibility"})).unwrap();
refused(&mut s, "holds text variables, and the document has none");
for name in ["First Name", "1st", "a:b", "x/y", "-dash"] {
let mut s = session();
s.execute("variable.define", &json!({"name": name, "kind": "text"})).unwrap();
refused(&mut s, &format!("variable `{name}` can't be saved in a variable library"));
}
for name in ["Ünïcode", "_x", "a.b-c_d", "名前"] {
let mut s = session();
s.execute("variable.define", &json!({"name": name, "kind": "text"})).unwrap();
s.execute("dataset.new", &json!({"name": "A", "values": {name: "v"}})).unwrap();
let xml = s.execute("variable.saveLibrary", &json!({})).unwrap()["xml"].clone();
let mut other = session();
other.execute("variable.loadLibrary", &json!({"xml": xml})).unwrap();
assert_eq!(other.doc().unwrap().doc.variables.datasets, s.doc().unwrap().doc.variables.datasets, "{name}");
}
let mut s = session();
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("dataset.new", &json!({"name": "A", "values": {"Name": "bell \u{7}"}})).unwrap();
refused(&mut s, "data set `A` holds a control character");
let mut s = session();
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("dataset.new", &json!({"name": "B\u{1}"})).unwrap();
refused(&mut s, "holds a control character");
// A document opened from a file can hold names that the commands don't make, and Load Variable
// Library would read them differently or refuse them.
let renamed = |dataset: Option<&str>, variable: Option<&str>| {
let mut s = session();
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("variable.define", &json!({"name": "Other", "kind": "text"})).unwrap();
s.execute("dataset.new", &json!({"name": "A"})).unwrap();
s.execute("dataset.new", &json!({"name": "B"})).unwrap();
let vars = &mut std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc).variables;
if let Some(name) = dataset {
vars.datasets[1].name = name.to_string();
}
if let Some(name) = variable {
vars.variables[1].name = name.to_string();
}
s
};
for name in [" B", "B\t", "B\n"] {
refused(&mut renamed(Some(name), None), "starts or ends with white space");
}
refused(&mut renamed(Some(""), None), "missing `dataSetName`");
refused(&mut renamed(Some(&"d".repeat(256)), None), "`dataSetName` is longer than 255 characters");
refused(&mut renamed(Some("A"), None), "dataset `A` already exists");
refused(&mut renamed(None, Some(&"v".repeat(256))), "`varName` is longer than 255 characters");
refused(&mut renamed(None, Some("Name")), "variable `Name` is already defined");
let mut s = renamed(Some(&"d".repeat(255)), Some(&"v".repeat(255)));
let xml = s.execute("variable.saveLibrary", &json!({})).unwrap()["xml"].clone();
let mut other = session();
other.execute("variable.loadLibrary", &json!({"xml": xml})).unwrap();
assert_eq!(other.doc().unwrap().doc.variables.datasets, s.doc().unwrap().doc.variables.datasets);
let mut s = session();
for i in 0..257 {
s.execute("variable.define", &json!({"name": format!("V{i}"), "kind": "text"})).unwrap();
}
refused(&mut s, "more than 256 variables");
let mut s = session();
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
for i in 0..1001 {
s.execute("dataset.new", &json!({"name": format!("D{i}")})).unwrap();
}
refused(&mut s, "more than 1000 data sets");
let mut s = session();
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("dataset.new", &json!({"name": "A", "values": {"Name": "x".repeat(9 << 20)}})).unwrap();
refused(&mut s, "larger than 8388608 bytes");
}
/// A library near the size cap is saved only when Load Variable Library reads it back, after its
/// entity references expand: each text variable's `&ns_flows;`, two `&ns_vars;` and one
/// `&ns_custom;` grow by 20, 25 and 36 bytes.
#[test]
fn a_saved_variable_library_near_the_size_cap_loads_back() {
let mut s = session();
for i in 0..200 {
s.execute("variable.define", &json!({"name": format!("V{i}"), "kind": "text"})).unwrap();
}
s.execute("dataset.new", &json!({"name": "A", "values": {"V0": ""}})).unwrap();
let base = s.execute("variable.saveLibrary", &json!({})).unwrap()["xml"].as_str().unwrap().len();
let room = (8 << 20) - (200 * 20 + 2 * 25 + 36);
s.execute("dataset.set", &json!({"name": "A", "values": {"V0": "x".repeat(room - base + 1)}})).unwrap();
let e = s.execute("variable.saveLibrary", &json!({})).unwrap_err().to_string();
assert!(e.contains("larger than 8388608 bytes"), "{e}");
s.execute("dataset.set", &json!({"name": "A", "values": {"V0": "x".repeat(room - base)}})).unwrap();
let xml = s.execute("variable.saveLibrary", &json!({})).unwrap()["xml"].clone();
assert_eq!(xml.as_str().map(str::len), Some(room));
let mut other = session();
other.execute("variable.loadLibrary", &json!({"xml": xml})).unwrap();
assert_eq!(other.doc().unwrap().doc.variables.datasets, s.doc().unwrap().doc.variables.datasets);
// The same library with the value grown to a file of exactly 8 MiB doesn't load back.
let full = xml.as_str().unwrap().replacen("<p>x", &format!("<p>{}x", "x".repeat((8 << 20) - room)), 1);
assert_eq!(full.len(), 8 << 20);
let e = other.execute("variable.loadLibrary", &json!({"xml": full})).unwrap_err().to_string();
assert!(e.contains("entities expand to more than"), "{e}");
}
/// While the variables are locked, a library that would delete or define a variable is refused, as
/// Delete Variable and New Variable are, and the document keeps its variables and data sets. That
/// covers a library without text variables loaded into a document with variables, and one that
/// adds a text variable to the document's. A library of the document's own text variables, and
/// one without text variables loaded into a document without variables, delete and define none:
/// they load, replacing the data sets.
#[test]
fn locked_variables_refuse_a_library_that_deletes_or_defines_variables() {
let mut s = session();
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("dataset.new", &json!({"name": "A", "values": {"Name": "Old"}})).unwrap();
s.execute("variable.lock", &json!({"locked": true})).unwrap();
let before = s.doc().unwrap().doc.variables.clone();
for vars in [&[("Shown", "other")][..], &[("Name", "textcontent"), ("Note", "textcontent")][..]] {
let e = s.execute("variable.loadLibrary", &json!({"xml": library(vars, &[("B", &[])])})).unwrap_err();
assert!(matches!(&e, EngineError::Disabled(cmd, why) if cmd == "variable.loadLibrary" && why == "variables are locked"), "{e}");
assert_eq!(s.doc().unwrap().doc.variables, before);
}
// A library of the document's own text variable deletes and defines none: it loads its data sets.
let r = s.execute("variable.loadLibrary", &json!({"xml": library(&[("Name", "textcontent")], &[("B", &[])])})).unwrap();
assert_eq!((r["deleted"].clone(), r["deletedDataSets"].clone()), (json!([]), json!(["A"])));
let mut s = session();
s.execute("dataset.new", &json!({"name": "A"})).unwrap();
s.execute("variable.lock", &json!({"locked": true})).unwrap();
let e = s.execute("variable.loadLibrary", &json!({"xml": library(&[("Name", "textcontent")], &[])})).unwrap_err();
assert!(matches!(&e, EngineError::Disabled(_, why) if why == "variables are locked"), "{e}");
let r = s.execute("variable.loadLibrary", &json!({"xml": library(&[("Shown", "other")], &[("B", &[])])})).unwrap();
assert_eq!((r["deletedDataSets"].clone(), r["names"].clone()), (json!(["A"]), json!(["B"])));
let vars = &s.doc().unwrap().doc.variables;
assert!(vars.variables.is_empty() && vars.locked);
assert_eq!(vars.datasets.iter().map(|d| d.name.as_str()).collect::<Vec<_>>(), ["B"]);
}
/// A graph variable's data set applies its CSV as `graph.setData` does: a right-axis series the row's data no longer
/// has is dropped, and a row with neither a label nor a value is not drawn as an empty slot.
#[test]
fn graph_variable_applies_csv_as_set_data() {
let mut s = session();
let create = json!({"type": "column", "x": 10, "y": 10, "width": 120, "height": 80, "csv": ",a,b\nQ1,10,1000\nQ2,5,500"});
let id = s.execute("graph.create", &create).unwrap()["id"].as_u64().unwrap();
let other = s.execute("graph.create", &create).unwrap()["id"].as_u64().unwrap();
for g in [id, other] {
s.execute("graph.setType", &json!({"id": g, "seriesIndexes": [1], "valueAxis": "right"})).unwrap();
}
s.execute("select.set", &json!({"ids": [id]})).unwrap();
s.execute("variable.makeGraphDynamic", &json!({})).unwrap();
let name = s.execute("variable.list", &json!({})).unwrap()["variables"][0]["name"].as_str().unwrap().to_string();
let spec = |s: &Session, g: u64| s.doc().unwrap().doc.node(NodeId(g)).unwrap().extras().graph.as_deref().unwrap().clone();
// A dropped empty row wider than the others (`,,`) adds no blank series, so the right axis keeps nothing.
for (row, csv) in [("Wide", ",a\nQ1,10\n,,\nQ2,5"), ("One", ",a\nQ1,10\nQ2,5"), ("Blank", ",a\nQ1,10\n,\nQ2,5")] {
s.execute("dataset.new", &json!({"name": row, "values": {name.clone(): csv}})).unwrap();
s.execute("dataset.select", &json!({"name": row})).unwrap();
s.execute("graph.setData", &json!({"id": other, "csv": csv})).unwrap();
let (merged, set) = (spec(&s, id), spec(&s, other));
assert_eq!(merged.right_series, set.right_series, "{row}");
assert_eq!(merged.categories, set.categories, "{row}");
assert_eq!(merged.cells(), set.cells(), "{row}");
assert!(merged.right_series.is_empty(), "{row}");
assert_eq!(merged.cells(), [[Some(10.0)], [Some(5.0)]], "{row}");
}
assert!(spec(&s, id).right_series.is_empty());
assert_eq!(spec(&s, id).categories, ["Q1", "Q2"]);
}
#[test]
fn graph_variable_applies_csv_and_captures() {
let mut s = session();
let id = s
.execute(
"graph.create",
&json!({
"type": "column",
"x": 10,
"y": 10,
"width": 120,
"height": 80,
"csv": ",Sales\nA,1\nB,2"
}),
)
.unwrap()["id"]
.as_u64()
.unwrap();
s.execute("select.set", &json!({"ids": [id]})).unwrap();
s.execute("variable.makeGraphDynamic", &json!({})).unwrap();
let name = s.execute("variable.list", &json!({})).unwrap()["variables"][0]["name"].as_str().unwrap().to_string();
assert_eq!(s.execute("variable.list", &json!({})).unwrap()["variables"][0]["kind"], "graph");
s.execute("dataset.new", &json!({"name": "Alt", "values": {name.clone(): ",Sales\nA,10\nB,20"}})).unwrap();
let v = s.execute("dataset.select", &json!({"name": "Alt"})).unwrap();
assert_eq!((v["applied"].as_u64(), v["skipped"].as_u64()), (Some(1), Some(0)));
let csv = s.doc().unwrap().doc.variable_value(NodeId(id), vectorcraft_doc::VariableKind::Graph).unwrap();
match csv {
vectorcraft_doc::DataValue::Graph(c) => assert!(c.contains("10") && c.contains("20"), "{c}"),
other => panic!("expected graph csv, got {other:?}"),
}
// Binding a non-graph is refused.
let r = rect(&mut s);
assert!(s.execute("variable.bind", &json!({"variable": name, "ids": [r.0]})).is_err());
}
/// A Variables command that leaves the document as it was (binding art to the variable it already
/// has, unbinding art that has none, deleting a variable or data set that does not exist, writing a
/// row's own values back, applying the active data set again) records no undo step: the step to
/// redo stays, and the document stays unmodified.
#[test]
fn a_variables_command_that_changes_nothing_records_no_undo_step() {
let dir = Folder::new("vars-noop");
let red_path = dir.file("red.png");
write(&red_path, &png(8, 8, RED));
let mut s = session();
let t = text(&mut s, "Alice");
let r = rect(&mut s);
let img = place(&mut s, &red_path);
let other = rect(&mut s);
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("variable.define", &json!({"name": "Show", "kind": "visibility"})).unwrap();
s.execute("variable.define", &json!({"name": "Photo", "kind": "image"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).unwrap();
s.execute("variable.bind", &json!({"variable": "Show", "ids": [r.0]})).unwrap();
s.execute("variable.bind", &json!({"variable": "Photo", "ids": [img.0]})).unwrap();
s.execute("dataset.new", &json!({"name": "A", "values": {"Name": "Bob", "Show": false, "Photo": red_path}})).unwrap();
s.execute("dataset.select", &json!({"name": "A"})).unwrap();
let a = json!({"name": "A", "values": {"Name": "Bob", "Show": false, "Photo": red_path}});
let calls = [
("variable.bind", json!({"variable": "Show", "ids": [r.0]})),
("variable.unbind", json!({"ids": [other.0]})),
("variable.unbind", json!({"variable": "Name", "ids": [r.0]})),
("variable.delete", json!({"names": ["Nope"]})),
("dataset.delete", json!({"names": ["Nope"]})),
("dataset.set", a),
("dataset.update", json!({})),
("dataset.update", json!({"all": true})),
("dataset.select", json!({"name": "A"})),
("dataset.next", json!({})),
("dataset.prev", json!({})),
];
for (cmd, p) in calls {
super::select_with_a_step_to_redo(&mut s, &[other]);
let before = super::history(&s);
assert_eq!((before.1, before.2), (1, false), "a step to redo, and nothing unsaved");
s.execute(cmd, &p).unwrap();
assert_eq!(super::history(&s), before, "{cmd} {p}");
}
assert_eq!(plain(&s, t), "Bob");
// Applying the row again still deselects the art it leaves hidden, without an undo step.
s.execute("select.set", &json!({"ids": [r.0, other.0]})).unwrap();
assert!(s.doc().unwrap().selection.objects.contains(&r), "the hidden rectangle is selected");
let before = super::history(&s);
s.execute("dataset.select", &json!({"name": "A"})).unwrap();
assert_eq!((super::history(&s), s.doc().unwrap().selection.objects.clone()), (before, vec![other]));
// A command that changes something is still one undo step, and it clears Redo.
super::select_with_a_step_to_redo(&mut s, &[other]);
let undo = super::history(&s).0;
s.execute("variable.bind", &json!({"variable": "Show", "ids": [other.0]})).unwrap();
assert_eq!(super::history(&s), (undo + 1, 0, true));
s.execute("dataset.new", &json!({"name": "B", "values": {"Name": "Cy"}})).unwrap();
s.doc_mut().unwrap().mark_saved();
let undo = super::history(&s).0;
s.execute("dataset.next", &json!({})).unwrap();
assert_eq!((super::history(&s), plain(&s, t)), ((undo + 1, 0, true), "Cy".to_string()));
}
/// A Variables command that changes nothing, run while the Type tool is typing, still ends the
/// typing session as one that changes something does: the typing is its own undo step, and the
/// next keystroke starts a new one.
#[test]
fn a_variables_command_that_changes_nothing_ends_the_typing() {
use vectorcraft_tools::{PointerEvent, PointerKind};
let mut s = session();
let v = ViewInfo::default();
let t = text(&mut s, "Hello");
let r = s.execute("shape.rectangle", &json!({"x": 200, "y": 200, "width": 50, "height": 40})).unwrap();
let r = NodeId(r["id"].as_u64().unwrap());
s.execute("variable.define", &json!({"name": "Show", "kind": "visibility"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Show", "ids": [r.0]})).unwrap();
s.execute("dataset.new", &json!({"name": "A", "values": {"Show": true}})).unwrap();
s.execute("dataset.select", &json!({"name": "A"})).unwrap();
for (cmd, p) in [("variable.bind", json!({"variable": "Show", "ids": [r.0]})), ("dataset.select", json!({"name": "A"}))] {
s.select_tool("type", v).unwrap();
let b = s.doc().unwrap().doc.bounds_of(&[t], false).unwrap();
for kind in [PointerKind::Down, PointerKind::Up] {
s.pointer(&PointerEvent::new(kind, b.x1 - 1.0, b.center().y), v).unwrap();
}
s.tool_text("!", v).unwrap();
assert!(crate::cmd::typing_in_progress(&s), "{cmd}");
let steps = s.doc().unwrap().history.undo.len();
s.execute(cmd, &p).unwrap();
assert!(!crate::cmd::typing_in_progress(&s), "{cmd}");
let labels: Vec<String> = s.doc().unwrap().history.undo.iter().map(|e| e.label.clone()).collect();
assert_eq!((labels.len(), labels.last().map(String::as_str)), (steps + 1, Some("Typing")), "{cmd}");
}
}
/// Applying the active data set again to threaded type that already holds the row's story records
/// no undo step.
#[test]
fn applying_a_story_row_again_records_no_undo_step() {
let (mut s, a, b, _) = threaded(200);
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Name", "ids": [a.0]})).unwrap();
s.execute("dataset.capture", &json!({"name": "Orig"})).unwrap();
s.execute("dataset.select", &json!({"name": "Orig"})).unwrap();
assert!(!plain(&s, b).is_empty());
super::select_with_a_step_to_redo(&mut s, &[a]);
let before = super::history(&s);
s.execute("dataset.select", &json!({"name": "Orig"})).unwrap();
assert_eq!(super::history(&s), before);
}
/// Applying the active data set again to a graph whose data already matches the row records no
/// undo step, and a row with other data still rebuilds the graph in one.
#[test]
fn applying_a_graph_row_again_records_no_undo_step() {
let mut s = session();
let graph = json!({"type": "column", "x": 10, "y": 10, "width": 120, "height": 80, "csv": ",Sales\nA,1\nB,2"});
let id = s.execute("graph.create", &graph).unwrap()["id"].as_u64().unwrap();
s.execute("select.set", &json!({"ids": [id]})).unwrap();
s.execute("variable.makeGraphDynamic", &json!({})).unwrap();
let name = s.execute("variable.list", &json!({})).unwrap()["variables"][0]["name"].as_str().unwrap().to_string();
s.execute("dataset.new", &json!({"name": "A", "values": {name.clone(): ",Sales\nA,10\nB,20"}})).unwrap();
s.execute("dataset.new", &json!({"name": "B", "values": {name: ",Sales\nA,3\nB,4"}})).unwrap();
s.execute("dataset.select", &json!({"name": "A"})).unwrap();
super::select_with_a_step_to_redo(&mut s, &[NodeId(id)]);
let before = super::history(&s);
let v = s.execute("dataset.select", &json!({"name": "A"})).unwrap();
assert_eq!((v["applied"].as_u64(), super::history(&s)), (Some(1), before));
s.execute("dataset.select", &json!({"name": "B"})).unwrap();
assert_eq!(super::history(&s), (before.0 + 1, 0, true));
}