use super::*;
#[test]
fn contents_between_range() {
let mut parser = crate::Parser::new(TerminalSize { rows: 5, cols: 20 }, 0);
parser.process(b"line one\r\n");
parser.process(b"line two\r\n");
parser.process(b"line three\r\n");
let text = parser
.screen()
.contents_between(Position { row: 0, col: 0 }, Position { row: 1, col: 7 });
let expected = format!("{:<20}\nline two", "line one");
assert_eq!(text, expected);
}
#[test]
fn contents_between_single_line() {
let mut parser = crate::Parser::new(TerminalSize { rows: 5, cols: 20 }, 0);
parser.process(b"hello world");
let text = parser
.screen()
.contents_between(Position { row: 0, col: 6 }, Position { row: 0, col: 10 });
assert_eq!(text, "world");
}
#[test]
fn contents_between_clamps_to_bounds() {
let mut parser = crate::Parser::new(TerminalSize { rows: 3, cols: 10 }, 0);
parser.process(b"abc");
let text = parser
.screen()
.contents_between(Position { row: 0, col: 0 }, Position { row: 99, col: 99 });
assert!(text.contains("abc"));
}
#[test]
fn contents_between_empty_range() {
let mut parser = crate::Parser::new(TerminalSize { rows: 5, cols: 20 }, 0);
parser.process(b"hello");
let text = parser
.screen()
.contents_between(Position { row: 2, col: 0 }, Position { row: 2, col: 19 });
assert_eq!(text.trim_end(), "");
assert_eq!(text.len(), 20);
}
#[test]
fn contents_between_respects_wrapped() {
let mut parser = crate::Parser::new(TerminalSize { rows: 5, cols: 5 }, 0);
parser.process(b"helloworld");
let text = parser
.screen()
.contents_between(Position { row: 0, col: 0 }, Position { row: 1, col: 4 });
assert_eq!(text, "helloworld");
}
#[test]
fn contents_formatted_roundtrip() {
let mut parser = crate::Parser::new(TerminalSize { rows: 5, cols: 20 }, 0);
parser.process(b"\x1b[31mred text\x1b[0m normal");
let formatted = parser
.screen()
.contents_formatted(FormattedOptions::default());
assert!(!formatted.is_empty());
let mut parser2 = crate::Parser::new(TerminalSize { rows: 5, cols: 20 }, 0);
parser2.process(&formatted);
assert_eq!(
parser.screen().contents(),
parser2.screen().contents(),
"round-tripping contents_formatted must reproduce the original plain text",
);
let cell_r = parser2.screen().cell(0, 0).unwrap();
assert_eq!(
cell_r.attrs().fg_color,
crate::attrs::Color::Index(1),
"red SGR fg must survive the contents_formatted round-trip",
);
assert_eq!(cell_r.contents(), "r");
let cell_n = parser2.screen().cell(0, 9).unwrap();
assert_eq!(
cell_n.attrs().fg_color,
crate::attrs::Color::Default,
"post-reset fg must round-trip as Default, not stay red from the previous span",
);
assert_eq!(cell_n.contents(), "n");
}
#[test]
fn contents_formatted_preserves_bold_italic() {
let mut parser = crate::Parser::new(TerminalSize { rows: 3, cols: 20 }, 0);
parser.process(b"\x1b[1;3mbold italic\x1b[0m");
let formatted = parser
.screen()
.contents_formatted(FormattedOptions::default());
let mut parser2 = crate::Parser::new(TerminalSize { rows: 3, cols: 20 }, 0);
parser2.process(&formatted);
let cell = parser2.screen().cell(0, 0).unwrap();
assert!(
cell.attrs().bold(),
"bold attribute lost across the formatted round-trip"
);
assert!(
cell.attrs().italic(),
"italic attribute lost across the formatted round-trip",
);
}
#[test]
fn contents_between_wide_chars() {
let mut parser = crate::Parser::new(TerminalSize { rows: 3, cols: 10 }, 0);
parser.process("AB\u{4e16}CD".as_bytes());
let text = parser
.screen()
.contents_between(Position { row: 0, col: 0 }, Position { row: 0, col: 5 });
assert_eq!(text, "AB\u{4e16}CD");
let text = parser
.screen()
.contents_between(Position { row: 0, col: 3 }, Position { row: 0, col: 5 });
assert_eq!(text, "CD");
}
#[test]
fn contents_formatted_wide_chars() {
let mut parser = crate::Parser::new(TerminalSize { rows: 3, cols: 10 }, 0);
parser.process("A\u{4e16}B".as_bytes());
let formatted = parser
.screen()
.contents_formatted(FormattedOptions::default());
let mut parser2 = crate::Parser::new(TerminalSize { rows: 3, cols: 10 }, 0);
parser2.process(&formatted);
assert_eq!(parser.screen().contents(), parser2.screen().contents());
let cell = parser2.screen().cell(0, 1).unwrap();
assert!(
cell.is_wide(),
"wide-char head lost its width flag across the formatted round-trip",
);
let cell_cont = parser2.screen().cell(0, 2).unwrap();
assert!(
cell_cont.is_wide_continuation(),
"wide-char continuation cell lost its flag across the formatted round-trip",
);
}
#[test]
fn contents_formatted_with_scrollback_offset() {
let mut parser = crate::Parser::new(TerminalSize { rows: 3, cols: 10 }, 100);
for i in 0..6 {
parser.process(format!("line{i}\r\n").as_bytes());
}
let available = parser.screen().scrollback_available();
assert!(available > 0);
parser.screen_mut().scroll_to(available);
let formatted = parser
.screen()
.contents_formatted(FormattedOptions::default());
let mut parser2 = crate::Parser::new(TerminalSize { rows: 3, cols: 10 }, 0);
parser2.process(&formatted);
assert_eq!(parser.screen().contents(), parser2.screen().contents(),);
}
#[test]
fn contents_plain_drops_sgr() {
let mut parser = crate::Parser::new(TerminalSize { rows: 3, cols: 20 }, 0);
parser.process(b"\x1b[31mred\x1b[0m \x1b[1;3mbold\x1b[0m");
let plain = parser.screen().contents_plain(PlainOptions::default());
assert!(!plain.contains('\x1b'));
assert!(!plain.contains('['));
assert_eq!(plain, "red bold");
}
#[test]
fn contents_plain_wide_chars() {
let mut parser = crate::Parser::new(TerminalSize { rows: 3, cols: 10 }, 0);
parser.process("A\u{4e16}B".as_bytes());
let plain = parser.screen().contents_plain(PlainOptions::default());
assert_eq!(plain, "A\u{4e16}B");
}
#[test]
fn contents_plain_with_scrollback_offset() {
let mut parser = crate::Parser::new(TerminalSize { rows: 3, cols: 10 }, 100);
for i in 0..6 {
parser.process(format!("line{i}\r\n").as_bytes());
}
let available = parser.screen().scrollback_available();
assert!(available > 0);
parser.screen_mut().scroll_to(available);
let plain = parser.screen().contents_plain(PlainOptions::default());
assert!(
plain.contains("line0"),
"expected oldest history row in scrollback view, got: {plain:?}"
);
}
#[test]
fn contents_plain_trims_trailing_blanks() {
let mut parser = crate::Parser::new(TerminalSize { rows: 5, cols: 10 }, 0);
parser.process(b"hello");
let plain = parser.screen().contents_plain(PlainOptions::default());
assert_eq!(plain, "hello");
}
#[test]
fn contents_plain_empty_screen() {
let parser = crate::Parser::new(TerminalSize { rows: 5, cols: 10 }, 0);
assert_eq!(parser.screen().contents_plain(PlainOptions::default()), "");
}
#[test]
fn contents_plain_default_keeps_physical_layout() {
let mut parser = crate::Parser::new(TerminalSize { rows: 4, cols: 8 }, 0);
parser.process(b"abcdefghIJ");
let plain = parser.screen().contents_plain(PlainOptions::default());
assert_eq!(plain, "abcdefgh\nIJ");
}
#[test]
fn contents_plain_join_wrapped_reconstructs_logical_line() {
let mut parser = crate::Parser::new(TerminalSize { rows: 4, cols: 8 }, 0);
parser.process(b"abcdefghIJ");
let plain = parser.screen().contents_plain(PlainOptions {
join_wrapped: true,
..Default::default()
});
assert_eq!(plain, "abcdefghIJ");
}
#[test]
fn contents_plain_join_wrapped_preserves_explicit_newlines() {
let mut parser = crate::Parser::new(TerminalSize { rows: 6, cols: 8 }, 0);
parser.process(b"abcdefghIJ\r\nnext");
let plain = parser.screen().contents_plain(PlainOptions {
join_wrapped: true,
..Default::default()
});
assert_eq!(plain, "abcdefghIJ\nnext");
}
#[test]
fn contents_formatted_default_keeps_physical_layout() {
let mut parser = crate::Parser::new(TerminalSize { rows: 4, cols: 8 }, 0);
parser.process(b"abcdefghIJ");
let formatted = parser
.screen()
.contents_formatted(FormattedOptions::default());
assert_eq!(formatted, b"abcdefgh\r\nIJ");
}
#[test]
fn contents_formatted_join_wrapped_reconstructs_logical_line() {
let mut parser = crate::Parser::new(TerminalSize { rows: 4, cols: 8 }, 0);
parser.process(b"\x1b[31mabcdefghIJ\x1b[0m");
let formatted = parser.screen().contents_formatted(FormattedOptions {
join_wrapped: true,
..Default::default()
});
assert_eq!(formatted, b"\x1b[31mabcdefghIJ\x1b[0m");
let mut replay = crate::Parser::new(TerminalSize { rows: 2, cols: 16 }, 0);
replay.process(&formatted);
for col in 0..10 {
let cell = replay.screen().cell(0, col).unwrap();
assert_eq!(
cell.attrs().fg_color,
crate::attrs::Color::Index(1),
"col {col} lost red fg across the wrap join",
);
}
}
#[test]
fn contents_formatted_join_wrapped_preserves_explicit_newlines() {
let mut parser = crate::Parser::new(TerminalSize { rows: 6, cols: 8 }, 0);
parser.process(b"abcdefghIJ\r\nnext");
let formatted = parser.screen().contents_formatted(FormattedOptions {
join_wrapped: true,
..Default::default()
});
assert_eq!(formatted, b"abcdefghIJ\r\nnext");
}
#[test]
fn contents_plain_include_scrollback_emits_history_then_viewport() {
let mut parser = crate::Parser::new(TerminalSize { rows: 3, cols: 10 }, 100);
for i in 0..6 {
parser.process(format!("line{i}\r\n").as_bytes());
}
assert!(
parser.screen().scrollback_available() >= 4,
"fixture must push at least 4 lines into scrollback to exercise the history walk",
);
let viewport_only = parser.screen().contents_plain(PlainOptions::default());
assert_eq!(
viewport_only, "line4\nline5",
"default PlainOptions must not include retained scrollback history",
);
let with_history = parser.screen().contents_plain(PlainOptions {
include_scrollback: true,
..Default::default()
});
assert_eq!(
with_history, "line0\nline1\nline2\nline3\nline4\nline5",
"include_scrollback must prepend retained history (oldest first) to the live region",
);
}
#[test]
fn contents_plain_no_trim_emits_blank_rows_to_full_height() {
let mut parser = crate::Parser::new(TerminalSize { rows: 4, cols: 8 }, 0);
parser.process(b"hello");
let untrimmed = parser.screen().contents_plain(PlainOptions {
trim_trailing_blanks: false,
..Default::default()
});
assert_eq!(
untrimmed, "hello\n\n\n",
"trim_trailing_blanks=false must emit one blank row per remaining row of the screen",
);
}
#[test]
fn contents_formatted_include_scrollback_emits_history_then_viewport() {
let mut parser = crate::Parser::new(TerminalSize { rows: 3, cols: 10 }, 100);
for i in 0..6 {
parser.process(format!("line{i}\r\n").as_bytes());
}
assert!(
parser.screen().scrollback_available() >= 4,
"fixture must push at least 4 lines into scrollback to exercise the history walk",
);
let viewport_only = parser
.screen()
.contents_formatted(FormattedOptions::default());
assert_eq!(
viewport_only, b"line4\r\nline5",
"default FormattedOptions must not include retained scrollback history",
);
let with_history = parser.screen().contents_formatted(FormattedOptions {
include_scrollback: true,
..Default::default()
});
assert_eq!(
with_history, b"line0\r\nline1\r\nline2\r\nline3\r\nline4\r\nline5",
"include_scrollback must prepend retained history (oldest first) to the live region",
);
}
#[test]
fn contents_formatted_no_trim_emits_blank_rows_to_full_height() {
let mut parser = crate::Parser::new(TerminalSize { rows: 4, cols: 8 }, 0);
parser.process(b"hello");
let untrimmed = parser.screen().contents_formatted(FormattedOptions {
trim_trailing_blanks: false,
..Default::default()
});
assert_eq!(
untrimmed, b"hello\r\n\r\n\r\n",
"trim_trailing_blanks=false must emit a CRLF separator per remaining row of the screen",
);
}