342 lines
20 KiB
Rust
342 lines
20 KiB
Rust
//! The Libraries panel's libraries (#745): making, renaming and deleting them, adding graphics,
|
||
//! colours and text styles from the selection, using them in another document, and keeping them
|
||
//! in files across sessions.
|
||
|
||
use serde_json::{Value, json};
|
||
|
||
use super::*;
|
||
|
||
fn session() -> Session {
|
||
let mut s = Session::new();
|
||
s.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
|
||
s
|
||
}
|
||
|
||
fn run(s: &mut Session, id: &str, p: Value) -> Value {
|
||
s.execute(id, &p).unwrap_or_else(|e| panic!("{id} {p}: {e}"))
|
||
}
|
||
|
||
fn objects(s: &Session) -> usize {
|
||
s.doc().unwrap().doc.layers[0].children().map_or(0, |c| c.len())
|
||
}
|
||
|
||
fn hex_of(s: &Session, id: &Value, stroke: bool) -> String {
|
||
let n = s.doc().unwrap().doc.node(NodeId(id.as_u64().unwrap())).unwrap().clone();
|
||
let paint = if stroke { n.appearance.stroke().unwrap().paint.clone() } else { n.appearance.fill().unwrap().paint.clone() };
|
||
paint.color().unwrap().to_hex()
|
||
}
|
||
|
||
#[test]
|
||
fn libraries_are_made_renamed_and_deleted() {
|
||
let mut s = session();
|
||
assert_eq!(run(&mut s, "library.list", json!({}))["libraries"], json!([]));
|
||
let id = run(&mut s, "library.create", json!({"name": "Brand"}))["id"].as_str().unwrap().to_string();
|
||
let other = run(&mut s, "library.create", json!({"name": "Brand"}))["id"].as_str().unwrap().to_string();
|
||
assert_ne!(id, other, "two libraries of one name are two files");
|
||
run(&mut s, "library.rename", json!({"library": other, "name": "Client"}));
|
||
let list = run(&mut s, "library.list", json!({}));
|
||
let names: Vec<&str> = list["libraries"].as_array().unwrap().iter().map(|l| l["name"].as_str().unwrap()).collect();
|
||
assert_eq!(names, ["Brand", "Client"]);
|
||
assert!(list["folder"].is_null(), "no folder: libraries last for the session");
|
||
run(&mut s, "library.delete", json!({"library": "client"}));
|
||
assert_eq!(run(&mut s, "library.list", json!({}))["libraries"].as_array().unwrap().len(), 1);
|
||
assert!(s.execute("library.get", &json!({"library": "Client"})).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn a_graphic_goes_into_a_library_and_comes_out_in_another_document() {
|
||
let mut s = session();
|
||
let id = run(&mut s, "shape.ellipse", json!({"x": 10, "y": 10, "width": 40, "height": 30}))["id"].clone();
|
||
run(&mut s, "paint.setFill", json!({"color": "#cc3300", "ids": [id]}));
|
||
run(&mut s, "select.set", json!({"ids": [id]}));
|
||
// With no library yet, adding makes "My Library".
|
||
let added = run(&mut s, "library.add", json!({"kind": "graphic"}));
|
||
assert_eq!(added["existing"], false);
|
||
let lib = added["library"].as_str().unwrap().to_string();
|
||
let item = added["id"].as_str().unwrap().to_string();
|
||
let got = run(&mut s, "library.get", json!({"library": lib}));
|
||
assert_eq!(got["name"], "My Library");
|
||
let g = &got["graphics"][0];
|
||
// Its size as it draws: the 1 pt stroke included.
|
||
assert!((g["width"].as_f64().unwrap() - 41.0).abs() < 1e-6 && (g["height"].as_f64().unwrap() - 31.0).abs() < 1e-6, "{g}");
|
||
assert!(!g["thumbnail"].as_str().unwrap().is_empty(), "a thumbnail of the art");
|
||
// The same art again is the item already there.
|
||
assert_eq!(run(&mut s, "library.add", json!({"kind": "graphic"}))["existing"], true);
|
||
// Placed in another document, centred where asked, selected, one undo step.
|
||
run(&mut s, "file.new", json!({"width": 300, "height": 300}));
|
||
let placed = run(&mut s, "library.use", json!({"library": lib, "kind": "graphic", "item": item, "center": [150, 100]}));
|
||
let new_id = placed["ids"][0].clone();
|
||
let st = s.doc().unwrap();
|
||
let b = st.doc.node(NodeId(new_id.as_u64().unwrap())).unwrap().geometric_bounds().unwrap();
|
||
assert!((b.center().x - 150.0).abs() < 1e-6 && (b.center().y - 100.0).abs() < 1e-6, "{b:?}");
|
||
assert_eq!(st.selection.objects, vec![NodeId(new_id.as_u64().unwrap())]);
|
||
assert_eq!(hex_of(&s, &new_id, false), "#cc3300");
|
||
assert_eq!(objects(&s), 1);
|
||
run(&mut s, "edit.undo", json!({}));
|
||
assert_eq!(objects(&s), 0);
|
||
// A graphic is removed by id.
|
||
run(&mut s, "library.removeItem", json!({"library": lib, "kind": "graphic", "item": item}));
|
||
assert!(s.execute("library.use", &json!({"library": lib, "kind": "graphic", "item": item})).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn colours_come_from_the_selection_and_paint_it() {
|
||
let mut s = session();
|
||
let lib = run(&mut s, "library.create", json!({}))["id"].as_str().unwrap().to_string();
|
||
let a = run(&mut s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 20, "height": 20}))["id"].clone();
|
||
run(&mut s, "paint.setFill", json!({"color": "#123456", "ids": [a]}));
|
||
run(&mut s, "paint.setStroke", json!({"color": "#abcdef", "ids": [a]}));
|
||
run(&mut s, "select.set", json!({"ids": [a]}));
|
||
assert_eq!(run(&mut s, "library.add", json!({"library": lib, "kind": "fillColor"}))["name"], "#123456");
|
||
assert_eq!(run(&mut s, "library.add", json!({"library": lib, "kind": "strokeColor"}))["name"], "#abcdef");
|
||
assert_eq!(run(&mut s, "library.add", json!({"library": lib, "kind": "fillColor"}))["existing"], true);
|
||
let b = run(&mut s, "shape.rectangle", json!({"x": 50, "y": 0, "width": 20, "height": 20}))["id"].clone();
|
||
run(&mut s, "select.set", json!({"ids": [b]}));
|
||
run(&mut s, "library.use", json!({"library": lib, "kind": "fillColor", "item": "#123456"}));
|
||
run(&mut s, "library.use", json!({"library": lib, "kind": "fillColor", "item": "#123456", "to": "stroke"}));
|
||
assert_eq!(hex_of(&s, &b, false), "#123456");
|
||
assert_eq!(hex_of(&s, &b, true), "#123456");
|
||
assert!(s.execute("library.use", &json!({"library": lib, "kind": "fillColor", "item": "#nope"})).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn text_styles_come_from_the_selected_type_and_apply_in_one_step() {
|
||
let mut s = session();
|
||
let lib = run(&mut s, "library.create", json!({}))["id"].as_str().unwrap().to_string();
|
||
assert!(s.execute("library.add", &json!({"library": lib, "kind": "charStyle"})).is_err(), "no type selected");
|
||
let t = run(&mut s, "text.create", json!({"x": 10, "y": 50, "text": "Heading", "size": 31}))["id"].clone();
|
||
run(&mut s, "select.set", json!({"ids": [t]}));
|
||
let name = run(&mut s, "library.add", json!({"library": lib, "kind": "charStyle"}))["name"].as_str().unwrap().to_string();
|
||
assert!(name.ends_with("31 pt"), "{name}");
|
||
run(&mut s, "library.add", json!({"library": lib, "kind": "paraStyle"}));
|
||
// Another document: the style is added and applied, and one undo takes both back.
|
||
run(&mut s, "file.new", json!({"width": 200, "height": 200}));
|
||
let u = run(&mut s, "text.create", json!({"x": 10, "y": 50, "text": "Body", "size": 9}))["id"].clone();
|
||
run(&mut s, "select.set", json!({"ids": [u]}));
|
||
let undo = s.doc().unwrap().history.undo.len();
|
||
let r = run(&mut s, "library.use", json!({"library": lib, "kind": "charStyle", "item": name}));
|
||
assert_eq!(r["style"], name);
|
||
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1, "one undo step");
|
||
let size = |s: &Session| match &s.doc().unwrap().doc.node(NodeId(u.as_u64().unwrap())).unwrap().kind {
|
||
vectorcraft_doc::NodeKind::Text(t) => t.runs[0].style.size,
|
||
_ => 0.0,
|
||
};
|
||
assert_eq!(size(&s), 31.0);
|
||
run(&mut s, "edit.undo", json!({}));
|
||
assert_eq!(size(&s), 9.0);
|
||
assert!(s.doc().unwrap().doc.char_styles.iter().all(|c| c.name != name), "the style went with the undo");
|
||
// A style of that name with other attributes in the document: the library's comes in numbered.
|
||
run(&mut s, "charStyle.new", json!({"name": name, "attrs": {"size": 5}}));
|
||
let r = run(&mut s, "library.use", json!({"library": lib, "kind": "charStyle", "item": name}));
|
||
assert_eq!(r["style"], format!("{name} 2"));
|
||
}
|
||
|
||
#[cfg(not(target_arch = "wasm32"))]
|
||
#[test]
|
||
fn libraries_are_kept_in_files_across_sessions() {
|
||
let dir = std::env::temp_dir().join(format!("vc-libraries-{}", std::process::id()));
|
||
let _ = std::fs::remove_dir_all(&dir);
|
||
let folder = dir.to_string_lossy().to_string();
|
||
let mut s = session();
|
||
s.libraries.set_dir(Some(folder.clone()));
|
||
let lib = run(&mut s, "library.create", json!({"name": "Kept"}))["id"].as_str().unwrap().to_string();
|
||
let id = run(&mut s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 10, "height": 10}))["id"].clone();
|
||
run(&mut s, "select.set", json!({"ids": [id]}));
|
||
run(&mut s, "library.add", json!({"library": lib, "kind": "graphic", "name": "Square"}));
|
||
run(&mut s, "library.add", json!({"library": lib, "kind": "fillColor"}));
|
||
let mut t = session();
|
||
t.libraries.set_dir(Some(folder.clone()));
|
||
let got = run(&mut t, "library.get", json!({"library": "Kept"}));
|
||
assert_eq!(got["graphics"][0]["name"], "Square");
|
||
assert_eq!(got["colors"].as_array().unwrap().len(), 1);
|
||
run(&mut t, "library.delete", json!({"library": "Kept"}));
|
||
let mut u = session();
|
||
u.libraries.set_dir(Some(folder));
|
||
assert_eq!(run(&mut u, "library.list", json!({}))["libraries"], json!([]), "deleting removes the file");
|
||
// A damaged file is skipped.
|
||
std::fs::create_dir_all(&dir).unwrap();
|
||
std::fs::write(dir.join("Bad.vclibrary"), b"{not json").unwrap();
|
||
let mut v = session();
|
||
v.libraries.set_dir(Some(dir.to_string_lossy().to_string()));
|
||
assert_eq!(run(&mut v, "library.list", json!({}))["libraries"], json!([]));
|
||
let _ = std::fs::remove_dir_all(&dir);
|
||
}
|
||
|
||
/// A library "Brand" holding a graphic, a colour and a character style, each added from the
|
||
/// selection → (session, the graphic's id, the colour's name, the style's name).
|
||
fn brand() -> (Session, String, String, String) {
|
||
let mut s = session();
|
||
run(&mut s, "library.create", json!({"name": "Brand"}));
|
||
let r = run(&mut s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 20, "height": 20}))["id"].clone();
|
||
run(&mut s, "paint.setFill", json!({"color": "#336699", "ids": [r]}));
|
||
run(&mut s, "select.set", json!({"ids": [r]}));
|
||
let g = run(&mut s, "library.add", json!({"kind": "graphic", "name": "Logo"}))["id"].as_str().unwrap().to_string();
|
||
let c = run(&mut s, "library.add", json!({"kind": "fillColor"}))["name"].as_str().unwrap().to_string();
|
||
let t = run(&mut s, "text.create", json!({"x": 10, "y": 80, "text": "Type", "size": 18}))["id"].clone();
|
||
run(&mut s, "select.set", json!({"ids": [t]}));
|
||
let st = run(&mut s, "library.add", json!({"kind": "charStyle"}))["name"].as_str().unwrap().to_string();
|
||
(s, g, c, st)
|
||
}
|
||
|
||
/// The current library's groups as `library.get` reports them: (name, [(kind, item)]).
|
||
fn groups(s: &mut Session) -> Vec<(String, Vec<(String, String)>)> {
|
||
let got = run(s, "library.get", json!({}));
|
||
got["groups"]
|
||
.as_array()
|
||
.unwrap()
|
||
.iter()
|
||
.map(|g| {
|
||
let items = g["items"].as_array().unwrap().iter().map(|r| (r["kind"].as_str().unwrap().into(), r["item"].as_str().unwrap().into()));
|
||
(g["name"].as_str().unwrap().to_string(), items.collect())
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
fn refs(v: &[(&str, &str)]) -> Vec<(String, String)> {
|
||
v.iter().map(|(k, i)| (k.to_string(), i.to_string())).collect()
|
||
}
|
||
|
||
/// Graphics added one after another get ids of their own (the third used to repeat the second's).
|
||
#[test]
|
||
fn every_graphic_gets_an_id_of_its_own() {
|
||
let mut s = session();
|
||
run(&mut s, "library.create", json!({}));
|
||
let mut ids = vec![];
|
||
for x in [0, 30, 60, 90] {
|
||
let r = run(&mut s, "shape.rectangle", json!({"x": x, "y": 0, "width": 10 + x, "height": 10}))["id"].clone();
|
||
ids.push(run(&mut s, "library.add", json!({"kind": "graphic", "ids": [r]}))["id"].as_str().unwrap().to_string());
|
||
}
|
||
assert_eq!(ids, ["graphic", "graphic-2", "graphic-3", "graphic-4"]);
|
||
}
|
||
|
||
/// Items go into user-named groups and back out (#926); deleting a group keeps its items.
|
||
#[test]
|
||
fn items_are_grouped_moved_and_ungrouped() {
|
||
let (mut s, g, c, st) = brand();
|
||
assert!(groups(&mut s).is_empty(), "no groups yet");
|
||
// A group made with items; a graphic is named by its name or its id.
|
||
let r = run(&mut s, "library.createGroup", json!({"name": "Logos", "items": [{"kind": "graphic", "item": "Logo"}]}));
|
||
assert_eq!(r["group"], "Logos");
|
||
assert_eq!(groups(&mut s), [("Logos".into(), refs(&[("graphic", &g)]))]);
|
||
// A name taken (ignoring case) gets a number.
|
||
assert_eq!(run(&mut s, "library.createGroup", json!({"name": "logos"}))["group"], "logos 2");
|
||
run(&mut s, "library.renameGroup", json!({"group": "logos 2", "name": "Palette"}));
|
||
assert!(s.execute("library.renameGroup", &json!({"group": "Palette", "name": "LOGOS"})).is_err(), "another group's name");
|
||
assert!(s.execute("library.renameGroup", &json!({"group": "Nope", "name": "X"})).is_err());
|
||
// Moving an item takes it out of the group it was in; a colour is named as fill or stroke.
|
||
run(&mut s, "library.moveItem", json!({"kind": "strokeColor", "item": c, "group": "palette"}));
|
||
run(&mut s, "library.moveItem", json!({"kind": "charStyle", "item": st, "group": "Palette"}));
|
||
let moved = run(&mut s, "library.moveItem", json!({"kind": "graphic", "item": "Logo", "group": "Palette"}));
|
||
assert_eq!((moved["item"].as_str(), moved["group"].as_str()), (Some(g.as_str()), Some("Palette")));
|
||
assert_eq!(groups(&mut s), [("Logos".into(), vec![]), ("Palette".into(), refs(&[("color", &c), ("charStyle", &st), ("graphic", &g)]))]);
|
||
// Without a group, out of its group.
|
||
assert!(run(&mut s, "library.moveItem", json!({"kind": "fillColor", "item": c}))["group"].is_null());
|
||
assert!(s.execute("library.moveItem", &json!({"kind": "fillColor", "item": c, "group": "Nope"})).is_err());
|
||
assert!(s.execute("library.moveItem", &json!({"kind": "fillColor", "item": "#nope", "group": "Palette"})).is_err());
|
||
assert!(s.execute("library.createGroup", &json!({"items": [{"kind": "graphic", "item": "nope"}]})).is_err(), "no such item");
|
||
assert_eq!(groups(&mut s).len(), 2, "a failed command makes no group");
|
||
// A deleted item leaves its group.
|
||
run(&mut s, "library.removeItem", json!({"kind": "charStyle", "item": st}));
|
||
assert_eq!(groups(&mut s)[1].1, refs(&[("graphic", &g)]));
|
||
// A deleted group's items stay, ungrouped.
|
||
assert_eq!(run(&mut s, "library.deleteGroup", json!({"group": "Palette"}))["ungrouped"], 1);
|
||
assert_eq!(run(&mut s, "library.get", json!({}))["graphics"].as_array().unwrap().len(), 1);
|
||
assert_eq!(groups(&mut s), [("Logos".into(), vec![])]);
|
||
assert_eq!(run(&mut s, "library.list", json!({}))["libraries"][0]["groups"], 1);
|
||
}
|
||
|
||
/// A library goes out to a file with its art and groups and comes back as a new library, numbered
|
||
/// when its name is taken; the graphic it brings places in a document (#926).
|
||
#[test]
|
||
fn a_library_is_exported_and_imported() {
|
||
let (mut s, g, c, _) = brand();
|
||
run(&mut s, "library.createGroup", json!({"name": "Logos", "items": [{"kind": "graphic", "item": g}, {"kind": "fillColor", "item": c}]}));
|
||
let out = run(&mut s, "library.export", json!({}));
|
||
assert_eq!(out["name"], "Brand");
|
||
let data = out["data"].as_str().unwrap().to_string();
|
||
let r = run(&mut s, "library.import", json!({"data": data}));
|
||
let counts = ["graphics", "colors", "charStyles", "paraStyles", "groups"].map(|k| r[k].as_u64().unwrap());
|
||
assert_eq!((r["name"].as_str(), counts), (Some("Brand (2)"), [1, 1, 1, 0, 1]));
|
||
let id = r["id"].as_str().unwrap().to_string();
|
||
assert_eq!(run(&mut s, "library.list", json!({}))["current"], id, "the imported library is the current one");
|
||
let a = run(&mut s, "library.get", json!({"library": "Brand"}));
|
||
let b = run(&mut s, "library.get", json!({"library": id}));
|
||
for k in ["graphics", "colors", "charStyles", "paraStyles", "groups"] {
|
||
assert_eq!(a[k], b[k], "{k}");
|
||
}
|
||
// Again: the next number; given a name, that name.
|
||
let b64 = vectorcraft_format::base64_encode(data.as_bytes());
|
||
assert_eq!(run(&mut s, "library.import", json!({"dataBase64": b64}))["name"], "Brand (3)");
|
||
assert_eq!(run(&mut s, "library.import", json!({"data": data, "name": "Client"}))["name"], "Client");
|
||
// Its graphic places in another document.
|
||
run(&mut s, "file.new", json!({"width": 100, "height": 100}));
|
||
let placed = run(&mut s, "library.use", json!({"library": id, "kind": "graphic", "item": "Logo", "center": [50, 50]}));
|
||
assert_eq!(placed["ids"].as_array().unwrap().len(), 1);
|
||
// Not a library file.
|
||
let swatches = json!({"format": "vcswatches", "name": "S"}).to_string();
|
||
for bad in [json!({"data": "{not json"}), json!({"data": "[1, 2]"}), json!({"data": swatches}), json!({"dataBase64": "%%"}), json!({})] {
|
||
assert!(s.execute("library.import", &bad).is_err(), "{bad}");
|
||
}
|
||
}
|
||
|
||
/// A file written before groups loads; one with repeated names, groups naming items that aren't
|
||
/// there or kinds this version doesn't know, and a thumbnail claiming a huge image is made safe.
|
||
#[test]
|
||
fn imported_libraries_are_made_consistent() {
|
||
let mut s = session();
|
||
let old = json!({"name": "Old", "colors": [{"name": "Red", "color": {"model": "rgb", "r": 1, "g": 0, "b": 0}}]});
|
||
let r = run(&mut s, "library.import", json!({"data": old.to_string()}));
|
||
assert_eq!((r["colors"].as_u64(), r["groups"].as_u64()), (Some(1), Some(0)));
|
||
// A PNG header claiming 100000 × 100000 pixels.
|
||
let mut png = b"\x89PNG\r\n\x1a\n\0\0\0\x0dIHDR".to_vec();
|
||
png.extend(100_000u32.to_be_bytes());
|
||
png.extend(100_000u32.to_be_bytes());
|
||
png.extend([8, 6, 0, 0, 0]);
|
||
let messy = json!({
|
||
"name": " ",
|
||
"colors": [{"name": "Red", "color": {"model": "rgb", "r": 1, "g": 0, "b": 0}}, {"name": "Red", "color": {"model": "rgb", "r": 0, "g": 1, "b": 0}}, {"name": "", "color": {"model": "rgb", "r": 0, "g": 0, "b": 1}}],
|
||
"graphics": [
|
||
{"id": "g", "name": "A", "width": 1, "height": 1, "data": "", "thumbnail": vectorcraft_format::base64_encode(&png)},
|
||
{"id": "g", "name": "B", "width": -5, "height": 1, "data": ""}
|
||
],
|
||
"groups": [
|
||
{"name": "One", "items": [{"kind": "color", "item": "Red 2"}, {"kind": "graphic", "item": "nope"}, {"kind": "brush", "item": "Red"}]},
|
||
{"name": "one", "items": [{"kind": "color", "item": "Red 2"}, {"kind": "graphic", "item": "g-2"}]}
|
||
]
|
||
});
|
||
let r = run(&mut s, "library.import", json!({"data": messy.to_string()}));
|
||
assert_eq!(r["name"], "Library", "a blank name");
|
||
let got = run(&mut s, "library.get", json!({}));
|
||
let names: Vec<&str> = got["colors"].as_array().unwrap().iter().map(|c| c["name"].as_str().unwrap()).collect();
|
||
assert_eq!(names, ["Red", "Red 2", "Color"]);
|
||
let graphics = got["graphics"].as_array().unwrap();
|
||
assert_eq!((graphics[0]["id"].as_str(), graphics[1]["id"].as_str()), (Some("g"), Some("g-2")));
|
||
assert_eq!((graphics[0]["thumbnail"].as_str(), graphics[1]["width"].as_f64()), (Some(""), Some(0.0)));
|
||
assert_eq!(
|
||
got["groups"],
|
||
json!([{"name": "One", "items": [{"kind": "color", "item": "Red 2"}]}, {"name": "one 2", "items": [{"kind": "graphic", "item": "g-2"}]}])
|
||
);
|
||
}
|
||
|
||
#[cfg(not(target_arch = "wasm32"))]
|
||
#[test]
|
||
fn a_library_is_exported_to_a_file_and_imported_from_it() {
|
||
let dir = vectorcraft_testkit::temp_dir("library-export");
|
||
let path = dir.join("Brand.vclibrary").to_string_lossy().to_string();
|
||
let (mut s, g, ..) = brand();
|
||
run(&mut s, "library.createGroup", json!({"name": "Logos", "items": [{"kind": "graphic", "item": g}]}));
|
||
assert_eq!(run(&mut s, "library.export", json!({"library": "Brand", "path": path}))["path"], path.as_str());
|
||
let mut t = session();
|
||
let r = run(&mut t, "library.import", json!({"path": path}));
|
||
assert_eq!((r["name"].as_str(), r["graphics"].as_u64(), r["groups"].as_u64()), (Some("Brand"), Some(1), Some(1)));
|
||
assert!(t.execute("library.import", &json!({"path": dir.join("missing.vclibrary").to_string_lossy()})).is_err());
|
||
// Groups are kept in the library folder's files too.
|
||
let folder = dir.join("folder").to_string_lossy().to_string();
|
||
let mut u = session();
|
||
u.libraries.set_dir(Some(folder.clone()));
|
||
run(&mut u, "library.import", json!({"path": path}));
|
||
let mut v = session();
|
||
v.libraries.set_dir(Some(folder));
|
||
assert_eq!(groups(&mut v), [("Logos".into(), refs(&[("graphic", &g)]))]);
|
||
}
|