use pdfrum_form::edit::{Place, Selection, UndoItem, UndoStack};
fn typed(ch: char, before: Selection) -> UndoItem {
UndoItem::InsertWord {
old: Place::start(),
new: Place::start(),
ch,
before,
}
}
fn replace_group(removed: &str, inserted: &str, before: Selection) -> Vec<UndoItem> {
let mut items = vec![UndoItem::GroupBoundary];
if !removed.is_empty() {
items.push(UndoItem::Clear {
range: pdfrum_form::edit::Range::empty_at(Place::start()),
text: removed.to_string(),
before,
});
}
if !inserted.is_empty() {
items.push(UndoItem::InsertText {
old: Place::start(),
new: Place::start(),
text: inserted.to_string(),
before,
});
}
items.push(UndoItem::GroupBoundary);
items
}
fn push_all(stack: &mut UndoStack, items: Vec<UndoItem>) {
for item in items {
stack.push(item);
}
}
#[test]
fn five_stacked_items_undo_and_redo_one_at_a_time() {
let mut stack = UndoStack::default();
assert!(!stack.can_undo());
assert!(!stack.can_redo());
for ch in "ABCDE".chars() {
stack.push(typed(ch, Selection::empty()));
}
assert_eq!(stack.len(), 5, "one item per character, never coalesced");
assert!(stack.can_undo());
assert!(!stack.can_redo());
assert_eq!(stack.undo().len(), 1);
assert!(stack.can_undo());
assert!(stack.can_redo());
assert_eq!(stack.undo().len(), 1);
assert!(stack.can_undo());
assert!(stack.can_redo());
assert_eq!(stack.redo().len(), 1);
assert!(stack.can_undo());
assert!(stack.can_redo());
assert_eq!(stack.redo().len(), 1);
assert!(stack.can_undo(), "three characters remain below");
assert!(!stack.can_redo(), "and the top has been reached");
}
#[test]
fn a_focus_change_to_another_field_empties_the_stack() {
let mut stack = UndoStack::default();
for ch in "ABC".chars() {
stack.push(typed(ch, Selection::empty()));
}
assert!(stack.can_undo());
stack.clear();
assert!(!stack.can_undo());
assert!(!stack.can_redo());
for ch in "ABC".chars() {
stack.push(typed(ch, Selection::empty()));
}
for _ in 0..3 {
stack.undo();
}
assert!(!stack.can_undo(), "the bottom of the stack is observable");
}
#[test]
fn a_bracketed_group_is_one_undo_step_however_many_members() {
let mut stack = UndoStack::default();
push_all(&mut stack, replace_group("", "UVW", Selection::empty()));
assert!(stack.can_undo());
let undone = stack.undo();
assert!(
undone.len() >= 2,
"the group is walked as a unit, boundaries included"
);
assert!(!stack.can_undo(), "…and that was the only step");
}
#[test]
fn a_group_above_loose_items_undoes_without_disturbing_them() {
let mut stack = UndoStack::default();
for ch in "ABC".chars() {
stack.push(typed(ch, Selection::empty()));
}
assert_eq!(stack.len(), 3);
push_all(&mut stack, replace_group("ABC", "", Selection::empty()));
stack.undo();
assert!(
stack.can_undo(),
"the three typed characters are still below the cut"
);
let mut steps = 0;
while stack.can_undo() {
stack.undo();
steps += 1;
}
assert_eq!(steps, 3);
}
#[test]
fn two_loose_items_and_a_group_are_three_walk_steps() {
let mut stack = UndoStack::default();
stack.push(typed('A', Selection::empty()));
stack.push(typed('B', Selection::empty()));
push_all(&mut stack, replace_group("A", "XYZ", Selection::empty()));
let mut down = 0;
while stack.can_undo() {
stack.undo();
down += 1;
}
assert_eq!(down, 3, "three undo steps, not five items' worth");
let mut up = 0;
while stack.can_redo() {
stack.redo();
up += 1;
}
assert_eq!(up, 3, "and the walk back up matches");
}
#[test]
fn a_fresh_edit_after_an_undo_drops_the_redo_branch() {
let mut stack = UndoStack::default();
push_all(&mut stack, replace_group("", "XYZ", Selection::empty()));
stack.undo();
assert!(stack.can_redo());
push_all(&mut stack, replace_group("", "UVW", Selection::empty()));
assert!(stack.can_undo());
assert!(!stack.can_redo(), "the redo branch is gone");
}
#[test]
fn an_item_carries_the_selection_from_before_the_edit() {
let before = Selection::new(Place::new(0, 0, Some(0)), Place::new(0, 0, Some(3)));
let mut stack = UndoStack::default();
stack.push(typed('X', before));
let undone = stack.undo();
let restored = undone.first().and_then(UndoItem::before);
assert_eq!(
restored,
Some(before),
"undo has the pre-edit selection to restore"
);
assert_eq!(UndoItem::GroupBoundary.before(), None);
}
#[test]
fn a_replace_group_fits_at_the_smallest_capacity() {
let mut stack = UndoStack::with_max(4);
assert_eq!(stack.max(), 4, "four is the floor, being the group's size");
for round in 0..6 {
push_all(
&mut stack,
replace_group("A", &format!("v{round}"), Selection::empty()),
);
assert_eq!(
stack.len(),
4,
"the worst-case group exactly fills the floor"
);
assert!(stack.len() <= 4, "the capacity holds");
let boundaries = stack.items().filter(|i| i.is_boundary()).count();
assert_eq!(boundaries % 2, 0, "no group was left half-evicted");
}
let mut steps = 0;
while stack.can_undo() {
stack.undo();
steps += 1;
assert!(steps < 10, "the walk must terminate");
}
}
#[test]
fn one_pushed_item_is_one_step() {
let mut stack = UndoStack::default();
stack.push(typed('A', Selection::empty()));
assert_eq!(stack.len(), 1);
assert_eq!(stack.undo().len(), 1);
assert!(!stack.can_undo(), "one undo reached the bottom");
}