use justerm_core::{Engine, TermDamage};
#[test]
fn single_cell_change_damages_its_line_span() {
let mut term = Engine::new(10, 3);
term.reset_damage(); term.feed(b"x");
match term.damage() {
TermDamage::Partial(lines) => {
assert_eq!(lines.len(), 1);
assert_eq!((lines[0].line, lines[0].left, lines[0].right), (0, 0, 0));
}
other => panic!("expected partial damage, got {other:?}"),
}
}
#[test]
fn erase_damages_the_cleared_span() {
let mut term = Engine::new(10, 1);
term.feed(b"abcde");
term.reset_damage(); term.feed(b"\x1b[1;3H\x1b[K");
match term.damage() {
TermDamage::Partial(lines) => {
assert_eq!(lines.len(), 1);
assert_eq!((lines[0].line, lines[0].left, lines[0].right), (0, 2, 9));
}
other => panic!("{other:?}"),
}
}
#[test]
fn cursor_move_puts_old_and_new_cells_in_frame() {
let mut term = Engine::new(80, 24);
term.feed(b"abc"); term.reset_damage(); term.feed(b"\x1b[10;20H");
let covers = |spans: &[justerm_core::Span], line: u16, col: u16| {
spans
.iter()
.any(|s| s.line == line && s.left <= col && s.right >= col)
};
let f = term.frame();
assert!(
covers(&f.spans, 0, 3),
"old cursor cell (0,3) must be in the frame, got {:?}",
f.spans
);
assert!(
covers(&f.spans, 9, 19),
"new cursor cell (9,19) must be in the frame, got {:?}",
f.spans
);
assert!(
matches!(term.damage(), TermDamage::Partial(ref l) if l.is_empty()),
"damage() is content-only and must stay empty on a pure cursor move"
);
}
#[test]
fn scroll_emits_first_class_op_not_full_damage() {
let mut term = Engine::new(4, 2);
term.feed(b"a\r\nb"); term.reset_damage();
term.feed(b"\r\nc");
let op = term.scroll_delta().expect("expected a scroll op");
assert_eq!((op.top, op.bottom, op.count), (0, 1, 1)); assert!(!matches!(term.damage(), TermDamage::Full)); }
#[test]
fn alt_screen_switch_is_full_damage() {
let mut term = Engine::new(4, 2);
term.feed(b"ab");
term.reset_damage();
term.feed(b"\x1b[?1049h");
assert!(matches!(term.damage(), TermDamage::Full));
}
#[test]
fn resize_clears_stale_scroll_op() {
let mut term = Engine::new(4, 5);
term.feed(b"a\r\nb\r\nc\r\nd\r\ne\r\nf"); assert!(term.scroll_delta().is_some());
term.resize(4, 3);
assert!(term.scroll_delta().is_none());
}
#[test]
fn reverse_index_emits_down_scroll_op() {
let mut term = Engine::new(4, 2);
term.feed(b"\x1b[1;1H"); term.reset_damage();
term.feed(b"\x1bM");
let op = term.scroll_delta().expect("expected a scroll op");
assert_eq!((op.top, op.bottom, op.count), (0, 1, -1));
}
#[test]
fn write_then_scroll_realigns_damage_with_content() {
let mut term = Engine::new(4, 2);
term.feed(b"\x1b[2;1H"); term.reset_damage();
term.feed(b"Z\n");
let lines = match term.damage() {
TermDamage::Partial(l) => l,
other => panic!("{other:?}"),
};
assert!(
lines.iter().any(|d| d.line == 0),
"row 0 not damaged: {lines:?}"
);
assert!(
lines.iter().any(|d| d.line == 1),
"row 1 not damaged: {lines:?}"
);
}
#[test]
fn repeated_scroll_accumulates_count() {
let mut term = Engine::new(4, 3);
term.feed(b"a\r\nb\r\nc"); term.reset_damage();
term.feed(b"\r\nd\r\ne");
let op = term.scroll_delta().expect("scroll op");
assert_eq!((op.top, op.bottom, op.count), (0, 2, 2));
}
#[test]
fn reported_scroll_count_is_capped_at_the_region_height() {
let mut term = Engine::new(4, 3);
term.feed(b"a\r\nb\r\nc"); term.reset_damage();
term.feed(&vec![b'\n'; 40_000]);
let op = term.scroll_delta().expect("scroll op");
assert_eq!((op.top, op.bottom), (0, 2), "still one full-screen region");
assert_eq!(op.count, 3, "capped at the 3-row region's height");
}
#[test]
fn the_cap_applies_to_a_down_scroll_too() {
let mut term = Engine::new(4, 3);
term.feed(b"a\r\nb\r\nc");
term.reset_damage();
let mut down = b"\x1b[H".to_vec(); down.extend(std::iter::repeat_n(*b"\x1bM", 40_000).flatten());
term.feed(&down);
let op = term.scroll_delta().expect("scroll op");
assert_eq!(op.count, -3, "capped at -height, not saturated to +height");
}
#[test]
fn a_scroll_that_returns_to_its_start_reports_no_net_shift() {
let mut term = Engine::new(4, 3);
term.feed(b"a\r\nb\r\nc");
term.reset_damage();
term.feed(&vec![b'\n'; 40_000]); let mut down = b"\x1b[H".to_vec();
down.extend(std::iter::repeat_n(*b"\x1bM", 40_000).flatten()); term.feed(&down);
let op = term.scroll_delta().expect("scroll op");
assert_eq!(op.count, 0, "the net is zero and small enough to say so");
}
#[test]
fn scrolls_of_different_regions_degrade_to_full() {
let mut term = Engine::new(4, 4);
term.feed(b"\x1b[4;1H"); term.reset_damage();
term.feed(b"\n");
term.feed(b"\x1b[2;3r"); term.feed(b"\x1b[3;1H\n");
assert!(matches!(term.damage(), TermDamage::Full));
}