//! Sidecar JSON tests: round trips, unknown top-level keys, every hostile case, //! and the sidecar naming convention. use notescraft_annotate::{Annotations, ColorRef, Item, Point, Rgb, StrokeTool, image_of_sidecar, sidecar_path}; /// One of each item kind, with both colour forms and both tools filled. fn sample() -> Annotations { let mut notes = Annotations::new(1000, 800); notes.items.extend([ Item::Stroke { tool: StrokeTool::Pen, color: ColorRef::Theme(3), width: 2.5, points: vec![Point::new(1.0, 2.0), Point::new(3.5, 4.0), Point::new(990.0, 795.0)], }, Item::Stroke { tool: StrokeTool::Highlighter, color: ColorRef::Custom(Rgb(0xF0, 0x0F, 0x80)), width: 10.0, points: vec![Point::new(0.0, 0.0), Point::new(40.0, 40.0)], }, Item::Line { color: ColorRef::Theme(5), width: 1.0, from: Point::new(10.0, 20.0), to: Point::new(30.0, 40.0), arrow: true }, Item::Rect { color: ColorRef::Custom(Rgb(12, 34, 56)), width: 2.0, min: Point::new(20.0, 30.0), max: Point::new(50.0, 60.0), filled: true }, Item::Ellipse { color: ColorRef::Theme(0), width: 200.0, min: Point::new(-20.0, 30.0), max: Point::new(1050.0, 60.0), filled: false }, Item::Text { color: ColorRef::Theme(7), size: 3.5, at: Point::new(100.0, 100.0), text: "hello\nsecond line".to_string() }, ]); notes.extra.insert("x-craft".to_string(), serde_json::json!({"a": [1, 2]})); notes } #[test] fn json_round_trips_every_item_kind() { let notes = sample(); let text = notes.to_json().unwrap(); let parsed = Annotations::from_json(&text, 1000, 800).unwrap(); assert_eq!(parsed.items.len(), 6); assert_eq!(parsed.items, notes.items); assert_eq!(parsed.image.width, 1000); assert_eq!(parsed.image.height, 800); // Saving the reparsed document again is byte-stable. assert_eq!(parsed.to_json().unwrap(), text); } #[test] fn unknown_top_level_keys_survive() { let mut notes = sample(); notes.extra.insert("future-field".to_string(), serde_json::json!("keep me")); let text = notes.to_json().unwrap(); let parsed = Annotations::from_json(&text, 1000, 800).unwrap(); assert_eq!(parsed.extra.get("future-field"), Some(&serde_json::json!("keep me"))); // And a document written by hand with an unknown key keeps it too. let hand = "{\"version\":1,\"image\":{\"hash\":null,\"width\":1000,\"height\":800},\ \"items\":[],\"unknown\":{\"deep\":[true]}}"; let parsed = Annotations::from_json(hand, 1000, 800).unwrap(); assert_eq!(parsed.extra.get("unknown"), Some(&serde_json::json!({"deep": [true]}))); let again = Annotations::from_json(&parsed.to_json().unwrap(), 1000, 800).unwrap(); assert_eq!(again.extra, parsed.extra); } #[test] fn sidecar_path_and_reverse() { assert_eq!(sidecar_path("a/photo.png"), "a/photo.png.annotations.json"); assert_eq!(image_of_sidecar("a/photo.png.annotations.json"), Some("a/photo.png".to_string())); assert_eq!(image_of_sidecar("a/photo.png"), None); for path in ["b/c.jpg", "top.webp"] { assert_eq!(image_of_sidecar(&sidecar_path(path)), Some(path.to_string())); } } #[test] fn an_old_sidecar_with_lines_and_text_still_loads() { let old = "{\"version\":1,\"image\":{\"hash\":null,\"width\":100,\"height\":80},\"items\":[\ {\"type\":\"line\",\"color\":{\"theme\":0},\"width\":2.0,\"from\":[1,2],\"to\":[30,40],\"arrow\":false},\ {\"type\":\"line\",\"color\":{\"theme\":0},\"width\":2.0,\"from\":[1,2],\"to\":[30,40],\"arrow\":true},\ {\"type\":\"text\",\"color\":{\"theme\":1},\"size\":20.0,\"at\":[5,6],\"text\":\"hi\"}]}"; let parsed = Annotations::from_json(old, 100, 80).unwrap(); assert_eq!(parsed.items.len(), 3); assert!(matches!(parsed.items[1], Item::Line { arrow: true, .. })); let hostile = old.replace("\"arrow\":true", "\"arrow\":true,\"future\":1"); assert!(Annotations::from_json(&hostile, 100, 80).is_err(), "unknown item fields stay rejected"); }