Files
Marcos Paulo bc0b258e04 feat: engine and core crates: comment sidecars hidden and removed with the note, e2e comments test, extension tables, annotate text and arrow items
Sidecars (*.comments.json) stay out of the index and listings and follow rename and delete of their note; end-to-end comments test over a stub server (Ana comments and mentions Bob, Bob resolves, second sync changes nothing); viewer extension tables gain ico, ogv, 3gp, opus, aac; annotate crate: Edit::Replace for undoable text moves and edits, line items carry an arrow flag; syntax test fixed (TeX is a bundled grammar). Built by MiniMax M3.1 Flash and Claude Sonnet; verified with the full cargo xtask ci on the combined tree (4422 tests, clippy -D warnings, layers, tree, i18n).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 02:31:52 -03:00

82 lines
4.0 KiB
Rust

//! 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");
}