#![cfg(all(feature = "terminal-widget", unix))]
use std::time::Duration;
use ftui_core::geometry::Rect;
use ftui_extras::terminal::{
AnsiHandler, AnsiParser, CellAttrs, ClearRegion, TerminalEmulator, TerminalEmulatorState,
TerminalModes, TerminalState,
};
use ftui_pty::input_forwarding::{BracketedPaste, Key, KeyEvent, Modifiers, key_to_sequence};
use ftui_pty::virtual_terminal::VirtualTerminal;
use ftui_pty::{PtyConfig, spawn_command};
use ftui_render::cell::StyleFlags;
use ftui_render::frame::Frame;
use ftui_render::grapheme_pool::GraphemePool;
use ftui_style::Color;
use ftui_widgets::StatefulWidget;
use portable_pty::CommandBuilder;
use vt100::Parser as Vt100Parser;
#[test]
fn pty_spawn_shell_success() {
let config = PtyConfig::default()
.with_size(80, 24)
.with_test_name("pty_spawn_shell")
.logging(false);
let mut cmd = CommandBuilder::new("sh");
cmd.args(["-c", "echo 'hello from shell'"]);
let mut session = spawn_command(config, cmd).expect("spawn should succeed");
let status = session.wait().expect("wait should succeed");
assert!(status.success(), "shell should exit successfully");
let output = session
.read_until(b"hello from shell", Duration::from_secs(2))
.expect("should capture output");
assert!(
output
.windows(b"hello from shell".len())
.any(|w| w == b"hello from shell"),
"output should contain expected text"
);
}
#[test]
fn pty_environment_inheritance() {
let config = PtyConfig::default()
.with_size(80, 24)
.with_env("FTUI_TEST_VAR", "test_value_12345")
.logging(false);
let mut cmd = CommandBuilder::new("sh");
cmd.args(["-c", "echo $FTUI_TEST_VAR"]);
let mut session = spawn_command(config, cmd).expect("spawn should succeed");
let _ = session.wait().expect("wait should succeed");
let output = session
.read_until(b"test_value_12345", Duration::from_secs(2))
.expect("should capture env output");
assert!(
output
.windows(b"test_value_12345".len())
.any(|w| w == b"test_value_12345"),
"environment variable should be inherited"
);
}
#[test]
fn pty_term_variable_set() {
let config = PtyConfig::default()
.with_size(80, 24)
.with_term("xterm-256color")
.logging(false);
let mut cmd = CommandBuilder::new("sh");
cmd.args(["-c", "echo $TERM"]);
let mut session = spawn_command(config, cmd).expect("spawn should succeed");
let _ = session.wait().expect("wait should succeed");
let output = session
.read_until(b"xterm-256color", Duration::from_secs(2))
.expect("should capture TERM output");
assert!(
output
.windows(b"xterm-256color".len())
.any(|w| w == b"xterm-256color"),
"TERM should be set correctly"
);
}
struct TestHandler {
state: TerminalState,
}
impl TestHandler {
fn new(width: u16, height: u16) -> Self {
Self {
state: TerminalState::new(width, height),
}
}
}
impl AnsiHandler for TestHandler {
fn print(&mut self, ch: char) {
self.state.put_char(ch);
}
fn execute(&mut self, byte: u8) {
match byte {
0x08 => self.state.move_cursor_relative(-1, 0), 0x09 => {
let x = self.state.cursor().x;
let next = ((x / 8) + 1) * 8;
self.state.move_cursor(next, self.state.cursor().y);
}
0x0A..=0x0C => {
let cursor = self.state.cursor();
if cursor.y + 1 >= self.state.height() {
self.state.scroll_up(1);
} else {
self.state.move_cursor_relative(0, 1);
}
}
0x0D => {
self.state.move_cursor(0, self.state.cursor().y);
}
_ => {}
}
}
fn csi_dispatch(&mut self, params: &[i64], intermediates: &[u8], action: char) {
match (action, intermediates) {
('H', []) | ('f', []) => {
let row = params.first().copied().unwrap_or(1).max(1) as u16;
let col = params.get(1).copied().unwrap_or(1).max(1) as u16;
self.state
.move_cursor(col.saturating_sub(1), row.saturating_sub(1));
}
('A', []) => {
let n = params.first().copied().unwrap_or(1).max(1) as i16;
self.state.move_cursor_relative(0, -n);
}
('B', []) => {
let n = params.first().copied().unwrap_or(1).max(1) as i16;
self.state.move_cursor_relative(0, n);
}
('C', []) => {
let n = params.first().copied().unwrap_or(1).max(1) as i16;
self.state.move_cursor_relative(n, 0);
}
('D', []) => {
let n = params.first().copied().unwrap_or(1).max(1) as i16;
self.state.move_cursor_relative(-n, 0);
}
('J', []) => {
let mode = params.first().copied().unwrap_or(0);
match mode {
0 => self.state.clear_region(ClearRegion::CursorToEnd),
1 => self.state.clear_region(ClearRegion::StartToCursor),
2 | 3 => self.state.clear_region(ClearRegion::All),
_ => {}
}
}
('K', []) => {
let mode = params.first().copied().unwrap_or(0);
match mode {
0 => self.state.clear_region(ClearRegion::LineFromCursor),
1 => self.state.clear_region(ClearRegion::LineToCursor),
2 => self.state.clear_region(ClearRegion::Line),
_ => {}
}
}
('m', []) => {
let mut iter = params.iter().copied().peekable();
if params.is_empty() {
self.state.pen_mut().reset();
return;
}
while let Some(code) = iter.next() {
match code {
0 => self.state.pen_mut().reset(),
1 => {
self.state.pen_mut().attrs =
self.state.pen_mut().attrs.with(CellAttrs::BOLD)
}
2 => {
self.state.pen_mut().attrs =
self.state.pen_mut().attrs.with(CellAttrs::DIM)
}
3 => {
self.state.pen_mut().attrs =
self.state.pen_mut().attrs.with(CellAttrs::ITALIC)
}
4 => {
self.state.pen_mut().attrs =
self.state.pen_mut().attrs.with(CellAttrs::UNDERLINE)
}
5 => {
self.state.pen_mut().attrs =
self.state.pen_mut().attrs.with(CellAttrs::BLINK)
}
7 => {
self.state.pen_mut().attrs =
self.state.pen_mut().attrs.with(CellAttrs::REVERSE)
}
8 => {
self.state.pen_mut().attrs =
self.state.pen_mut().attrs.with(CellAttrs::HIDDEN)
}
9 => {
self.state.pen_mut().attrs =
self.state.pen_mut().attrs.with(CellAttrs::STRIKETHROUGH)
}
22 => {
self.state.pen_mut().attrs = self
.state
.pen_mut()
.attrs
.without(CellAttrs::BOLD)
.without(CellAttrs::DIM);
}
23 => {
self.state.pen_mut().attrs =
self.state.pen_mut().attrs.without(CellAttrs::ITALIC)
}
24 => {
self.state.pen_mut().attrs =
self.state.pen_mut().attrs.without(CellAttrs::UNDERLINE)
}
25 => {
self.state.pen_mut().attrs =
self.state.pen_mut().attrs.without(CellAttrs::BLINK)
}
27 => {
self.state.pen_mut().attrs =
self.state.pen_mut().attrs.without(CellAttrs::REVERSE)
}
28 => {
self.state.pen_mut().attrs =
self.state.pen_mut().attrs.without(CellAttrs::HIDDEN)
}
29 => {
self.state.pen_mut().attrs =
self.state.pen_mut().attrs.without(CellAttrs::STRIKETHROUGH)
}
30..=37 => {
self.state.pen_mut().fg = Some(ansi_color_to_color(code - 30));
}
38 => {
if let Some(color) = parse_extended_color(&mut iter) {
self.state.pen_mut().fg = Some(color);
}
}
39 => self.state.pen_mut().fg = None,
40..=47 => {
self.state.pen_mut().bg = Some(ansi_color_to_color(code - 40));
}
48 => {
if let Some(color) = parse_extended_color(&mut iter) {
self.state.pen_mut().bg = Some(color);
}
}
49 => self.state.pen_mut().bg = None,
90..=97 => {
self.state.pen_mut().fg = Some(ansi_bright_color(code - 90));
}
100..=107 => {
self.state.pen_mut().bg = Some(ansi_bright_color(code - 100));
}
_ => {}
}
}
}
_ => {}
}
}
fn osc_dispatch(&mut self, _params: &[&[u8]]) {
}
fn esc_dispatch(&mut self, _intermediates: &[u8], _c: char) {
}
}
fn ansi_color_to_color(index: i64) -> Color {
match index {
0 => Color::rgb(0, 0, 0), 1 => Color::rgb(187, 0, 0), 2 => Color::rgb(0, 187, 0), 3 => Color::rgb(187, 187, 0), 4 => Color::rgb(0, 0, 187), 5 => Color::rgb(187, 0, 187), 6 => Color::rgb(0, 187, 187), 7 => Color::rgb(187, 187, 187), _ => Color::rgb(187, 187, 187),
}
}
fn ansi_bright_color(index: i64) -> Color {
match index {
0 => Color::rgb(85, 85, 85), 1 => Color::rgb(255, 85, 85), 2 => Color::rgb(85, 255, 85), 3 => Color::rgb(255, 255, 85), 4 => Color::rgb(85, 85, 255), 5 => Color::rgb(255, 85, 255), 6 => Color::rgb(85, 255, 255), 7 => Color::rgb(255, 255, 255), _ => Color::rgb(255, 255, 255),
}
}
fn parse_extended_color(
iter: &mut std::iter::Peekable<impl Iterator<Item = i64>>,
) -> Option<Color> {
match iter.next() {
Some(5) => {
let idx = iter.next()? as u8;
Some(color_from_256(idx))
}
Some(2) => {
let r = iter.next()? as u8;
let g = iter.next()? as u8;
let b = iter.next()? as u8;
Some(Color::rgb(r, g, b))
}
_ => None,
}
}
fn color_from_256(idx: u8) -> Color {
match idx {
0..=7 => ansi_color_to_color(idx as i64),
8..=15 => ansi_bright_color((idx - 8) as i64),
16..=231 => {
let idx = idx - 16;
let r = (idx / 36) % 6;
let g = (idx / 6) % 6;
let b = idx % 6;
let to_rgb = |v: u8| if v == 0 { 0 } else { 55 + v * 40 };
Color::rgb(to_rgb(r), to_rgb(g), to_rgb(b))
}
232..=255 => {
let gray = 8 + (idx - 232) * 10;
Color::rgb(gray, gray, gray)
}
}
}
fn terminal_state_screen_text(state: &TerminalState) -> String {
(0..state.height())
.map(|y| {
let mut row = String::with_capacity(usize::from(state.width()));
for x in 0..state.width() {
let ch = state.cell(x, y).map_or(' ', |cell| cell.ch);
row.push(ch);
}
row.trim_end().to_string()
})
.collect::<Vec<_>>()
.join("\n")
}
fn vt100_screen_text(screen: &vt100::Screen) -> String {
let (_rows, cols) = screen.size();
screen
.rows(0, cols)
.map(|row| row.trim_end().to_string())
.collect::<Vec<_>>()
.join("\n")
}
fn assert_differential_parity(width: u16, height: u16, stream: &[u8]) {
let mut handler = TestHandler::new(width, height);
let mut parser = AnsiParser::new();
let mut reference = VirtualTerminal::new(width, height);
let mut vt100 = Vt100Parser::new(height, width, 0);
parser.parse(stream, &mut handler);
reference.feed(stream);
vt100.process(stream);
let lhs = terminal_state_screen_text(&handler.state);
let rhs_virtual = reference.screen_text();
assert_eq!(
lhs, rhs_virtual,
"screen text must match VirtualTerminal reference emulator"
);
let rhs_vt100 = vt100_screen_text(vt100.screen());
assert_eq!(
lhs, rhs_vt100,
"screen text must match vt100 reference emulator"
);
let lhs_cursor = (handler.state.cursor().x, handler.state.cursor().y);
let rhs_cursor = reference.cursor();
assert_eq!(
lhs_cursor, rhs_cursor,
"cursor must match VirtualTerminal reference emulator"
);
let (row, col) = vt100.screen().cursor_position();
let rhs_cursor_vt100 = (col, row);
assert_eq!(
lhs_cursor, rhs_cursor_vt100,
"cursor must match vt100 reference emulator"
);
}
#[test]
fn parser_to_state_basic_text() {
let mut handler = TestHandler::new(80, 24);
let mut parser = AnsiParser::new();
parser.parse(b"Hello, World!", &mut handler);
for (i, ch) in "Hello, World!".chars().enumerate() {
let cell = handler.state.cell(i as u16, 0).expect("cell should exist");
assert_eq!(cell.ch, ch, "character at position {}", i);
}
}
#[test]
fn parser_to_state_cursor_movement() {
let mut handler = TestHandler::new(80, 24);
let mut parser = AnsiParser::new();
parser.parse(b"\x1b[5;10H", &mut handler);
let cursor = handler.state.cursor();
assert_eq!(cursor.x, 9, "cursor x should be 9 (0-indexed)");
assert_eq!(cursor.y, 4, "cursor y should be 4 (0-indexed)");
}
#[test]
fn parser_to_state_sgr_colors() {
let mut handler = TestHandler::new(80, 24);
let mut parser = AnsiParser::new();
parser.parse(b"\x1b[31;42mX", &mut handler);
let cell = handler.state.cell(0, 0).expect("cell should exist");
assert_eq!(cell.ch, 'X');
assert!(cell.fg.is_some(), "foreground should be set");
assert!(cell.bg.is_some(), "background should be set");
}
#[test]
fn parser_to_state_256_colors() {
let mut handler = TestHandler::new(80, 24);
let mut parser = AnsiParser::new();
parser.parse(b"\x1b[38;5;196mR", &mut handler);
let cell = handler.state.cell(0, 0).expect("cell should exist");
assert_eq!(cell.ch, 'R');
assert!(cell.fg.is_some(), "256-color foreground should be set");
}
#[test]
fn parser_to_state_rgb_colors() {
let mut handler = TestHandler::new(80, 24);
let mut parser = AnsiParser::new();
parser.parse(b"\x1b[38;2;100;150;200mB", &mut handler);
let cell = handler.state.cell(0, 0).expect("cell should exist");
assert_eq!(cell.ch, 'B');
let fg = cell.fg.expect("RGB foreground should be set");
let rgb = fg.to_rgb();
assert_eq!(rgb.r, 100);
assert_eq!(rgb.g, 150);
assert_eq!(rgb.b, 200);
}
#[test]
fn parser_to_state_text_attributes() {
let mut handler = TestHandler::new(80, 24);
let mut parser = AnsiParser::new();
parser.parse(b"\x1b[1;3mB", &mut handler);
let cell = handler.state.cell(0, 0).expect("cell should exist");
assert_eq!(cell.ch, 'B');
assert!(cell.attrs.contains(CellAttrs::BOLD), "should be bold");
assert!(cell.attrs.contains(CellAttrs::ITALIC), "should be italic");
}
#[test]
fn parser_to_state_clear_screen() {
let mut handler = TestHandler::new(80, 24);
let mut parser = AnsiParser::new();
parser.parse(b"Hello", &mut handler);
parser.parse(b"\x1b[2J", &mut handler);
for x in 0..5 {
let cell = handler.state.cell(x, 0).expect("cell should exist");
assert_eq!(cell.ch, ' ', "cell at {} should be cleared", x);
}
}
#[test]
fn parser_to_state_line_wrapping() {
let mut handler = TestHandler::new(10, 5);
let mut parser = AnsiParser::new();
handler.state.set_mode(TerminalModes::WRAP, true);
parser.parse(b"1234567890ABC", &mut handler);
for (i, ch) in "1234567890".chars().enumerate() {
let cell = handler.state.cell(i as u16, 0).expect("cell should exist");
assert_eq!(cell.ch, ch, "line 0 position {}", i);
}
for (i, ch) in "ABC".chars().enumerate() {
let cell = handler.state.cell(i as u16, 1).expect("cell should exist");
assert_eq!(cell.ch, ch, "line 1 position {}", i);
}
}
#[test]
fn differential_replay_matches_virtual_terminal_for_basic_stream() {
let stream = b"HELLO\x1b[2;1Hworld\x1b[1;3HX\x1b[K\x1b[4;5H!";
assert_differential_parity(12, 4, stream);
}
#[test]
fn differential_replay_matches_virtual_terminal_for_erase_sequences() {
let stream = b"abcde\x1b[2;1H12345\x1b[1;3H\x1b[K\x1b[2J\x1b[1;1HZ";
assert_differential_parity(10, 4, stream);
}
#[test]
fn differential_replay_matches_virtual_terminal_for_scroll_progression() {
let stream = b"one\r\ntwo\r\nthree\r\nfour\r\n";
assert_differential_parity(6, 3, stream);
}
#[test]
fn differential_replay_matches_virtual_terminal_for_sgr_heavy_stream() {
let stream = b"\x1b[31mR\x1b[0m-\x1b[38;5;196mX\x1b[0m-\x1b[38;2;1;2;3mY\x1b[0m";
assert_differential_parity(20, 4, stream);
}
#[test]
fn input_forward_simple_key() {
let event = KeyEvent::plain(Key::Char('a'));
let seq = key_to_sequence(event);
assert_eq!(seq, b"a");
}
#[test]
fn input_forward_ctrl_c() {
let event = KeyEvent::new(Key::Char('c'), Modifiers::CTRL);
let seq = key_to_sequence(event);
assert_eq!(seq, &[0x03]); }
#[test]
fn input_forward_arrow_keys() {
assert_eq!(key_to_sequence(KeyEvent::plain(Key::Up)), b"\x1b[A");
assert_eq!(key_to_sequence(KeyEvent::plain(Key::Down)), b"\x1b[B");
assert_eq!(key_to_sequence(KeyEvent::plain(Key::Right)), b"\x1b[C");
assert_eq!(key_to_sequence(KeyEvent::plain(Key::Left)), b"\x1b[D");
}
#[test]
fn input_forward_function_keys() {
assert_eq!(key_to_sequence(KeyEvent::plain(Key::F(1))), b"\x1bOP");
assert_eq!(key_to_sequence(KeyEvent::plain(Key::F(5))), b"\x1b[15~");
assert_eq!(key_to_sequence(KeyEvent::plain(Key::F(12))), b"\x1b[24~");
}
#[test]
fn input_forward_enter_and_tab() {
assert_eq!(key_to_sequence(KeyEvent::plain(Key::Enter)), b"\r");
assert_eq!(key_to_sequence(KeyEvent::plain(Key::Tab)), b"\t");
}
#[test]
fn input_forward_alt_modifier() {
let event = KeyEvent::new(Key::Char('x'), Modifiers::ALT);
let seq = key_to_sequence(event);
assert_eq!(seq, b"\x1bx"); }
#[test]
fn input_forward_bracketed_paste() {
let mut paste = BracketedPaste::new();
paste.enable();
let seq = paste.wrap(b"Hello, World!");
assert!(seq.starts_with(b"\x1b[200~"));
assert!(seq.ends_with(b"\x1b[201~"));
assert!(
seq.windows(b"Hello, World!".len())
.any(|w| w == b"Hello, World!")
);
}
fn create_test_frame(width: u16, height: u16) -> (Frame<'static>, Box<GraphemePool>) {
let pool = Box::new(GraphemePool::default());
let pool_ref: &'static mut GraphemePool = Box::leak(pool);
let frame = Frame::new(width, height, pool_ref);
(frame, Box::new(GraphemePool::default()))
}
#[test]
fn widget_renders_text() {
let mut state = TerminalEmulatorState::new(80, 24);
for (i, ch) in "Hello!".chars().enumerate() {
state.terminal.move_cursor(i as u16, 0);
state.terminal.put_char(ch);
}
let widget = TerminalEmulator::new();
let area = Rect::new(0, 0, 80, 24);
let (mut frame, _pool) = create_test_frame(80, 24);
StatefulWidget::render(&widget, area, &mut frame, &mut state);
for (i, expected) in "Hello!".chars().enumerate() {
let cell = frame.buffer.get(i as u16, 0).expect("cell should exist");
assert_eq!(cell.content.as_char(), Some(expected), "position {}", i);
}
}
#[test]
fn widget_renders_colors() {
let mut state = TerminalEmulatorState::new(80, 24);
state.terminal.pen_mut().fg = Some(Color::rgb(255, 0, 0));
state.terminal.pen_mut().bg = Some(Color::rgb(0, 255, 0));
state.terminal.move_cursor(0, 0);
state.terminal.put_char('R');
let widget = TerminalEmulator::new();
let area = Rect::new(0, 0, 80, 24);
let (mut frame, _pool) = create_test_frame(80, 24);
StatefulWidget::render(&widget, area, &mut frame, &mut state);
let cell = frame.buffer.get(0, 0).expect("cell should exist");
assert_eq!(cell.content.as_char(), Some('R'));
assert_ne!(cell.fg.0, 0, "foreground should be set");
assert_ne!(cell.bg.0, 0, "background should be set");
}
#[test]
fn widget_renders_cursor() {
let mut state = TerminalEmulatorState::new(80, 24);
state.terminal.move_cursor(5, 3);
state.terminal.set_cursor_visible(true);
let widget = TerminalEmulator::new().show_cursor(true).cursor_phase(true);
let area = Rect::new(0, 0, 80, 24);
let (mut frame, _pool) = create_test_frame(80, 24);
StatefulWidget::render(&widget, area, &mut frame, &mut state);
let cursor_cell = frame.buffer.get(5, 3).expect("cursor cell should exist");
assert!(
cursor_cell.attrs.flags().contains(StyleFlags::REVERSE),
"cursor should have REVERSE style"
);
}
#[test]
fn widget_cursor_hidden_when_disabled() {
let mut state = TerminalEmulatorState::new(80, 24);
state.terminal.move_cursor(5, 3);
state.terminal.set_cursor_visible(true);
let widget = TerminalEmulator::new().show_cursor(false);
let area = Rect::new(0, 0, 80, 24);
let (mut frame, _pool) = create_test_frame(80, 24);
StatefulWidget::render(&widget, area, &mut frame, &mut state);
let cursor_cell = frame.buffer.get(5, 3).expect("cursor cell should exist");
assert!(
!cursor_cell.attrs.flags().contains(StyleFlags::REVERSE),
"cursor should NOT have REVERSE style when disabled"
);
}
#[test]
fn widget_scroll_offset() {
let mut state = TerminalEmulatorState::with_scrollback(10, 5, 100);
for _ in 0..3 {
state.terminal.scroll_up(1);
}
state.scroll_up(2);
assert_eq!(state.scroll_offset, 2);
state.scroll_down(1);
assert_eq!(state.scroll_offset, 1);
state.reset_scroll();
assert_eq!(state.scroll_offset, 0);
}
#[test]
fn widget_resize() {
let mut state = TerminalEmulatorState::new(80, 24);
state.resize(120, 40);
assert_eq!(state.terminal.width(), 120);
assert_eq!(state.terminal.height(), 40);
}
#[test]
fn full_pipeline_pty_to_widget() {
let config = PtyConfig::default().with_size(40, 10).logging(false);
let mut cmd = CommandBuilder::new("sh");
cmd.args(["-c", "printf 'TESTOUTPUT'"]);
let mut session = spawn_command(config, cmd).expect("spawn should succeed");
let _ = session.wait().expect("wait should succeed");
let output = session
.read_until(b"TESTOUTPUT", Duration::from_secs(2))
.expect("should capture output");
let mut handler = TestHandler::new(40, 10);
let mut parser = AnsiParser::new();
parser.parse(&output, &mut handler);
let mut state = TerminalEmulatorState::new(40, 10);
state.terminal = handler.state;
let widget = TerminalEmulator::new();
let area = Rect::new(0, 0, 40, 10);
let (mut frame, _pool) = create_test_frame(40, 10);
StatefulWidget::render(&widget, area, &mut frame, &mut state);
let mut found = false;
for y in 0..10 {
let mut line = String::new();
for x in 0..40 {
if let Some(cell) = frame.buffer.get(x, y)
&& let Some(c) = cell.content.as_char()
{
line.push(c);
}
}
if line.contains("TESTOUTPUT") {
found = true;
break;
}
}
assert!(found, "TESTOUTPUT should appear in rendered frame");
}
#[test]
fn input_forwarding_roundtrip() {
let config = PtyConfig::default().with_size(80, 24).logging(false);
let mut cmd = CommandBuilder::new("sh");
cmd.args(["-c", "IFS= read -r line; printf '%s' \"$line\""]);
let mut session = spawn_command(config, cmd).expect("spawn should succeed");
session
.send_input(&key_to_sequence(KeyEvent::plain(Key::Char('X'))))
.expect("send should succeed");
session
.send_input(&key_to_sequence(KeyEvent::plain(Key::Enter)))
.expect("send should succeed");
let output = session
.read_until(b"X", Duration::from_secs(2))
.expect("should receive echo");
let status = session.wait().expect("wait should succeed");
assert!(status.success(), "child should exit successfully");
assert!(
output.windows(1).any(|w| w == b"X"),
"output should contain echoed character"
);
}
#[test]
fn empty_area_render_does_not_panic() {
let mut state = TerminalEmulatorState::new(80, 24);
let widget = TerminalEmulator::new();
let area = Rect::new(0, 0, 0, 24);
let (mut frame, _pool) = create_test_frame(80, 24);
StatefulWidget::render(&widget, area, &mut frame, &mut state);
let area = Rect::new(0, 0, 80, 0);
StatefulWidget::render(&widget, area, &mut frame, &mut state);
}
#[test]
fn terminal_smaller_than_area() {
let mut state = TerminalEmulatorState::new(40, 10);
state.terminal.move_cursor(0, 0);
state.terminal.put_char('X');
let widget = TerminalEmulator::new();
let area = Rect::new(0, 0, 80, 24);
let (mut frame, _pool) = create_test_frame(80, 24);
StatefulWidget::render(&widget, area, &mut frame, &mut state);
let cell = frame.buffer.get(0, 0).expect("cell should exist");
assert_eq!(cell.content.as_char(), Some('X'));
}
#[test]
fn area_offset_respected() {
let mut state = TerminalEmulatorState::new(10, 5);
state.terminal.move_cursor(0, 0);
state.terminal.put_char('Z');
let widget = TerminalEmulator::new();
let area = Rect::new(5, 3, 10, 5);
let (mut frame, _pool) = create_test_frame(80, 24);
StatefulWidget::render(&widget, area, &mut frame, &mut state);
let cell = frame.buffer.get(5, 3).expect("cell should exist");
assert_eq!(cell.content.as_char(), Some('Z'));
}