use super::test_helpers::*;
use super::*;
use render::AnsiRenderer;
fn simulate_reconnect(screen: &mut Screen) -> (Vec<Vec<u8>>, Vec<u8>) {
let _ = screen.take_pending_scrollback();
let _ = screen.take_passthrough();
let history = screen.get_history();
let mut cache = AnsiRenderer::new();
let render = cache.render(screen, true);
(history, render)
}
fn app_redraw_inplace(screen: &mut Screen, lines: &[&str]) {
for (i, line) in lines.iter().enumerate() {
let cup = format!("\x1b[{};1H", i + 1);
screen.process(cup.as_bytes());
screen.process(line.as_bytes());
screen.process(b"\x1b[K");
}
}
fn app_redraw_sequential(screen: &mut Screen, lines: &[&str]) {
screen.process(b"\x1b[H");
for (i, line) in lines.iter().enumerate() {
screen.process(line.as_bytes());
if i < lines.len() - 1 {
screen.process(b"\r\n");
}
}
}
fn count_in_history(screen: &Screen, needle: &str) -> usize {
history_texts(screen)
.iter()
.filter(|line| line.contains(needle))
.count()
}
#[test]
fn get_history_is_idempotent() {
let mut screen = Screen::new(20, 3, 100);
screen.process(b"line1\r\nline2\r\nline3\r\nline4\r\nline5");
let _ = screen.take_pending_scrollback();
let h1 = screen.get_history();
let h2 = screen.get_history();
assert_eq!(
h1.len(),
h2.len(),
"get_history should return same length each time"
);
for (a, b) in h1.iter().zip(h2.iter()) {
assert_eq!(a, b, "get_history should return identical data each time");
}
}
#[test]
fn get_history_unchanged_without_new_output() {
let mut screen = Screen::new(20, 3, 100);
screen.process(b"A\r\nB\r\nC\r\nD\r\nE");
let _ = screen.take_pending_scrollback();
let h1 = history_texts(&screen);
let _ = simulate_reconnect(&mut screen);
let h2 = history_texts(&screen);
assert_eq!(
h1, h2,
"history should be unchanged after reconnect without new output"
);
}
#[test]
fn inplace_redraw_does_not_grow_scrollback() {
let mut screen = Screen::new(20, 5, 100);
let content = [
"LOGO: MyApp",
"===========",
"Status: OK",
"Line 4",
"Line 5",
];
app_redraw_inplace(&mut screen, &content);
let _ = screen.take_pending_scrollback();
let initial_history_len = screen.get_history().len();
let _ = simulate_reconnect(&mut screen);
app_redraw_inplace(&mut screen, &content);
let after_history_len = screen.get_history().len();
assert_eq!(
initial_history_len, after_history_len,
"in-place redraw should not add to scrollback"
);
}
#[test]
fn inplace_redraw_multiple_reconnects_no_growth() {
let mut screen = Screen::new(20, 4, 100);
let content = ["LOGO", "====", "Info", "Prompt>"];
app_redraw_inplace(&mut screen, &content);
let _ = screen.take_pending_scrollback();
for cycle in 0..5 {
let _ = simulate_reconnect(&mut screen);
app_redraw_inplace(&mut screen, &content);
let logo_count = count_in_history(&screen, "LOGO");
assert_eq!(
logo_count, 0,
"cycle {}: in-place redraw should never push LOGO into scrollback",
cycle
);
}
}
#[test]
fn sequential_redraw_at_top_does_not_scroll_when_fits() {
let mut screen = Screen::new(20, 5, 100);
let content = ["LOGO", "====", "Line1", "Line2", "Line3"];
app_redraw_sequential(&mut screen, &content);
let _ = screen.take_pending_scrollback();
let h0 = screen.get_history().len();
let _ = simulate_reconnect(&mut screen);
app_redraw_sequential(&mut screen, &content);
let h1 = screen.get_history().len();
assert_eq!(
h0, h1,
"sequential redraw fitting within screen should not grow scrollback"
);
}
#[test]
fn sequential_redraw_overflows_screen_grows_scrollback() {
let mut screen = Screen::new(20, 3, 100);
let content = ["LOGO", "====", "Line1", "Line2", "Line3"];
app_redraw_sequential(&mut screen, &content);
let pending_rows = screen.take_pending_scrollback();
let pending = AnsiRenderer::new().render_rows(&screen, &pending_rows);
assert_eq!(
pending.len(),
2,
"2 lines should scroll off when 5 lines written to 3-row screen"
);
let h = history_texts(&screen);
assert!(
h[0].contains("LOGO"),
"LOGO should be first scrollback line"
);
}
#[test]
fn logo_duplicates_on_reconnect_with_overflow_redraw() {
let mut screen = Screen::new(20, 4, 100);
screen.process(b"LOGO\r\n====\r\nLine1\r\nLine2");
let _ = screen.take_pending_scrollback();
assert_eq!(count_in_history(&screen, "LOGO"), 0, "LOGO still on screen");
screen.process(b"\r\nLine3\r\nLine4\r\nLine5");
let _ = screen.take_pending_scrollback();
assert_eq!(
count_in_history(&screen, "LOGO"),
1,
"LOGO scrolled into history once"
);
let _ = simulate_reconnect(&mut screen);
screen.process(b"\x1b[H"); screen.process(b"LOGO\nLine6\nLine7\nLine8\nLine9");
let _ = screen.take_pending_scrollback();
let logo_count = count_in_history(&screen, "LOGO");
assert_eq!(
logo_count, 2,
"expected 2 LOGOs in scrollback (original + redraw overflow), got {}",
logo_count
);
}
#[test]
fn logo_accumulates_with_each_reconnect_cycle() {
let mut screen = Screen::new(20, 3, 100);
screen.process(b"LOGO\r\nContent1\r\nPrompt>");
let _ = screen.take_pending_scrollback();
screen.process(b"\r\nOutput1\r\nOutput2");
let _ = screen.take_pending_scrollback();
assert_eq!(count_in_history(&screen, "LOGO"), 1);
for cycle in 1..=3 {
let _ = simulate_reconnect(&mut screen);
screen.process(b"\x1b[H");
screen.process(b"LOGO\nRedrawn\nMore\nPrompt>");
let _ = screen.take_pending_scrollback();
let logo_count = count_in_history(&screen, "LOGO");
assert_eq!(
logo_count,
1 + cycle,
"after {} reconnect cycles, expected {} LOGOs, got {}",
cycle,
1 + cycle,
logo_count
);
}
}
#[test]
fn shrink_vertical_does_not_push_to_scrollback() {
let mut screen = Screen::new(20, 5, 100);
screen.process(b"Row1\r\nRow2\r\nRow3\r\nRow4\r\nRow5");
let _ = screen.take_pending_scrollback();
let h_before = screen.get_history().len();
screen.resize(20, 3);
let h_after = screen.get_history().len();
assert_eq!(
h_before, h_after,
"vertical shrink should not push rows into scrollback"
);
}
#[test]
fn grow_vertical_restores_from_scrollback() {
let mut screen = Screen::new(20, 3, 100);
screen.process(b"LOGO\r\nLine2\r\nLine3\r\nLine4\r\nLine5");
let _ = screen.take_pending_scrollback();
assert_eq!(screen.get_history().len(), 2);
screen.resize(20, 5);
assert_eq!(
screen.get_history().len(),
0,
"growing should restore scrollback lines to grid"
);
}
#[test]
fn shrink_then_grow_cycle_scrollback_consistency() {
let mut screen = Screen::new(20, 5, 100);
screen.process(b"S1\r\nS2\r\nS3\r\nV1\r\nV2\r\nV3\r\nV4\r\nV5");
let _ = screen.take_pending_scrollback();
let h_initial = screen.get_history().len();
assert_eq!(h_initial, 3, "should have 3 lines in scrollback");
screen.resize(20, 3);
assert_eq!(
screen.get_history().len(),
3,
"shrink should not affect scrollback count"
);
screen.resize(20, 5);
assert_eq!(
screen.get_history().len(),
1,
"grow should restore 2 lines from scrollback"
);
}
#[test]
fn resize_smaller_then_app_redraw_inplace_no_duplication() {
let mut screen = Screen::new(20, 5, 100);
let content = ["LOGO", "====", "Line1", "Line2", "Prompt>"];
app_redraw_inplace(&mut screen, &content);
let _ = screen.take_pending_scrollback();
assert_eq!(count_in_history(&screen, "LOGO"), 0);
screen.resize(20, 3);
let _ = simulate_reconnect(&mut screen);
let short_content = ["LOGO", "====", "Prompt>"];
app_redraw_inplace(&mut screen, &short_content);
assert_eq!(
count_in_history(&screen, "LOGO"),
0,
"in-place redraw after resize should not push LOGO into scrollback"
);
}
#[test]
fn resize_smaller_then_app_sequential_redraw_overflows() {
let mut screen = Screen::new(20, 5, 100);
let content = ["LOGO", "====", "Line1", "Line2", "Prompt>"];
app_redraw_inplace(&mut screen, &content);
let _ = screen.take_pending_scrollback();
assert_eq!(count_in_history(&screen, "LOGO"), 0);
screen.resize(20, 3);
let _ = simulate_reconnect(&mut screen);
app_redraw_sequential(&mut screen, &content);
let _ = screen.take_pending_scrollback();
assert_eq!(
count_in_history(&screen, "LOGO"),
1,
"sequential redraw overflow after shrink should push LOGO into scrollback"
);
}
#[test]
fn clear_screen_does_not_generate_scrollback() {
let mut screen = Screen::new(20, 3, 100);
screen.process(b"LOGO\r\n====\r\nPrompt>");
let _ = screen.take_pending_scrollback();
let h_before = screen.get_history().len();
screen.process(b"\x1b[2J");
let _ = screen.take_pending_scrollback();
let h_after = screen.get_history().len();
assert_eq!(
h_before, h_after,
"\\e[2J should not push content into scrollback"
);
}
#[test]
fn clear_then_redraw_inplace_no_scrollback() {
let mut screen = Screen::new(20, 4, 100);
app_redraw_inplace(&mut screen, &["LOGO", "====", "Line1", "Line2"]);
let _ = screen.take_pending_scrollback();
screen.process(b"\x1b[2J");
app_redraw_inplace(&mut screen, &["LOGO", "====", "NewLine1", "NewLine2"]);
let h = screen.get_history().len();
assert_eq!(
h, 0,
"clear + in-place redraw should not generate scrollback"
);
}
#[test]
fn full_reconnect_cycle_same_size_inplace_no_duplication() {
let mut screen = Screen::new(20, 5, 100);
app_redraw_inplace(&mut screen, &["LOGO", "====", "", "", ""]);
screen.process(b"\x1b[3;1H"); screen.process(b"user> hello\r\n");
screen.process(b"response: hi\r\n");
screen.process(b"user> ");
let _ = screen.take_pending_scrollback();
let (history, _render) = simulate_reconnect(&mut screen);
assert_eq!(
count_in_history(&screen, "LOGO"),
0,
"LOGO should still be on screen"
);
app_redraw_inplace(
&mut screen,
&["LOGO", "====", "user> hello", "response: hi", "user> "],
);
assert_eq!(
count_in_history(&screen, "LOGO"),
0,
"reconnect + in-place redraw should not create duplicate LOGO"
);
let _ = history; }
#[test]
fn full_reconnect_cycle_different_size_with_overflow_causes_duplication() {
let mut screen = Screen::new(20, 5, 100);
screen.process(b"LOGO\r\n====\r\nLine1\r\nLine2\r\nLine3");
let _ = screen.take_pending_scrollback();
screen.process(b"\r\nLine4\r\nLine5\r\nLine6");
let _ = screen.take_pending_scrollback();
assert_eq!(count_in_history(&screen, "LOGO"), 1, "1 LOGO in scrollback");
screen.resize(20, 3);
let _ = simulate_reconnect(&mut screen);
screen.process(b"\x1b[H");
screen.process(b"LOGO\n====\nLine7\nLine8\nLine9");
let _ = screen.take_pending_scrollback();
let logo_count = count_in_history(&screen, "LOGO");
assert!(
logo_count >= 2,
"expected at least 2 LOGOs after overflow redraw on smaller screen, got {}",
logo_count
);
}
#[test]
fn scrollback_limit_caps_logo_accumulation() {
let limit = 10;
let mut screen = Screen::new(20, 3, limit);
for i in 1..=5 {
screen.process(format!("line{}\r\n", i).as_bytes());
}
let _ = screen.take_pending_scrollback();
for _ in 0..20 {
let _ = simulate_reconnect(&mut screen);
screen.process(b"\x1b[H");
screen.process(b"LOGO\nStuff\nMore\nExtra\nEnd");
let _ = screen.take_pending_scrollback();
}
let total = screen.get_history().len();
assert!(
total <= limit,
"scrollback should be capped at limit {}, got {}",
limit,
total
);
}
#[test]
fn alt_screen_app_no_scrollback_on_reconnect() {
let mut screen = Screen::new(20, 5, 100);
screen.process(b"\x1b[?1049h");
screen.process(b"Alt content\r\nMore alt\r\nEven more\r\n");
for _ in 0..20 {
screen.process(b"scroll in alt\r\n");
}
let _ = screen.take_pending_scrollback();
assert!(screen.in_alt_screen(), "should be in alt screen");
let history = screen.get_history();
assert!(
history.is_empty(),
"alt screen should have no scrollback history, got {} lines",
history.len()
);
}