use super::*;
fn fresh_state(byte: u8) -> Cpu8080State {
let mut state = Cpu8080State::default();
state.memory.write(0, byte);
state
}
#[test]
fn text_pushes_coalesce_on_same_field() {
let mut stack = UndoStack::default();
stack.push_text("addr", "00".to_owned(), "0A".to_owned());
stack.push_text("addr", "0A".to_owned(), "0AB".to_owned());
assert_eq!(stack.undo.len(), 1, "consecutive same-field edits coalesce");
match &stack.undo[0] {
UndoEntry::Text {
field,
before,
after,
} => {
assert_eq!(*field, "addr");
assert_eq!(before, "00");
assert_eq!(after, "0AB");
}
other => panic!("unexpected entry kind: {other:?}"),
}
}
#[test]
fn text_pushes_split_across_fields() {
let mut stack = UndoStack::default();
stack.push_text("addr", "00".to_owned(), "01".to_owned());
stack.push_text("value", "00".to_owned(), "FF".to_owned());
assert_eq!(stack.undo.len(), 2);
}
#[test]
fn cpu_push_breaks_text_coalescing() {
let mut stack = UndoStack::default();
stack.push_text("addr", "00".to_owned(), "01".to_owned());
stack.push_cpu(fresh_state(0), fresh_state(0xAB));
stack.push_text("addr", "01".to_owned(), "02".to_owned());
assert_eq!(stack.undo.len(), 3, "cpu push must break coalescing");
}
#[test]
fn redo_clears_on_new_push() {
let mut stack = UndoStack::default();
stack.push_text("addr", "00".to_owned(), "01".to_owned());
stack.pop_undo();
assert_eq!(stack.redo.len(), 1);
stack.push_text("addr", "00".to_owned(), "0A".to_owned());
assert!(stack.redo.is_empty());
}
#[test]
fn no_op_pushes_are_dropped() {
let mut stack = UndoStack::default();
stack.push_text("addr", "00".to_owned(), "00".to_owned());
assert!(stack.undo.is_empty());
let state = fresh_state(0x42);
stack.push_cpu(state.clone(), state);
assert!(stack.undo.is_empty());
}
#[test]
fn pop_undo_then_redo_round_trips() {
let mut stack = UndoStack::default();
stack.push_text("addr", "00".to_owned(), "01".to_owned());
let undone = stack.pop_undo().expect("undo entry available");
match undone {
UndoEntry::Text { before, after, .. } => {
assert_eq!(before, "00");
assert_eq!(after, "01");
}
other => panic!("unexpected entry: {other:?}"),
}
let redone = stack.pop_redo().expect("redo entry available");
match redone {
UndoEntry::Text { before, after, .. } => {
assert_eq!(before, "00");
assert_eq!(after, "01");
}
other => panic!("unexpected entry: {other:?}"),
}
assert_eq!(stack.undo.len(), 1);
assert!(stack.redo.is_empty());
}
#[test]
fn depth_limit_drops_oldest() {
let mut stack = UndoStack::default();
for i in 0..(UNDO_DEPTH_LIMIT as u32 + 5) {
stack.push_text(
"addr",
format!("{i:04X}"),
format!("{:04X}", i.wrapping_add(1)),
);
stack.break_coalescing();
}
assert_eq!(stack.undo.len(), UNDO_DEPTH_LIMIT);
}
#[test]
fn break_coalescing_splits_same_field_runs() {
let mut stack = UndoStack::default();
stack.push_text("addr", "00".to_owned(), "01".to_owned());
stack.break_coalescing();
stack.push_text("addr", "01".to_owned(), "02".to_owned());
assert_eq!(stack.undo.len(), 2);
}
#[test]
fn clear_wipes_both_stacks_and_coalesce_marker() {
let mut stack = UndoStack::default();
stack.push_text("addr", "00".to_owned(), "01".to_owned());
stack.pop_undo();
assert_eq!(stack.redo.len(), 1);
stack.push_text("value", "00".to_owned(), "FF".to_owned());
stack.clear();
assert!(stack.undo.is_empty());
assert!(stack.redo.is_empty());
stack.push_text("addr", "00".to_owned(), "01".to_owned());
assert_eq!(stack.undo.len(), 1);
}
#[test]
fn pop_undo_resets_coalesce_marker() {
let mut stack = UndoStack::default();
stack.push_text("addr", "00".to_owned(), "01".to_owned());
stack.break_coalescing();
stack.push_text("addr", "01".to_owned(), "02".to_owned());
assert_eq!(stack.undo.len(), 2);
stack.pop_undo();
assert_eq!(stack.undo.len(), 1);
stack.push_text("addr", "01".to_owned(), "0A".to_owned());
assert_eq!(
stack.undo.len(),
2,
"post-undo typing must not coalesce onto a re-exposed entry"
);
}