use super::*;
fn reattach_render(screen: &Screen) -> String {
let mut cache = RenderCache::new();
let output = screen.render(true, &mut cache);
String::from_utf8_lossy(&output).into_owned()
}
fn assert_cell(screen: &Screen, row: usize, col: usize, expected: char) {
let actual = screen.grid.visible_row(row)[col].c;
assert_eq!(actual, expected,
"cell ({}, {}) expected '{}', got '{}'", row, col, expected, actual);
}
fn collect_screen_lines(screen: &Screen) -> Vec<String> {
(0..screen.grid.visible_row_count()).map(|y| {
let s: String = screen.grid.visible_row(y).iter().map(|c| c.c).collect();
s.trim_end().to_string()
}).collect()
}
fn collect_full_history(screen: &Screen) -> Vec<String> {
let mut lines: Vec<String> = screen.get_history().iter()
.map(|b| String::from_utf8_lossy(b).trim_end().to_string())
.collect();
lines.extend(collect_screen_lines(screen).into_iter().filter(|s| !s.is_empty()));
lines
}
#[test]
fn resize_clears_wrap_pending() {
let mut screen = Screen::new(5, 3, 100);
screen.process(b"ABCDE"); assert!(screen.grid.wrap_pending);
screen.resize(10, 3);
assert!(!screen.grid.wrap_pending,
"wrap_pending should be cleared on resize");
}
#[test]
fn resize_horizontal_expand_preserves_text() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"ABCDEFGHIJ"); screen.resize(20, 3);
for (i, ch) in "ABCDEFGHIJ".chars().enumerate() {
assert_cell(&screen, 0, i, ch);
}
for c in 10..20 {
assert_cell(&screen, 0, c, ' ');
}
}
#[test]
fn resize_horizontal_shrink_preserves_visible_text() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"ABCDEFGHIJ");
screen.resize(5, 3);
for (i, ch) in "ABCDE".chars().enumerate() {
assert_cell(&screen, 0, i, ch);
}
}
#[test]
fn resize_vertical_expand_preserves_text() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"\x1b[1;1HRow1");
screen.process(b"\x1b[2;1HRow2");
screen.process(b"\x1b[3;1HRow3");
screen.resize(10, 6);
for (i, ch) in "Row1".chars().enumerate() { assert_cell(&screen, 0, i, ch); }
for (i, ch) in "Row2".chars().enumerate() { assert_cell(&screen, 1, i, ch); }
for (i, ch) in "Row3".chars().enumerate() { assert_cell(&screen, 2, i, ch); }
for r in 3..6 {
assert_cell(&screen, r, 0, ' ');
}
}
#[test]
fn resize_vertical_shrink_preserves_visible_text() {
let mut screen = Screen::new(10, 5, 100);
for i in 1..=5 {
screen.process(format!("\x1b[{};1HLine{}", i, i).as_bytes());
}
screen.resize(10, 3);
for (i, ch) in "Line1".chars().enumerate() { assert_cell(&screen, 0, i, ch); }
for (i, ch) in "Line2".chars().enumerate() { assert_cell(&screen, 1, i, ch); }
for (i, ch) in "Line3".chars().enumerate() { assert_cell(&screen, 2, i, ch); }
}
#[test]
fn resize_both_expand_preserves_text() {
let mut screen = Screen::new(5, 3, 100);
screen.process(b"\x1b[1;1Hab");
screen.process(b"\x1b[2;1Hcd");
screen.process(b"\x1b[3;1Hef");
screen.resize(10, 6);
assert_cell(&screen, 0, 0, 'a');
assert_cell(&screen, 0, 1, 'b');
assert_cell(&screen, 1, 0, 'c');
assert_cell(&screen, 1, 1, 'd');
assert_cell(&screen, 2, 0, 'e');
assert_cell(&screen, 2, 1, 'f');
assert_cell(&screen, 0, 5, ' ');
assert_cell(&screen, 3, 0, ' ');
}
#[test]
fn resize_both_shrink_preserves_visible_text() {
let mut screen = Screen::new(10, 5, 100);
screen.process(b"\x1b[1;1HABCDEFGHIJ");
screen.process(b"\x1b[2;1H0123456789");
screen.resize(5, 2);
for (i, ch) in "ABCDE".chars().enumerate() { assert_cell(&screen, 0, i, ch); }
for (i, ch) in "01234".chars().enumerate() { assert_cell(&screen, 1, i, ch); }
}
#[test]
fn resize_expand_cols_shrink_rows_preserves_overlap() {
let mut screen = Screen::new(5, 5, 100);
screen.process(b"\x1b[1;1Hone");
screen.process(b"\x1b[2;1Htwo");
screen.process(b"\x1b[3;1Htri");
screen.resize(10, 2);
for (i, ch) in "one".chars().enumerate() { assert_cell(&screen, 0, i, ch); }
for (i, ch) in "two".chars().enumerate() { assert_cell(&screen, 1, i, ch); }
assert_cell(&screen, 0, 5, ' '); }
#[test]
fn resize_shrink_cols_expand_rows_preserves_overlap() {
let mut screen = Screen::new(10, 2, 100);
screen.process(b"\x1b[1;1HABCDEFGHIJ");
screen.process(b"\x1b[2;1H0123456789");
screen.resize(4, 6);
for (i, ch) in "ABCD".chars().enumerate() { assert_cell(&screen, 0, i, ch); }
for (i, ch) in "0123".chars().enumerate() { assert_cell(&screen, 1, i, ch); }
assert_cell(&screen, 2, 0, ' '); }
#[test]
fn resize_reattach_render_preserves_content() {
let mut screen = Screen::new(20, 5, 100);
screen.process(b"\x1b[1;1HHello World");
screen.process(b"\x1b[3;1HResize Me");
screen.resize(30, 8); let rendered = reattach_render(&screen);
assert!(rendered.contains("Hello World"),
"reattach after expand should contain 'Hello World'");
assert!(rendered.contains("Resize Me"),
"reattach after expand should contain 'Resize Me'");
}
#[test]
fn resize_reattach_render_shrink_then_expand() {
let mut screen = Screen::new(20, 5, 100);
screen.process(b"\x1b[1;1HKeep This");
screen.process(b"\x1b[5;1HBottom");
screen.resize(10, 3); screen.resize(20, 5); let rendered = reattach_render(&screen);
assert!(rendered.contains("Keep This"),
"surviving content should render after shrink+expand");
assert!(!rendered.contains("Bottom"),
"truncated row content should not reappear");
}
#[test]
fn resize_preserves_styled_content() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"\x1b[1;31mSTYLED\x1b[0m");
screen.resize(20, 5); assert_cell(&screen, 0, 0, 'S');
assert!(screen.grid.visible_row(0)[0].style.bold, "bold should survive resize");
let rendered = reattach_render(&screen);
assert!(rendered.contains("STYLED"),
"styled text should survive resize and render");
}
#[test]
fn resize_same_dimensions_is_noop_for_content() {
let mut screen = Screen::new(10, 5, 100);
for i in 1..=5 {
screen.process(format!("\x1b[{};1HLine{}", i, i).as_bytes());
}
screen.resize(10, 5); for i in 1..=5 {
let tag = format!("Line{}", i);
for (j, ch) in tag.chars().enumerate() {
assert_cell(&screen, i - 1, j, ch);
}
}
}
#[test]
fn resize_after_scroll_preserves_scrollback() {
let mut screen = Screen::new(15, 3, 100);
for i in 1..=6 {
screen.process(format!("Line{}\r\n", i).as_bytes());
}
let scrollback_before = screen.get_history();
assert!(!scrollback_before.is_empty(), "scrollback should exist before resize");
screen.resize(20, 5);
let scrollback_after = screen.get_history();
assert_eq!(scrollback_after.len(), scrollback_before.len() - 2,
"scrollback should shrink by restored line count");
for (i, (before, after)) in scrollback_before.iter().zip(scrollback_after.iter()).enumerate() {
assert_eq!(before, after,
"remaining scrollback line {} should be identical", i);
}
}
#[test]
fn resize_after_scroll_preserves_visible_and_scrollback() {
let mut screen = Screen::new(15, 3, 100);
for i in 1..=8 {
screen.process(format!("Msg{}\r\n", i).as_bytes());
}
let history_before = screen.get_history();
screen.resize(15, 6);
let history_after = screen.get_history();
let restored = history_before.len() - history_after.len();
assert_eq!(restored, 3, "should restore 3 lines from scrollback");
for (i, (b, a)) in history_before.iter().zip(history_after.iter()).enumerate() {
assert_eq!(b, a, "remaining scrollback line {} changed after expand", i);
}
let rendered = reattach_render(&screen);
assert!(!rendered.is_empty(), "rendered content should not be empty after resize");
}
#[test]
fn resize_shrink_after_scroll_keeps_scrollback() {
let mut screen = Screen::new(20, 5, 100);
for i in 1..=10 {
screen.process(format!("Item{}\r\n", i).as_bytes());
}
let history_before = screen.get_history();
assert!(history_before.len() >= 5,
"expected at least 5 scrollback lines, got {}", history_before.len());
screen.resize(10, 3);
let history_after = screen.get_history();
assert_eq!(history_before.len(), history_after.len(),
"scrollback should survive shrink resize");
for (i, (b, a)) in history_before.iter().zip(history_after.iter()).enumerate() {
assert_eq!(b, a, "scrollback line {} corrupted after shrink", i);
}
}
#[test]
fn resize_scrollback_contains_correct_content() {
let mut screen = Screen::new(20, 3, 100);
for i in 1..=7 {
screen.process(format!("Tag{}\r\n", i).as_bytes());
}
let _ = screen.take_pending_scrollback();
let history = screen.get_history();
let first_line = String::from_utf8_lossy(&history[0]);
assert!(first_line.contains("Tag1"),
"first scrollback line should contain Tag1, got: {}", first_line);
screen.resize(30, 6);
let history_after = screen.get_history();
let first_after = String::from_utf8_lossy(&history_after[0]);
assert!(first_after.contains("Tag1"),
"first scrollback line after resize should still contain Tag1, got: {}", first_after);
}
#[test]
fn resize_pending_scrollback_independent_of_horizontal_resize() {
let mut screen = Screen::new(15, 3, 100);
for i in 1..=5 {
screen.process(format!("L{}\r\n", i).as_bytes());
}
let pending_before = screen.grid.scrollback_len - screen.grid.pending_start;
assert!(pending_before > 0, "pending scrollback should exist");
screen.resize(20, 3);
let pending_after = screen.grid.scrollback_len - screen.grid.pending_start;
assert_eq!(pending_before, pending_after,
"pending scrollback count should survive horizontal-only resize");
}
#[test]
fn resize_vertical_grow_restores_scrollback_from_pending() {
let mut screen = Screen::new(15, 3, 100);
for i in 1..=5 {
screen.process(format!("L{}\r\n", i).as_bytes());
}
let sb_before = screen.grid.scrollback_len;
assert!(sb_before > 0, "scrollback should exist");
screen.resize(15, 5);
let restored = sb_before - screen.grid.scrollback_len;
assert!(restored > 0, "some scrollback should have been restored");
assert!(screen.grid.pending_start <= screen.grid.scrollback_len);
}
#[test]
fn resize_expand_after_scroll_visible_cells_intact() {
let mut screen = Screen::new(6, 3, 100);
screen.process(b"\x1b[1;1Haaa");
screen.process(b"\x1b[2;1Hbbb");
screen.process(b"\x1b[3;1Hccc");
screen.process(b"\x1b[S");
assert_cell(&screen, 0, 0, 'b');
assert_cell(&screen, 1, 0, 'c');
screen.resize(10, 6);
assert_cell(&screen, 0, 0, 'a');
assert_cell(&screen, 0, 1, 'a');
assert_cell(&screen, 0, 2, 'a');
assert_cell(&screen, 1, 0, 'b');
assert_cell(&screen, 1, 1, 'b');
assert_cell(&screen, 1, 2, 'b');
assert_cell(&screen, 2, 0, 'c');
assert_cell(&screen, 2, 1, 'c');
assert_cell(&screen, 2, 2, 'c');
assert_cell(&screen, 3, 0, ' ');
assert_cell(&screen, 0, 6, ' ');
let history = screen.get_history();
assert!(history.is_empty(), "scrollback should be empty after restore");
}
#[test]
fn resize_shrink_after_scroll_visible_cells_intact() {
let mut screen = Screen::new(10, 4, 100);
screen.process(b"\x1b[1;1HAAAA");
screen.process(b"\x1b[2;1HBBBB");
screen.process(b"\x1b[3;1HCCCC");
screen.process(b"\x1b[4;1HDDDD");
screen.process(b"\x1b[2S");
assert_cell(&screen, 0, 0, 'C');
assert_cell(&screen, 1, 0, 'D');
screen.resize(6, 2);
assert_cell(&screen, 0, 0, 'C');
assert_cell(&screen, 0, 1, 'C');
assert_cell(&screen, 1, 0, 'D');
assert_cell(&screen, 1, 1, 'D');
let history = screen.get_history();
let all_text: String = history.iter()
.map(|l| String::from_utf8_lossy(l).into_owned())
.collect::<Vec<_>>()
.join("\n");
assert!(all_text.contains("AAAA"), "scrollback should contain AAAA");
assert!(all_text.contains("BBBB"), "scrollback should contain BBBB");
}
#[test]
fn resize_after_scroll_reattach_renders_correctly() {
let mut screen = Screen::new(20, 3, 100);
screen.process(b"\x1b[1;1HAlpha");
screen.process(b"\x1b[2;1HBravo");
screen.process(b"\x1b[3;1HCharlie");
screen.process(b"\x1b[S");
screen.resize(25, 5);
let rendered = reattach_render(&screen);
assert!(rendered.contains("Alpha"),
"Alpha should be restored to screen after expand");
assert!(rendered.contains("Bravo"),
"Bravo should be visible after scroll+expand");
assert!(rendered.contains("Charlie"),
"Charlie should be visible after scroll+expand");
let history = screen.get_history();
assert!(history.is_empty(),
"scrollback should be empty after all lines restored");
}
#[test]
fn resize_alt_screen_no_scrollback_leak() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"Main1\r\nMain2\r\nMain3\r\nMain4\r\n");
let main_history = screen.get_history();
screen.process(b"\x1b[?1049h");
screen.process(b"AltContent");
screen.resize(20, 5);
let history_after = screen.get_history();
assert_eq!(main_history.len(), history_after.len(),
"alt screen resize should not add scrollback lines");
}
#[test]
fn resize_vertical_shrink_drops_bottom_rows_without_scrollback() {
let mut screen = Screen::new(10, 5, 100);
screen.process(b"\x1b[1;1HTop");
screen.process(b"\x1b[5;1HBottom");
let history_before = screen.get_history();
screen.resize(10, 3);
let history_after = screen.get_history();
assert_eq!(history_before.len(), history_after.len(),
"vertical shrink should NOT add dropped rows to scrollback");
assert_cell(&screen, 0, 0, 'T');
let rendered = reattach_render(&screen);
assert!(!rendered.contains("Bottom"),
"Bottom row should be permanently lost after vertical shrink");
}
#[test]
fn scrollback_frozen_at_old_width_after_resize() {
let mut screen = Screen::new(20, 3, 100);
screen.process(b"\x1b[1;1HABCDEFGHIJKLMNOPQRST"); screen.process(b"\x1b[2;1HSecondLine");
screen.process(b"\x1b[3;1HThirdLine");
screen.process(b"\x1b[S");
let history_before = screen.get_history();
let scrollback_line = String::from_utf8_lossy(&history_before[0]);
assert!(scrollback_line.contains("ABCDEFGHIJKLMNOPQRST"),
"scrollback should contain full 20-char line");
screen.resize(10, 3);
let history_after = screen.get_history();
let scrollback_after = String::from_utf8_lossy(&history_after[0]);
assert!(scrollback_after.contains("ABCDEFGHIJKLMNOPQRST"),
"scrollback line should retain original width (20 chars), \
not be truncated to new width (10). Got: {}", scrollback_after);
}
#[test]
fn scrollback_mixed_widths_after_multiple_resizes() {
let mut screen = Screen::new(15, 3, 100);
screen.process(b"\x1b[1;1HWidth15_LineLn");
screen.process(b"\x1b[S");
screen.resize(10, 3);
screen.process(b"\x1b[1;1HWidth10_Li");
screen.process(b"\x1b[S");
screen.resize(20, 3);
screen.process(b"\x1b[1;1HWidth20_LineContent!");
screen.process(b"\x1b[S");
let history = screen.get_history();
let lines: Vec<String> = history.iter()
.map(|l| String::from_utf8_lossy(l).into_owned())
.collect();
assert!(lines[0].contains("Width15"),
"first scrollback (width=15) should be preserved: {}", lines[0]);
assert!(lines[1].contains("Width10"),
"second scrollback (width=10) should be preserved: {}", lines[1]);
assert!(lines[2].contains("Width20"),
"third scrollback (width=20) should be preserved: {}", lines[2]);
}
#[test]
fn render_with_scrollback_after_resize_positions_correctly() {
let mut screen = Screen::new(20, 5, 100);
screen.process(b"Line1\r\nLine2\r\nLine3\r\nLine4\r\nLine5\r\nLine6\r\n");
let _ = screen.take_pending_scrollback();
let history = screen.get_history();
assert!(!history.is_empty());
screen.resize(20, 8);
let mut cache = RenderCache::new();
let output = screen.render_with_scrollback(&history, &mut cache);
let rendered = String::from_utf8_lossy(&output);
assert!(rendered.contains("\x1b[8;1H"),
"scrollback injection should position at new row count (8), \
rendered: {}", rendered.chars().take(200).collect::<String>());
let sync_start = rendered.find("\x1b[?2026h").unwrap();
let scroll_pos = rendered.find("\x1b[8;1H").unwrap();
assert!(scroll_pos < sync_start,
"scrollback injection (pos {}) should appear before sync block (pos {})",
scroll_pos, sync_start);
}
#[test]
fn render_with_scrollback_after_shrink_positions_correctly() {
let mut screen = Screen::new(20, 6, 100);
for i in 1..=10 {
screen.process(format!("Row{}\r\n", i).as_bytes());
}
let history = screen.get_history();
screen.resize(20, 3);
let mut cache = RenderCache::new();
let output = screen.render_with_scrollback(&history, &mut cache);
let rendered = String::from_utf8_lossy(&output);
assert!(rendered.contains("\x1b[3;1H"),
"scrollback injection should position at shrunk row count (3)");
}
#[test]
fn reattach_with_scrollback_after_resize_has_both() {
let mut screen = Screen::new(20, 3, 100);
screen.process(b"\x1b[1;1HOldLine1");
screen.process(b"\x1b[2;1HOldLine2");
screen.process(b"\x1b[3;1HOldLine3");
screen.process(b"\x1b[2S");
screen.process(b"\x1b[2;1HNewLine4");
screen.process(b"\x1b[3;1HNewLine5");
let history = screen.get_history();
let hist_text: String = history.iter()
.map(|l| String::from_utf8_lossy(l).into_owned())
.collect::<Vec<_>>()
.join("|");
assert!(hist_text.contains("OldLine1"), "scrollback should have OldLine1");
assert!(hist_text.contains("OldLine2"), "scrollback should have OldLine2");
screen.resize(25, 5);
let mut cache = RenderCache::new();
let output = screen.render_with_scrollback(&history, &mut cache);
let rendered = String::from_utf8_lossy(&output);
assert!(rendered.contains("OldLine1"),
"reattach render should include scrollback OldLine1");
assert!(rendered.contains("OldLine2"),
"reattach render should include scrollback OldLine2");
assert!(rendered.contains("OldLine3"),
"reattach render should include visible OldLine3");
assert!(rendered.contains("NewLine4"),
"reattach render should include visible NewLine4");
assert!(rendered.contains("NewLine5"),
"reattach render should include visible NewLine5");
}
#[test]
fn scroll_after_resize_captures_at_new_width() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"\x1b[1;1H0123456789");
screen.resize(20, 3);
screen.process(b"\x1b[1;1HABCDEFGHIJ1234567890"); screen.process(b"\x1b[S");
let history = screen.get_history();
let last = String::from_utf8_lossy(&history[history.len() - 1]);
assert!(last.contains("ABCDEFGHIJ1234567890"),
"line scrolled off after resize should contain full 20-char content, got: {}", last);
}
#[test]
fn vertical_shrink_with_scrollback_then_reattach() {
let mut screen = Screen::new(15, 4, 100);
for i in 1..=8 {
screen.process(format!("Hist{}\r\n", i).as_bytes());
}
let scrollback_count = screen.get_history().len();
assert!(scrollback_count > 0);
screen.process(b"\x1b[1;1HVisible1");
screen.process(b"\x1b[4;1HVisible4");
screen.resize(15, 2);
assert_eq!(screen.get_history().len(), scrollback_count,
"scrollback count must not change on vertical shrink");
let history = screen.get_history();
let mut cache = RenderCache::new();
let output = screen.render_with_scrollback(&history, &mut cache);
let rendered = String::from_utf8_lossy(&output);
assert!(rendered.contains("Visible1"),
"surviving top row should render");
assert!(!rendered.contains("Visible4"),
"dropped bottom row should not render");
assert!(rendered.contains("Hist1"),
"scrollback should be in reattach output");
}
#[test]
fn scrollback_not_duplicated_on_resize() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"A\r\nB\r\nC\r\nD\r\nE\r\n");
let count_before = screen.get_history().len();
screen.resize(20, 5);
let after_expand = screen.get_history().len();
assert!(after_expand <= count_before,
"scrollback should not grow on expand");
screen.resize(5, 2);
let after_shrink = screen.get_history().len();
assert_eq!(after_shrink, after_expand,
"scrollback should not change on shrink");
screen.resize(10, 3);
let after_reexpand = screen.get_history().len();
assert!(after_reexpand <= after_shrink,
"scrollback should not grow on re-expand");
}
#[test]
fn resize_shrink_splits_wide_char_at_boundary() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"\x1b[1;5H"); screen.process("你".as_bytes()); assert_eq!(screen.grid.visible_row(0)[4].width, 2);
assert_eq!(screen.grid.visible_row(0)[5].width, 0);
screen.resize(5, 3); let cell4 = &screen.grid.visible_row(0)[4];
assert_ne!(cell4.width, 2,
"orphaned wide char (width=2 without continuation) should be cleaned up, \
got width={} char='{}'", cell4.width, cell4.c);
}
#[test]
fn resize_shrink_wide_char_fully_inside_survives() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"\x1b[1;3H"); screen.process("世".as_bytes()); assert_eq!(screen.grid.visible_row(0)[2].c, '世');
assert_eq!(screen.grid.visible_row(0)[2].width, 2);
assert_eq!(screen.grid.visible_row(0)[3].width, 0);
screen.resize(6, 3);
assert_eq!(screen.grid.visible_row(0)[2].c, '世');
assert_eq!(screen.grid.visible_row(0)[2].width, 2);
assert_eq!(screen.grid.visible_row(0)[3].width, 0,
"wide char fully inside new width should survive intact");
}
#[test]
fn resize_shrink_wide_char_at_exact_right_edge() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"\x1b[1;9H"); screen.process("界".as_bytes()); assert_eq!(screen.grid.visible_row(0)[8].width, 2);
assert_eq!(screen.grid.visible_row(0)[9].width, 0);
screen.resize(9, 3); let cell8 = &screen.grid.visible_row(0)[8];
assert_ne!(cell8.width, 2,
"wide char at right edge with truncated continuation should be cleaned up");
}
#[test]
fn resize_shrink_multiple_wide_chars_on_boundary() {
let mut screen = Screen::new(12, 3, 100);
screen.process("你好世界很棒".as_bytes());
screen.resize(7, 3);
assert_eq!(screen.grid.visible_row(0)[0].c, '你');
assert_eq!(screen.grid.visible_row(0)[0].width, 2);
assert_eq!(screen.grid.visible_row(0)[2].c, '好');
assert_eq!(screen.grid.visible_row(0)[4].c, '世');
assert_ne!(screen.grid.visible_row(0)[6].width, 2,
"wide char split at resize boundary should be cleaned up");
}
#[test]
fn resize_preserves_combining_marks() {
let mut screen = Screen::new(10, 3, 100);
screen.process("e\u{0301}".as_bytes());
assert_eq!(screen.grid.visible_row(0)[0].c, 'e');
assert_eq!(screen.grid.visible_row(0)[0].combining, vec!['\u{0301}']);
screen.resize(20, 5);
assert_eq!(screen.grid.visible_row(0)[0].c, 'e');
assert_eq!(screen.grid.visible_row(0)[0].combining, vec!['\u{0301}'],
"combining marks should survive resize expand");
screen.resize(5, 2);
assert_eq!(screen.grid.visible_row(0)[0].c, 'e');
assert_eq!(screen.grid.visible_row(0)[0].combining, vec!['\u{0301}'],
"combining marks should survive resize shrink");
}
#[test]
fn resize_wide_char_with_combining_survives() {
let mut screen = Screen::new(10, 3, 100);
screen.process("你\u{0308}".as_bytes()); assert_eq!(screen.grid.visible_row(0)[0].c, '你');
assert_eq!(screen.grid.visible_row(0)[0].width, 2);
assert!(screen.grid.visible_row(0)[0].combining.contains(&'\u{0308}'));
screen.resize(15, 3); assert_eq!(screen.grid.visible_row(0)[0].c, '你');
assert_eq!(screen.grid.visible_row(0)[0].width, 2);
assert!(screen.grid.visible_row(0)[0].combining.contains(&'\u{0308}'),
"combining mark on wide char should survive resize");
}
#[test]
fn resize_with_scroll_region_preserves_region_content() {
let mut screen = Screen::new(20, 10, 100);
screen.process(b"\x1b[3;7r");
for i in 1..=10 {
screen.process(format!("\x1b[{};1HR{}", i, i).as_bytes());
}
screen.resize(20, 6);
for (i, ch) in "R1".chars().enumerate() { assert_cell(&screen, 0, i, ch); }
for (i, ch) in "R2".chars().enumerate() { assert_cell(&screen, 1, i, ch); }
for (i, ch) in "R3".chars().enumerate() { assert_cell(&screen, 2, i, ch); }
for (i, ch) in "R4".chars().enumerate() { assert_cell(&screen, 3, i, ch); }
for (i, ch) in "R5".chars().enumerate() { assert_cell(&screen, 4, i, ch); }
for (i, ch) in "R6".chars().enumerate() { assert_cell(&screen, 5, i, ch); }
assert_eq!(screen.grid.scroll_top, 0);
assert_eq!(screen.grid.scroll_bottom, 5);
}
#[test]
fn resize_after_scroll_within_region() {
let mut screen = Screen::new(15, 6, 100);
screen.process(b"\x1b[2;4r");
screen.process(b"\x1b[2;1HInR2");
screen.process(b"\x1b[3;1HInR3");
screen.process(b"\x1b[4;1HInR4");
screen.process(b"\x1b[1;1HOutR1");
screen.process(b"\x1b[6;1HOutR6");
screen.process(b"\x1b[2;4r"); screen.process(b"\x1b[4;1H"); screen.process(b"\r\n");
screen.resize(15, 4);
for (i, ch) in "OutR1".chars().enumerate() { assert_cell(&screen, 0, i, ch); }
assert_eq!(screen.grid.scroll_top, 0);
assert_eq!(screen.grid.scroll_bottom, 3);
}
#[test]
fn resize_between_decsc_decrc_clamps_cursor() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b[24;80H"); screen.process(b"\x1b7"); assert_eq!(screen.grid.cursor_x, 79);
assert_eq!(screen.grid.cursor_y, 23);
screen.process(b"\x1b[1;1H");
screen.resize(40, 12);
screen.process(b"\x1b8"); assert_eq!(screen.grid.cursor_x, 39,
"restored cursor_x should clamp to cols-1 after resize");
assert_eq!(screen.grid.cursor_y, 11,
"restored cursor_y should clamp to rows-1 after resize");
}
#[test]
fn resize_between_csi_s_u_clamps_cursor() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b[20;70H"); screen.process(b"\x1b[s"); screen.process(b"\x1b[1;1H");
screen.resize(30, 10);
screen.process(b"\x1b[u"); assert_eq!(screen.grid.cursor_x, 29,
"CSI u cursor_x should clamp to cols-1 after resize");
assert_eq!(screen.grid.cursor_y, 9,
"CSI u cursor_y should clamp to rows-1 after resize");
}
#[test]
fn resize_expand_between_save_restore_preserves_cursor() {
let mut screen = Screen::new(40, 12, 100);
screen.process(b"\x1b[5;10H"); screen.process(b"\x1b7");
screen.process(b"\x1b[1;1H");
screen.resize(80, 24);
screen.process(b"\x1b8"); assert_eq!(screen.grid.cursor_x, 9,
"cursor_x within bounds should not change after expand");
assert_eq!(screen.grid.cursor_y, 4,
"cursor_y within bounds should not change after expand");
}
#[test]
fn resize_saved_cursor_style_preserved() {
let mut screen = Screen::new(20, 5, 100);
screen.process(b"\x1b[1;31m");
screen.process(b"\x1b[5;10H");
screen.process(b"\x1b7");
screen.process(b"\x1b[0m\x1b[1;1H"); screen.resize(10, 3);
screen.process(b"\x1b8"); assert!(screen.state.current_style.bold,
"restored style should be bold after resize");
}
#[test]
fn resize_in_alt_screen_then_exit_restores_main_resized() {
let mut screen = Screen::new(20, 5, 100);
screen.process(b"\x1b[1;1HMainContent");
screen.process(b"\x1b[3;1HRow3Data");
screen.process(b"\x1b[?1049h");
assert!(screen.state.in_alt_screen);
screen.process(b"AltText");
screen.resize(10, 3);
screen.process(b"\x1b[?1049l");
assert!(!screen.state.in_alt_screen);
assert_eq!(screen.grid.visible_row_count(), 3);
assert_eq!(screen.grid.visible_row(0).len(), 10);
for (i, ch) in "MainConten".chars().enumerate() {
assert_cell(&screen, 0, i, ch);
}
}
#[test]
fn resize_in_alt_screen_expand_then_exit() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"\x1b[1;1HSmall");
screen.process(b"\x1b[?1049h"); screen.resize(20, 6); screen.process(b"\x1b[?1049l");
assert_eq!(screen.grid.visible_row_count(), 6);
assert_eq!(screen.grid.visible_row(0).len(), 20);
for (i, ch) in "Small".chars().enumerate() {
assert_cell(&screen, 0, i, ch);
}
assert_cell(&screen, 3, 0, ' ');
assert_cell(&screen, 0, 10, ' ');
}
#[test]
fn resize_in_alt_screen_multiple_then_exit() {
let mut screen = Screen::new(20, 5, 100);
screen.process(b"\x1b[1;1HOriginal");
screen.process(b"\x1b[?1049h");
screen.resize(10, 3); screen.resize(30, 8); screen.resize(15, 4);
screen.process(b"\x1b[?1049l");
assert_eq!(screen.grid.visible_row_count(), 4);
assert_eq!(screen.grid.visible_row(0).len(), 15);
for (i, ch) in "Original".chars().enumerate() {
assert_cell(&screen, 0, i, ch);
}
}
#[test]
fn resize_current_style_persists_for_new_content() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"\x1b[1;31m");
screen.process(b"AB"); assert!(screen.grid.visible_row(0)[0].style.bold);
screen.resize(20, 5);
screen.process(b"CD");
assert!(screen.grid.visible_row(0)[2].style.bold,
"new text after resize should inherit bold from pre-resize style");
assert_eq!(screen.grid.visible_row(0)[2].c, 'C');
}
#[test]
fn resize_does_not_reset_sgr_state() {
let mut screen = Screen::new(20, 5, 100);
screen.process(b"\x1b[1;3;4;32m");
screen.process(b"X");
let style_before = screen.state.current_style;
screen.resize(10, 3);
assert_eq!(screen.state.current_style, style_before,
"current_style should not change on resize");
screen.process(b"Y");
assert_eq!(screen.grid.visible_row(0)[1].style, style_before,
"text written after resize should have identical style");
}
#[test]
fn resize_tab_content_preserved_but_stops_reset() {
let mut screen = Screen::new(40, 3, 100);
screen.process(b"A\tB\tC");
assert_cell(&screen, 0, 0, 'A');
assert_cell(&screen, 0, 8, 'B');
assert_cell(&screen, 0, 16, 'C');
screen.resize(20, 3);
assert_cell(&screen, 0, 0, 'A');
assert_cell(&screen, 0, 8, 'B');
assert_cell(&screen, 0, 16, 'C');
assert_eq!(screen.grid.tab_stops.len(), 20);
assert!(screen.grid.tab_stops[8]);
assert!(screen.grid.tab_stops[16]);
}
#[test]
fn resize_custom_tab_stop_lost() {
let mut screen = Screen::new(40, 3, 100);
screen.process(b"\x1b[1;6H"); screen.process(b"\x1bH"); screen.process(b"\x1b[1;1H");
screen.process(b"X\tY"); assert_cell(&screen, 0, 5, 'Y');
screen.resize(30, 3);
assert_cell(&screen, 0, 5, 'Y');
assert!(!screen.grid.tab_stops[5],
"custom tab stop at col 5 should be gone after resize");
}
#[test]
fn resize_cursor_at_top_left_stays() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b[1;1H"); screen.resize(40, 12);
assert_eq!(screen.grid.cursor_x, 0);
assert_eq!(screen.grid.cursor_y, 0);
}
#[test]
fn resize_cursor_at_top_right_clamps() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b[1;80H"); assert_eq!(screen.grid.cursor_x, 79);
screen.resize(40, 12);
assert_eq!(screen.grid.cursor_x, 39);
assert_eq!(screen.grid.cursor_y, 0);
}
#[test]
fn resize_cursor_at_bottom_left_clamps() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b[24;1H"); assert_eq!(screen.grid.cursor_y, 23);
screen.resize(40, 12);
assert_eq!(screen.grid.cursor_x, 0);
assert_eq!(screen.grid.cursor_y, 11);
}
#[test]
fn resize_cursor_at_bottom_right_clamps() {
let mut screen = Screen::new(80, 24, 100);
screen.process(b"\x1b[24;80H"); screen.resize(40, 12);
assert_eq!(screen.grid.cursor_x, 39);
assert_eq!(screen.grid.cursor_y, 11);
}
#[test]
fn resize_extreme_shrink_cursor_to_origin() {
let mut screen = Screen::new(100, 50, 100);
screen.process(b"\x1b[50;100H"); screen.resize(1, 1);
assert_eq!(screen.grid.cursor_x, 0);
assert_eq!(screen.grid.cursor_y, 0);
}
#[test]
fn resize_invalidates_render_cache() {
let mut screen = Screen::new(20, 5, 100);
screen.process(b"\x1b[1;1HCached");
let mut cache = RenderCache::new();
let first = screen.render(true, &mut cache);
let first_str = String::from_utf8_lossy(&first);
assert!(first_str.contains("Cached"));
screen.resize(30, 8);
screen.process(b"\x1b[6;1HNewRow");
let second = screen.render(false, &mut cache);
let second_str = String::from_utf8_lossy(&second);
assert!(second_str.contains("NewRow"),
"incremental render after resize should include new content \
(cache should be invalidated/resized)");
}
#[test]
fn resize_render_cache_shrink_then_expand() {
let mut screen = Screen::new(20, 6, 100);
for i in 1..=6 {
screen.process(format!("\x1b[{};1HR{}", i, i).as_bytes());
}
let mut cache = RenderCache::new();
let _ = screen.render(true, &mut cache);
screen.resize(20, 3);
let r2 = screen.render(true, &mut cache);
let r2_str = String::from_utf8_lossy(&r2);
assert!(r2_str.contains("R1"),
"full render after shrink should include surviving content");
screen.resize(20, 6);
screen.process(b"\x1b[5;1HNew5");
let r3 = screen.render(false, &mut cache);
let r3_str = String::from_utf8_lossy(&r3);
assert!(r3_str.contains("New5"),
"incremental render after expand should redraw new rows (sentinel cache)");
}
#[test]
fn resize_mid_csi_sequence_completes_after() {
let mut screen = Screen::new(20, 5, 100);
screen.process(b"\x1b[");
screen.resize(10, 3);
screen.process(b"3;5H");
assert_eq!(screen.grid.cursor_y, 2, "cursor row should be clamped to rows-1");
assert_eq!(screen.grid.cursor_x, 4, "cursor col within bounds");
}
#[test]
fn resize_mid_osc_sequence_completes_after() {
let mut screen = Screen::new(20, 5, 100);
screen.process(b"\x1b]2;My Ti");
screen.resize(10, 3);
screen.process(b"tle\x07"); assert_eq!(screen.state.title, "My Title",
"title should be set correctly despite resize mid-OSC");
}
#[test]
fn resize_mid_sgr_sequence_style_applied_after() {
let mut screen = Screen::new(20, 5, 100);
screen.process(b"\x1b[1;3");
screen.resize(10, 3);
screen.process(b"1m");
screen.process(b"X");
assert!(screen.grid.visible_row(0)[0].style.bold,
"bold should be applied despite resize mid-SGR");
}
#[test]
fn resize_in_alt_screen_modes_restored_correctly() {
let mut screen = Screen::new(20, 5, 100);
screen.process(b"\x1b[?2004h"); screen.process(b"\x1b[?1000h"); assert!(screen.grid.modes.bracketed_paste);
assert!(screen.grid.modes.mouse_modes.click);
screen.process(b"\x1b[?1049h");
screen.process(b"\x1b[?2004l"); assert!(!screen.grid.modes.bracketed_paste);
screen.resize(10, 3);
screen.process(b"\x1b[?1049l");
assert!(screen.grid.modes.bracketed_paste,
"bracketed paste should be restored from saved modes after resize");
assert!(screen.grid.modes.mouse_modes.click,
"mouse mode should be restored from saved modes after resize");
assert_eq!(screen.grid.scroll_top, 0);
assert_eq!(screen.grid.scroll_bottom, 2);
}
#[test]
fn resize_rapid_with_mixed_content() {
let mut screen = Screen::new(20, 5, 100);
screen.process(b"\x1b[1;31m"); screen.process("A你e\u{0301}B".as_bytes()); screen.process(b"\x1b[0m");
screen.process(b"\x1b[3;1HRow3");
screen.resize(10, 3);
screen.resize(30, 8);
screen.resize(5, 2);
screen.resize(15, 4);
screen.resize(20, 5);
assert_eq!(screen.grid.visible_row_count(), 5);
assert_eq!(screen.grid.visible_row(0).len(), 20);
assert_cell(&screen, 0, 0, 'A');
assert!(screen.grid.visible_row(0)[0].style.bold,
"style should survive rapid resizes");
}
#[test]
fn resize_vertical_expand_restores_scrollback() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"Line1\r\nLine2\r\nLine3\r\nLine4\r\nLine5");
screen.resize(10, 5);
assert_eq!(screen.grid.visible_row(0)[0].c, 'L');
assert_eq!(screen.grid.visible_row(0)[4].c, '1');
assert_eq!(screen.grid.visible_row(1)[4].c, '2');
assert_eq!(screen.grid.visible_row(2)[4].c, '3');
assert_eq!(screen.grid.visible_row(3)[4].c, '4');
assert_eq!(screen.grid.visible_row(4)[4].c, '5');
}
#[test]
fn resize_vertical_expand_shifts_cursor() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"Line1\r\nLine2\r\nLine3\r\nLine4\r\nLine5");
assert_eq!(screen.grid.cursor_y, 2);
let old_x = screen.grid.cursor_x;
screen.resize(10, 5);
assert_eq!(screen.grid.cursor_y, 4);
assert_eq!(screen.grid.cursor_x, old_x);
}
#[test]
fn resize_vertical_expand_limited_by_scrollback() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"AAA\r\nBBB\r\nCCC\r\nDDD");
screen.resize(10, 7);
assert_eq!(screen.grid.visible_row(0)[0].c, 'A');
assert_eq!(screen.grid.visible_row(1)[0].c, 'B');
for r in 4..7 {
assert_eq!(screen.grid.visible_row(r)[0].c, ' ',
"row {} should be blank", r);
}
assert_eq!(screen.grid.cursor_y, 3);
}
#[test]
fn resize_vertical_expand_no_restore_in_alt_screen() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"Line1\r\nLine2\r\nLine3\r\nLine4");
screen.process(b"\x1b[?1049h"); screen.resize(10, 5);
for r in 0..5 {
assert_eq!(screen.grid.visible_row(r)[0].c, ' ',
"row {} should be blank in alt screen", r);
}
}
#[test]
fn resize_vertical_expand_removes_restored_from_scrollback() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"L1\r\nL2\r\nL3\r\nL4\r\nL5");
assert_eq!(screen.get_history().len(), 2);
screen.resize(10, 5);
assert_eq!(screen.get_history().len(), 0);
}
#[test]
fn resize_same_height_no_scrollback_restore() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"L1\r\nL2\r\nL3\r\nL4\r\nL5");
let hist_before = screen.get_history().len();
screen.resize(10, 3); assert_eq!(screen.get_history().len(), hist_before);
}
#[test]
fn resize_scrollback_screen_boundary_integrity() {
let mut screen = Screen::new(10, 3, 100);
for i in 1..=10 {
if i < 10 {
screen.process(format!("L{:02}\r\n", i).as_bytes());
} else {
screen.process(format!("L{:02}", i).as_bytes());
}
}
let full_before = collect_full_history(&screen);
assert_eq!(full_before.len(), 10);
for (i, line) in full_before.iter().enumerate() {
assert!(line.contains(&format!("L{:02}", i + 1)),
"line {} should contain L{:02}, got: '{}'", i, i + 1, line);
}
screen.resize(10, 7);
let full_after_expand = collect_full_history(&screen);
assert_eq!(full_after_expand.len(), 10,
"total line count should stay 10 after expand, got: {:?}", full_after_expand);
for (i, line) in full_after_expand.iter().enumerate() {
assert!(line.contains(&format!("L{:02}", i + 1)),
"after expand: line {} should contain L{:02}, got: '{}'", i, i + 1, line);
}
assert_eq!(screen.get_history().len(), 3);
assert_eq!(screen.grid.visible_row(0)[0].c, 'L');
assert_eq!(screen.grid.visible_row(0)[1].c, '0');
assert_eq!(screen.grid.visible_row(0)[2].c, '4');
screen.resize(10, 4);
let scrollback_after_shrink = screen.get_history().len();
assert_eq!(scrollback_after_shrink, 3, "shrink should not alter scrollback");
let screen_lines = collect_screen_lines(&screen);
assert!(screen_lines[0].contains("L04"), "row 0 after shrink: '{}'", screen_lines[0]);
assert!(screen_lines[3].contains("L07"), "row 3 after shrink: '{}'", screen_lines[3]);
screen.resize(10, 10);
assert_eq!(screen.get_history().len(), 0, "all scrollback should be restored");
let screen_lines = collect_screen_lines(&screen);
assert!(screen_lines[0].contains("L01"),
"row 0 should be L01 from scrollback, got: '{}'", screen_lines[0]);
assert!(screen_lines[1].contains("L02"),
"row 1 should be L02 from scrollback, got: '{}'", screen_lines[1]);
assert!(screen_lines[2].contains("L03"),
"row 2 should be L03 from scrollback, got: '{}'", screen_lines[2]);
assert!(screen_lines[3].contains("L04"),
"row 3 should be L04, got: '{}'", screen_lines[3]);
}
#[test]
fn resize_expand_in_alt_screen_skips_scrollback_restore() {
let mut screen = Screen::new(10, 3, 100);
screen.process(b"L1\r\nL2\r\nL3\r\nL4\r\nL5");
let hist_before = screen.get_history().len();
assert!(hist_before > 0, "should have scrollback before alt screen");
screen.process(b"\x1b[?1049h");
screen.resize(10, 8);
assert_eq!(screen.get_history().len(), hist_before,
"scrollback should be preserved during alt screen resize");
screen.process(b"\x1b[?1049l");
assert_eq!(screen.get_history().len(), hist_before,
"scrollback should be preserved after exiting alt screen");
}