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

112 lines
4.3 KiB
Rust

//! History tests: round trips through undo and redo, and the step cap.
use notescraft_annotate::{Annotations, ColorRef, Edit, History, Item, Point, StrokeTool};
/// A small text item with a distinct label.
fn text(label: &str) -> Item {
Item::Text { color: ColorRef::Theme(0), size: 10.0, at: Point::new(5.0, 5.0), text: label.to_string() }
}
/// Labels the items currently on the list, in order.
fn labels(notes: &Annotations) -> Vec<String> {
notes
.items
.iter()
.map(|item| match item {
Item::Text { text, .. } => text.clone(),
Item::Stroke { .. } | Item::Line { .. } | Item::Rect { .. } | Item::Ellipse { .. } => String::from("<shape>"),
})
.collect()
}
#[test]
fn history_undo_redo_round_trip_and_cap() {
let mut notes = Annotations::new(400, 300);
let mut history = History::new();
for label in ["first", "second", "third"] {
notes.items.push(text(label));
history.push(Edit::Add(text(label)));
}
assert_eq!(labels(&notes), ["first", "second", "third"]);
// Undo pops additions one at a time, then runs dry.
assert!(history.undo(&mut notes));
assert_eq!(labels(&notes), ["first", "second"]);
assert!(history.undo(&mut notes));
assert!(history.undo(&mut notes));
assert_eq!(labels(&notes), Vec::<String>::new());
assert!(!history.undo(&mut notes));
assert_eq!(labels(&notes), Vec::<String>::new());
// Redo re-applies them in the original order, then runs dry.
assert!(history.redo(&mut notes));
assert!(history.redo(&mut notes));
assert!(history.redo(&mut notes));
assert_eq!(labels(&notes), ["first", "second", "third"]);
assert!(!history.redo(&mut notes));
// An edit invalidates the redo stack.
assert!(history.undo(&mut notes));
notes.items.push(text("tail"));
history.push(Edit::Add(text("tail")));
assert!(!history.redo(&mut notes));
assert_eq!(labels(&notes), ["first", "second", "tail"]);
// The cap: 201 records leave exactly 200 steps undoable.
let mut notes = Annotations::new(400, 300);
let mut history = History::new();
for step in 0..201_u64 {
let label = format!("step {step}");
notes.items.push(text(&label));
history.push(Edit::Add(text(&label)));
}
let mut undone = 0usize;
while history.undo(&mut notes) {
undone += 1;
}
assert_eq!(undone, 200);
assert_eq!(labels(&notes), ["step 0"]);
}
#[test]
fn clear_is_undoable() {
let mut notes = Annotations::new(400, 300);
notes.items.push(text("a"));
notes.items.push(text("b"));
let mut history = History::new();
notes.items.push(Item::Stroke { tool: StrokeTool::Pen, color: ColorRef::Theme(1), width: 1.0, points: vec![Point::new(0.0, 0.0), Point::new(10.0, 10.0)] });
history.push(Edit::Add(notes.items.last().cloned().unwrap_or_else(|| text("never"))));
// Remove the middle item ('b') and undo the removal back into its slot.
let removed = notes.remove(1);
history.push(Edit::Remove(1, removed.clone().unwrap_or_else(|| text("never"))));
assert_eq!(labels(&notes), ["a", "<shape>"]);
assert!(history.undo(&mut notes));
assert_eq!(labels(&notes), ["a", "b", "<shape>"]);
assert!(history.redo(&mut notes));
assert_eq!(labels(&notes), ["a", "<shape>"]);
// Clear everything and undo it.
let cleared = notes.items.clone();
notes.items.clear();
history.push(Edit::Clear(cleared));
assert_eq!(labels(&notes), Vec::<String>::new());
assert!(history.undo(&mut notes));
assert_eq!(labels(&notes), ["a", "<shape>"]);
assert!(history.redo(&mut notes));
assert_eq!(labels(&notes), Vec::<String>::new());
// Undoing clear of an empty list is a no-op round trip.
notes.items.clear();
history.push(Edit::Clear(Vec::new()));
assert!(!history.undo(&mut notes));
assert_eq!(labels(&notes), Vec::<String>::new());
}
#[test]
fn replace_undoes_and_redoes_in_place() {
let mut notes = Annotations::new(400, 300);
notes.items.extend([text("a"), text("b")]);
let mut history = History::new();
notes.items[0] = text("moved");
history.push(Edit::Replace(0, text("a"), text("moved")));
assert_eq!(labels(&notes), ["moved", "b"]);
assert!(history.undo(&mut notes));
assert_eq!(labels(&notes), ["a", "b"]);
assert!(history.redo(&mut notes));
assert_eq!(labels(&notes), ["moved", "b"]);
}