use serde_json::json; use super::*; fn session() -> Session { let mut s = Session::new(); s.execute("file.new", &json!({"width": 400, "height": 300})).unwrap(); s } #[test] fn batch_is_one_undo_step() { let mut s = session(); let r = s .execute( "command.batch", &json!({"label": "Logo", "commands": [ {"command": "paint.setFill", "params": {"color": "#ff0000"}}, {"command": "shape.rectangle", "params": {"x": 0, "y": 0, "width": 50, "height": 50}}, {"command": "shape.ellipse", "params": {"x": 60, "y": 0, "width": 50, "height": 50}}, ]}), ) .unwrap(); assert_eq!(r["results"].as_array().unwrap().len(), 3); let st = s.doc().unwrap(); assert_eq!(st.history.undo.len(), 1); assert_eq!(st.history.undo[0].label, "Logo"); assert_eq!(st.doc.layers[0].children().unwrap().len(), 2); assert_eq!(s.journal.iter().filter(|(c, _)| c == "command.batch").count(), 1); s.execute("edit.undo", &json!({})).unwrap(); assert_eq!(s.doc().unwrap().doc.layers[0].children().unwrap().len(), 0); } #[test] fn batch_rolls_back_on_error() { let mut s = session(); let r = s.execute( "command.batch", &json!({"commands": [ {"command": "shape.rectangle", "params": {"x": 0, "y": 0, "width": 50, "height": 50}}, {"command": "no.such.command", "params": {}}, ]}), ); assert!(r.is_err()); assert_eq!(s.doc().unwrap().doc.layers[0].children().unwrap().len(), 0); assert!(s.doc().unwrap().history.undo.is_empty()); assert!(!s.in_interaction()); } #[test] fn serialize_and_reopen() { let mut s = session(); s.execute("shape.ellipse", &json!({"x": 10, "y": 10, "width": 80, "height": 40})).unwrap(); let b64 = s.execute("document.serialize", &json!({"format": "vectorcraft"})).unwrap()["dataBase64"].as_str().unwrap().to_string(); let svg = s.execute("document.serialize", &json!({"format": "svg"})).unwrap()["text"].as_str().unwrap().to_string(); assert!(svg.contains(" 100); } #[test] fn save_and_open_path() { let mut s = session(); s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 80, "height": 40})).unwrap(); let dir = std::env::temp_dir().join(format!("dc-test-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); let path = dir.join("t.vectorcraft"); s.execute("document.save", &json!({"path": path.to_string_lossy()})).unwrap(); assert!(!s.doc().unwrap().is_dirty()); s.execute("document.open", &json!({"path": path.to_string_lossy()})).unwrap(); assert_eq!(s.doc().unwrap().path.as_deref(), Some(path.to_string_lossy().as_ref())); let svg = dir.join("t.svg"); s.execute("document.export", &json!({"path": svg.to_string_lossy()})).unwrap_or_default(); s.execute("document.export", &json!({"format": "svg", "path": svg.to_string_lossy()})).unwrap(); assert!(std::fs::read_to_string(&svg).unwrap().contains("= 2); } #[test] fn draw_behind_and_inside() { let mut s = session(); let a = s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 100, "height": 100})).unwrap()["id"].as_u64().unwrap(); s.execute("view.drawMode", &json!({"mode": "behind"})).unwrap(); let b = s.execute("shape.ellipse", &json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap()["id"].as_u64().unwrap(); let order: Vec = s.doc().unwrap().doc.layers[0].children().unwrap().iter().map(|n| n.id.0).collect(); assert_eq!(order, vec![b, a]); s.execute("select.set", &json!({"ids": [a]})).unwrap(); s.execute("view.drawMode", &json!({"mode": "inside"})).unwrap(); let c = s.execute("shape.ellipse", &json!({"x": 50, "y": 50, "width": 100, "height": 100})).unwrap()["id"].as_u64().unwrap(); let d = &s.doc().unwrap().doc; let g = d.parent_of(NodeId(c)).unwrap(); assert_eq!(d.node(g).unwrap().kind_label(), "Clip Group"); assert_eq!(d.parent_of(NodeId(a)), Some(g)); // A second shape goes into the same clip group. s.execute("shape.ellipse", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap(); assert_eq!(s.doc().unwrap().doc.node(g).unwrap().children().unwrap().len(), 4); s.execute("view.drawMode", &json!({"mode": "normal"})).unwrap(); assert!(s.draw_inside.is_none()); } #[test] fn export_for_screens_writes_every_combination() { let mut s = Session::new(); s.execute("file.new", &json!({"width": 100, "height": 80, "artboards": 2})).unwrap(); s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 50, "height": 40})).unwrap(); let dir = std::env::temp_dir().join(format!("dc-efs-{}", std::process::id())); let r = s .execute("document.exportForScreens", &json!({"folder": dir.to_string_lossy(), "formats": [{"format": "png", "scale": 1}, {"format": "png", "scale": 2}, {"format": "jpg"}, {"format": "svg"}, {"format": "webp"}]})) .unwrap(); let files = r["files"].as_array().unwrap(); assert_eq!(files.len(), 10); for f in files { assert!(std::fs::metadata(f.as_str().unwrap()).unwrap().len() > 50, "{f}"); } assert!(files.iter().any(|f| f.as_str().unwrap().ends_with("Artboard-2@2x.png"))); let _ = std::fs::remove_dir_all(dir); } #[test] fn recolor_maps_fills_strokes_gradients() { let mut s = session(); s.execute("paint.setFill", &json!({"color": "#ff0000"})).unwrap(); let a = s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap()["id"].as_u64().unwrap(); s.execute( "paint.setFill", &json!({"gradient": {"kind": "linear", "stops": [{"offset": 0, "color": "#ff0000"}, {"offset": 1, "color": "#0000ff"}]}}), ) .unwrap(); s.execute("select.all", &json!({})).unwrap(); let c = s.execute("recolor.colors", &json!({})).unwrap(); let hexes: Vec<&str> = c["colors"].as_array().unwrap().iter().map(|v| v["hex"].as_str().unwrap()).collect(); assert!(hexes.contains(&"#ff0000") && hexes.contains(&"#0000ff") && hexes.contains(&"#000000")); s.execute("recolor.apply", &json!({"map": {"#FF0000": "#00ff00"}})).unwrap(); let n = s.doc().unwrap().doc.node(NodeId(a)).unwrap().clone(); match n.appearance.fill_paint() { vectorcraft_color::Paint::Gradient(g) => assert_eq!(g.gradient.stops[0].color.to_hex(), "#00ff00"), other => panic!("{other:?}"), } } #[test] fn export_selection_crops_to_the_selection() { let mut s = session(); s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 50, "height": 50})).unwrap(); s.execute("shape.rectangle", &json!({"x": 200, "y": 100, "width": 80, "height": 40})).unwrap(); let v = s.execute("document.exportSelection", &json!({"format": "png", "scale": 2})).unwrap(); let b = v["bounds"].as_array().unwrap(); // Only the selected (last) rectangle, with its stroke. let w = b[2].as_f64().unwrap(); assert!((81.0..90.0).contains(&w) && b[0].as_f64().unwrap() > 190.0, "{v}"); let png = vectorcraft_format::base64_decode(v["dataBase64"].as_str().unwrap()).unwrap(); assert_eq!(&png[..4], b"\x89PNG"); assert_eq!(u32::from_be_bytes([png[16], png[17], png[18], png[19]]), (w * 2.0).round() as u32); let svg = s.execute("document.exportSelection", &json!({"format": "svg"})).unwrap(); let text = String::from_utf8(vectorcraft_format::base64_decode(svg["dataBase64"].as_str().unwrap()).unwrap()).unwrap(); assert_eq!(text.matches(" 0), "{name}"); } let _ = std::fs::remove_dir_all(dir); }