mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 02:07:09 -06:00
Search: a search field at the top of the Libraries panel filters the
current library's colours, character and paragraph styles and graphics
by name, ignoring case; groups without a match are hidden, groups with
one open, and a search nothing matches says so ("No results").
Groups: items can be put in user-named groups, kept in the library file
(`groups: [{name, items: [{kind, item}]}]`, left out when there are
none, so files written before groups load as they are). New commands:
library.createGroup {library?, name?, items?}, library.renameGroup
{library?, group, name}, library.deleteGroup {library?, group} (its
items stay, ungrouped) and library.moveItem {library?, kind, item,
group?}. An item is in one group at most; deleting an item takes it out
of its group. The panel shows each group as a collapsible section with
its item count after the ungrouped items (rename and delete on its
context menu), each item's context menu has Move to Group (No Group,
the groups, New Group...), and a New Group button sits in the footer.
library.get lists the groups, library.list counts them. The generic
command dialog takes fixed params in `__params` for this.
Export / import: library.export {library?, path?} writes a library
(colours, styles, graphics with art and thumbnails, groups) as a
.vclibrary file, the JSON the library folder keeps (without a path:
{data}); library.import {path | data | dataBase64, name?} adds one as a
new library, made current, numbered on a name clash ("Brand (2)").
The panel menu has Import Library... and Export Library..., through the
app's file dialogs (the web downloads, and picks through File > Open,
which now lists Libraries files). Imported and folder files are made
safe: at most 10,000 items of a kind and 1,000 groups, names clipped and
made unique, sizes finite, thumbnails kept only when their PNG header
says at most 1024 px a side, group entries naming missing items or
unknown kinds dropped; a file over 512 MB or that isn't a library is an
error. Paths go through the engine's file helpers (automation roots).
The importer is fuzzed (import_fuzz: garbage and mutated libraries).
Also fixed: the third graphic added to a library got the second's id
(`graphic-2` twice).
Docs: the commands' params docs, docs/mcp.md, ROADMAP.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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)]))]);
|
||
}
|