bc0b258e04
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>
112 lines
4.3 KiB
Rust
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(¬es), ["first", "second", "third"]);
|
|
// Undo pops additions one at a time, then runs dry.
|
|
assert!(history.undo(&mut notes));
|
|
assert_eq!(labels(¬es), ["first", "second"]);
|
|
assert!(history.undo(&mut notes));
|
|
assert!(history.undo(&mut notes));
|
|
assert_eq!(labels(¬es), Vec::<String>::new());
|
|
assert!(!history.undo(&mut notes));
|
|
assert_eq!(labels(¬es), 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(¬es), ["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(¬es), ["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(¬es), ["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(¬es), ["a", "<shape>"]);
|
|
assert!(history.undo(&mut notes));
|
|
assert_eq!(labels(¬es), ["a", "b", "<shape>"]);
|
|
assert!(history.redo(&mut notes));
|
|
assert_eq!(labels(¬es), ["a", "<shape>"]);
|
|
// Clear everything and undo it.
|
|
let cleared = notes.items.clone();
|
|
notes.items.clear();
|
|
history.push(Edit::Clear(cleared));
|
|
assert_eq!(labels(¬es), Vec::<String>::new());
|
|
assert!(history.undo(&mut notes));
|
|
assert_eq!(labels(¬es), ["a", "<shape>"]);
|
|
assert!(history.redo(&mut notes));
|
|
assert_eq!(labels(¬es), 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(¬es), 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(¬es), ["moved", "b"]);
|
|
assert!(history.undo(&mut notes));
|
|
assert_eq!(labels(¬es), ["a", "b"]);
|
|
assert!(history.redo(&mut notes));
|
|
assert_eq!(labels(¬es), ["moved", "b"]);
|
|
}
|