use crate::error::{Result, TermTestError};
use crate::events::{encode_key_event, KeyCode, KeyEvent, Modifiers};
use crate::pty::TestTerminal;
use crate::screen::ScreenState;
use portable_pty::{CommandBuilder, ExitStatus};
use std::time::{Duration, Instant};
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
const DEFAULT_POLL_INTERVAL: Duration = Duration::from_millis(100);
const DEFAULT_BUFFER_SIZE: usize = 4096;
pub struct TuiTestHarness {
terminal: TestTerminal,
state: ScreenState,
timeout: Duration,
poll_interval: Duration,
buffer_size: usize,
}
impl TuiTestHarness {
pub fn new(width: u16, height: u16) -> Result<Self> {
let terminal = TestTerminal::new(width, height)?;
let state = ScreenState::new(width, height);
Ok(Self {
terminal,
state,
timeout: DEFAULT_TIMEOUT,
poll_interval: DEFAULT_POLL_INTERVAL,
buffer_size: DEFAULT_BUFFER_SIZE,
})
}
pub fn builder() -> TuiTestHarnessBuilder {
TuiTestHarnessBuilder::default()
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn with_poll_interval(mut self, interval: Duration) -> Self {
self.poll_interval = interval;
self
}
pub fn spawn(&mut self, cmd: CommandBuilder) -> Result<()> {
self.terminal.spawn(cmd)
}
pub fn send_text(&mut self, text: &str) -> Result<()> {
self.terminal.write(text.as_bytes())?;
self.update_state()?;
Ok(())
}
pub fn send_key(&mut self, key: KeyCode) -> Result<()> {
self.send_key_event(KeyEvent::new(key))
}
pub fn send_key_with_modifiers(&mut self, key: KeyCode, modifiers: Modifiers) -> Result<()> {
self.send_key_event(KeyEvent::with_modifiers(key, modifiers))
}
pub fn send_keys(&mut self, text: &str) -> Result<()> {
for ch in text.chars() {
self.send_key(KeyCode::Char(ch))?;
}
Ok(())
}
fn send_key_event(&mut self, event: KeyEvent) -> Result<()> {
let bytes = encode_key_event(&event);
self.terminal.write_all(&bytes)?;
std::thread::sleep(Duration::from_millis(50));
self.update_state()?;
Ok(())
}
pub fn update_state(&mut self) -> Result<()> {
let mut buf = vec![0u8; self.buffer_size];
loop {
match self.terminal.read(&mut buf) {
Ok(0) => break, Ok(n) => {
self.state.feed(&buf[..n]);
}
Err(e) if e.to_string().contains("WouldBlock") => break,
Err(e) => return Err(e),
}
}
Ok(())
}
pub fn wait_for<F>(&mut self, condition: F) -> Result<()>
where
F: Fn(&ScreenState) -> bool,
{
self.wait_for_with_context(condition, "condition")
}
pub fn wait_for_with_context<F>(&mut self, condition: F, description: &str) -> Result<()>
where
F: Fn(&ScreenState) -> bool,
{
let start = Instant::now();
let mut iterations = 0;
loop {
self.update_state()?;
if condition(&self.state) {
return Ok(());
}
let elapsed = start.elapsed();
if elapsed >= self.timeout {
let current_state = self.state.debug_contents();
let cursor = self.state.cursor_position();
eprintln!("\n=== Timeout waiting for: {} ===", description);
eprintln!("Waited: {:?} ({} iterations)", elapsed, iterations);
eprintln!("Cursor position: row={}, col={}", cursor.0, cursor.1);
eprintln!("Current screen state:\n{}", current_state);
eprintln!("==========================================\n");
return Err(TermTestError::Timeout {
timeout_ms: self.timeout.as_millis() as u64,
});
}
iterations += 1;
std::thread::sleep(self.poll_interval);
}
}
pub fn wait_for_text(&mut self, text: &str) -> Result<()> {
let text = text.to_string();
let description = format!("text '{}'", text);
self.wait_for_with_context(
move |state| state.contains(&text),
&description,
)
}
pub fn wait_for_text_timeout(&mut self, text: &str, timeout: Duration) -> Result<()> {
let text = text.to_string();
let description = format!("text '{}'", text);
let start = Instant::now();
let mut iterations = 0;
loop {
self.update_state()?;
if self.state.contains(&text) {
return Ok(());
}
let elapsed = start.elapsed();
if elapsed >= timeout {
let current_state = self.state.debug_contents();
let cursor = self.state.cursor_position();
eprintln!("\n=== Timeout waiting for: {} ===", description);
eprintln!("Waited: {:?} ({} iterations)", elapsed, iterations);
eprintln!("Cursor position: row={}, col={}", cursor.0, cursor.1);
eprintln!("Current screen state:\n{}", current_state);
eprintln!("==========================================\n");
return Err(TermTestError::Timeout {
timeout_ms: timeout.as_millis() as u64,
});
}
iterations += 1;
std::thread::sleep(self.poll_interval);
}
}
pub fn wait_for_cursor(&mut self, pos: (u16, u16)) -> Result<()> {
let description = format!("cursor at ({}, {})", pos.0, pos.1);
self.wait_for_with_context(
move |state| state.cursor_position() == pos,
&description,
)
}
pub fn wait_for_cursor_timeout(&mut self, pos: (u16, u16), timeout: Duration) -> Result<()> {
let description = format!("cursor at ({}, {})", pos.0, pos.1);
let start = Instant::now();
let mut iterations = 0;
loop {
self.update_state()?;
if self.state.cursor_position() == pos {
return Ok(());
}
let elapsed = start.elapsed();
if elapsed >= timeout {
let current_state = self.state.debug_contents();
let cursor = self.state.cursor_position();
eprintln!("\n=== Timeout waiting for: {} ===", description);
eprintln!("Waited: {:?} ({} iterations)", elapsed, iterations);
eprintln!("Cursor position: row={}, col={}", cursor.0, cursor.1);
eprintln!("Current screen state:\n{}", current_state);
eprintln!("==========================================\n");
return Err(TermTestError::Timeout {
timeout_ms: timeout.as_millis() as u64,
});
}
iterations += 1;
std::thread::sleep(self.poll_interval);
}
}
pub fn screen_contents(&self) -> String {
self.state.contents()
}
pub fn cursor_position(&self) -> (u16, u16) {
self.state.cursor_position()
}
pub fn get_cursor_position(&self) -> (u16, u16) {
self.cursor_position()
}
pub fn state(&self) -> &ScreenState {
&self.state
}
pub fn state_mut(&mut self) -> &mut ScreenState {
&mut self.state
}
pub fn resize(&mut self, width: u16, height: u16) -> Result<()> {
self.terminal.resize(width, height)?;
self.state = ScreenState::new(width, height);
Ok(())
}
pub fn is_running(&mut self) -> bool {
self.terminal.is_running()
}
pub fn wait_exit(&mut self) -> Result<ExitStatus> {
self.terminal.wait()
}
#[cfg(feature = "sixel")]
pub fn sixel_regions(&self) -> &[crate::screen::SixelRegion] {
self.state.sixel_regions()
}
#[cfg(feature = "sixel")]
pub fn sixel_count(&self) -> usize {
self.sixel_regions().len()
}
#[cfg(feature = "sixel")]
pub fn sixel_at(&self, row: u16, col: u16) -> Option<&crate::screen::SixelRegion> {
self.state.sixel_regions()
.iter()
.find(|r| r.start_row == row && r.start_col == col)
}
#[cfg(feature = "sixel")]
pub fn assert_sixel_within_bounds(&self, area: (u16, u16, u16, u16)) -> Result<()> {
use crate::sixel::SixelCapture;
let capture = SixelCapture::from_screen_state(&self.state);
capture.assert_all_within(area)
}
#[cfg(feature = "sixel")]
pub fn has_sixel_in_area(&self, area: (u16, u16, u16, u16)) -> bool {
use crate::sixel::SixelCapture;
let capture = SixelCapture::from_screen_state(&self.state);
!capture.sequences_in_area(area).is_empty()
}
#[cfg(feature = "sixel")]
pub fn verify_sixel_cleared(&mut self) -> Result<bool> {
let before = self.sixel_count();
self.update_state()?;
let after = self.sixel_count();
Ok(after < before)
}
#[cfg(feature = "sixel")]
pub fn assert_preview_has_sixel(&self) -> Result<()> {
let preview_area = (5, 40, 35, 15);
if !self.has_sixel_in_area(preview_area) {
return Err(TermTestError::SixelValidation(
format!(
"No Sixel graphics found in standard preview area {:?}. \
Current Sixel count: {}. \
Regions: {:?}",
preview_area,
self.sixel_count(),
self.sixel_regions()
.iter()
.map(|r| (r.start_row, r.start_col, r.width, r.height))
.collect::<Vec<_>>()
)
));
}
Ok(())
}
#[cfg(feature = "sixel")]
pub fn assert_preview_has_sixel_in(&self, preview_area: (u16, u16, u16, u16)) -> Result<()> {
if !self.has_sixel_in_area(preview_area) {
return Err(TermTestError::SixelValidation(
format!(
"No Sixel graphics found in preview area {:?}. \
Current Sixel count: {}. \
Regions: {:?}",
preview_area,
self.sixel_count(),
self.sixel_regions()
.iter()
.map(|r| (r.start_row, r.start_col, r.width, r.height))
.collect::<Vec<_>>()
)
));
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct TuiTestHarnessBuilder {
width: u16,
height: u16,
timeout: Duration,
poll_interval: Duration,
buffer_size: usize,
}
impl Default for TuiTestHarnessBuilder {
fn default() -> Self {
Self {
width: 80,
height: 24,
timeout: DEFAULT_TIMEOUT,
poll_interval: DEFAULT_POLL_INTERVAL,
buffer_size: DEFAULT_BUFFER_SIZE,
}
}
}
impl TuiTestHarnessBuilder {
pub fn with_size(mut self, width: u16, height: u16) -> Self {
self.width = width;
self.height = height;
self
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn with_poll_interval(mut self, interval: Duration) -> Self {
self.poll_interval = interval;
self
}
pub fn with_buffer_size(mut self, size: usize) -> Self {
self.buffer_size = size;
self
}
pub fn build(self) -> Result<TuiTestHarness> {
let terminal = TestTerminal::new(self.width, self.height)?;
let state = ScreenState::new(self.width, self.height);
Ok(TuiTestHarness {
terminal,
state,
timeout: self.timeout,
poll_interval: self.poll_interval,
buffer_size: self.buffer_size,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_harness() {
let harness = TuiTestHarness::new(80, 24);
assert!(harness.is_ok());
let harness = harness.unwrap();
assert_eq!(harness.timeout, DEFAULT_TIMEOUT);
assert_eq!(harness.poll_interval, DEFAULT_POLL_INTERVAL);
assert_eq!(harness.buffer_size, DEFAULT_BUFFER_SIZE);
}
#[test]
fn test_with_timeout() {
let harness = TuiTestHarness::new(80, 24)
.unwrap()
.with_timeout(Duration::from_secs(10));
assert_eq!(harness.timeout, Duration::from_secs(10));
}
#[test]
fn test_with_poll_interval() {
let harness = TuiTestHarness::new(80, 24)
.unwrap()
.with_poll_interval(Duration::from_millis(50));
assert_eq!(harness.poll_interval, Duration::from_millis(50));
}
#[test]
fn test_builder_default() {
let harness = TuiTestHarness::builder().build();
assert!(harness.is_ok());
let harness = harness.unwrap();
assert_eq!(harness.timeout, DEFAULT_TIMEOUT);
assert_eq!(harness.poll_interval, DEFAULT_POLL_INTERVAL);
assert_eq!(harness.buffer_size, DEFAULT_BUFFER_SIZE);
}
#[test]
fn test_builder_with_size() {
let harness = TuiTestHarness::builder()
.with_size(120, 40)
.build()
.unwrap();
let (width, height) = harness.state.size();
assert_eq!(width, 120);
assert_eq!(height, 40);
}
#[test]
fn test_builder_with_timeout() {
let timeout = Duration::from_secs(15);
let harness = TuiTestHarness::builder()
.with_timeout(timeout)
.build()
.unwrap();
assert_eq!(harness.timeout, timeout);
}
#[test]
fn test_builder_with_poll_interval() {
let interval = Duration::from_millis(25);
let harness = TuiTestHarness::builder()
.with_poll_interval(interval)
.build()
.unwrap();
assert_eq!(harness.poll_interval, interval);
}
#[test]
fn test_builder_with_buffer_size() {
let buffer_size = 8192;
let harness = TuiTestHarness::builder()
.with_buffer_size(buffer_size)
.build()
.unwrap();
assert_eq!(harness.buffer_size, buffer_size);
}
#[test]
fn test_builder_chaining() {
let harness = TuiTestHarness::builder()
.with_size(100, 30)
.with_timeout(Duration::from_secs(20))
.with_poll_interval(Duration::from_millis(75))
.with_buffer_size(16384)
.build()
.unwrap();
assert_eq!(harness.state.size(), (100, 30));
assert_eq!(harness.timeout, Duration::from_secs(20));
assert_eq!(harness.poll_interval, Duration::from_millis(75));
assert_eq!(harness.buffer_size, 16384);
}
#[test]
fn test_cursor_position() {
let harness = TuiTestHarness::new(80, 24).unwrap();
let (row, col) = harness.cursor_position();
assert_eq!(row, 0);
assert_eq!(col, 0);
}
#[test]
fn test_get_cursor_position_alias() {
let harness = TuiTestHarness::new(80, 24).unwrap();
let pos1 = harness.cursor_position();
let pos2 = harness.get_cursor_position();
assert_eq!(pos1, pos2);
}
#[test]
fn test_wait_for_text_helper_exists() {
let harness = TuiTestHarness::new(80, 24).unwrap();
let _: fn(&mut TuiTestHarness, &str) -> Result<()> = TuiTestHarness::wait_for_text;
assert_eq!(harness.cursor_position(), (0, 0));
assert_eq!(harness.get_cursor_position(), (0, 0));
}
#[test]
fn test_state_manipulation() {
let mut harness = TuiTestHarness::new(80, 24).unwrap();
harness.state_mut().feed(b"Test Data");
let contents = harness.screen_contents();
assert!(contents.contains("Test"));
}
#[test]
fn test_cursor_position_tracking() {
let mut harness = TuiTestHarness::new(80, 24).unwrap();
assert_eq!(harness.cursor_position(), (0, 0));
harness.state_mut().feed(b"\x1b[2;5H");
let (row, col) = harness.cursor_position();
assert!(row >= 0); assert!(col >= 0);
}
#[test]
fn test_screen_state_access() {
let harness = TuiTestHarness::new(80, 24).unwrap();
let state = harness.state();
assert_eq!(state.size(), (80, 24));
let contents = harness.screen_contents();
assert!(contents.len() > 0 || contents.is_empty()); }
#[test]
fn test_resize() {
let mut harness = TuiTestHarness::new(80, 24).unwrap();
let result = harness.resize(100, 30);
assert!(result.is_ok());
assert_eq!(harness.state.size(), (100, 30));
}
#[test]
fn test_is_running_no_process() {
let mut harness = TuiTestHarness::new(80, 24).unwrap();
assert!(!harness.is_running());
}
#[test]
fn test_spawn_and_check_running() {
let mut harness = TuiTestHarness::new(80, 24).unwrap();
let mut cmd = CommandBuilder::new("sleep");
cmd.arg("0.1");
let spawn_result = harness.spawn(cmd);
if spawn_result.is_ok() {
assert!(harness.is_running());
std::thread::sleep(Duration::from_millis(200));
assert!(!harness.is_running());
}
}
#[test]
#[ignore] fn test_wait_for_text_success() -> Result<()> {
let mut harness = TuiTestHarness::new(80, 24)?
.with_timeout(Duration::from_secs(2));
let mut cmd = CommandBuilder::new("echo");
cmd.arg("hello world");
harness.spawn(cmd)?;
harness.wait_for_text("hello")?;
assert!(harness.screen_contents().contains("hello"));
Ok(())
}
#[test]
#[ignore] fn test_wait_for_text_timeout() {
let mut harness = TuiTestHarness::new(80, 24)
.unwrap()
.with_timeout(Duration::from_millis(300));
let mut cmd = CommandBuilder::new("sleep");
cmd.arg("10");
harness.spawn(cmd).unwrap();
let result = harness.wait_for_text("never_appears");
assert!(result.is_err());
match result {
Err(TermTestError::Timeout { timeout_ms }) => {
assert_eq!(timeout_ms, 300);
}
_ => panic!("Expected Timeout error"),
}
}
#[test]
#[ignore] fn test_wait_for_text_with_custom_timeout() -> Result<()> {
let mut harness = TuiTestHarness::new(80, 24)?;
let mut cmd = CommandBuilder::new("echo");
cmd.arg("quick test");
harness.spawn(cmd)?;
harness.wait_for_text_timeout("quick", Duration::from_millis(500))?;
assert!(harness.screen_contents().contains("quick"));
Ok(())
}
#[test]
#[ignore] fn test_wait_for_cursor_success() -> Result<()> {
let mut harness = TuiTestHarness::new(80, 24)?;
harness.state_mut().feed(b"\x1b[10;20H");
harness.wait_for_cursor((9, 19))?;
let pos = harness.cursor_position();
assert_eq!(pos, (9, 19));
Ok(())
}
#[test]
#[ignore] fn test_wait_for_cursor_timeout() {
let mut harness = TuiTestHarness::new(80, 24)
.unwrap()
.with_timeout(Duration::from_millis(300));
let result = harness.wait_for_cursor((50, 50));
assert!(result.is_err());
match result {
Err(TermTestError::Timeout { .. }) => {
}
_ => panic!("Expected Timeout error"),
}
}
#[test]
#[ignore] fn test_wait_for_cursor_with_custom_timeout() -> Result<()> {
let mut harness = TuiTestHarness::new(80, 24)?;
harness.state_mut().feed(b"\x1b[5;10H");
harness.wait_for_cursor_timeout((4, 9), Duration::from_millis(500))?;
let pos = harness.cursor_position();
assert_eq!(pos, (4, 9));
Ok(())
}
#[test]
#[ignore] fn test_wait_for_custom_predicate() -> Result<()> {
let mut harness = TuiTestHarness::new(80, 24)?
.with_timeout(Duration::from_secs(2));
let mut cmd = CommandBuilder::new("echo");
cmd.arg("test123");
harness.spawn(cmd)?;
harness.wait_for(|state| {
state.contents().chars().any(|c| c.is_numeric())
})?;
assert!(harness.screen_contents().contains('1'));
Ok(())
}
#[test]
#[ignore] fn test_wait_for_multiline_output() -> Result<()> {
let mut harness = TuiTestHarness::new(80, 24)?
.with_timeout(Duration::from_secs(2));
let mut cmd = CommandBuilder::new("sh");
cmd.arg("-c");
cmd.arg("echo 'line1'; echo 'line2'; echo 'line3'");
harness.spawn(cmd)?;
harness.wait_for(|state| {
let contents = state.contents();
contents.contains("line1") &&
contents.contains("line2") &&
contents.contains("line3")
})?;
let contents = harness.screen_contents();
assert!(contents.contains("line1"));
assert!(contents.contains("line2"));
assert!(contents.contains("line3"));
Ok(())
}
#[test]
#[ignore] fn test_wait_for_complex_predicate() -> Result<()> {
let mut harness = TuiTestHarness::new(80, 24)?
.with_timeout(Duration::from_secs(2));
let mut cmd = CommandBuilder::new("echo");
cmd.arg("Ready: 100%");
harness.spawn(cmd)?;
harness.wait_for(|state| {
let contents = state.contents();
contents.contains("Ready") && contents.contains("%")
})?;
assert!(harness.screen_contents().contains("Ready: 100%"));
Ok(())
}
#[test]
#[ignore] fn test_update_state_multiple_times() -> Result<()> {
let mut harness = TuiTestHarness::new(80, 24)?;
let mut cmd = CommandBuilder::new("echo");
cmd.arg("data");
harness.spawn(cmd)?;
harness.update_state()?;
harness.update_state()?;
harness.update_state()?;
assert!(harness.screen_contents().contains("data"));
Ok(())
}
#[cfg(feature = "sixel")]
#[test]
fn test_sixel_count() -> Result<()> {
let mut harness = TuiTestHarness::new(80, 24)?;
assert_eq!(harness.sixel_count(), 0);
harness.state_mut().feed(b"\x1b[5;10H"); harness.state_mut().feed(b"\x1bPq\"1;1;100;50#0~\x1b\\");
assert_eq!(harness.sixel_count(), 1);
harness.state_mut().feed(b"\x1b[10;20H");
harness.state_mut().feed(b"\x1bPq\"1;1;80;60#0~\x1b\\");
assert_eq!(harness.sixel_count(), 2);
Ok(())
}
#[cfg(feature = "sixel")]
#[test]
fn test_sixel_regions() -> Result<()> {
let mut harness = TuiTestHarness::new(80, 24)?;
harness.state_mut().feed(b"\x1b[5;10H"); harness.state_mut().feed(b"\x1bPq\"1;1;100;50#0~\x1b\\");
let regions = harness.sixel_regions();
assert_eq!(regions.len(), 1);
let region = ®ions[0];
assert_eq!(region.start_row, 4); assert_eq!(region.start_col, 9); assert_eq!(region.width, 100);
assert_eq!(region.height, 50);
Ok(())
}
#[cfg(feature = "sixel")]
#[test]
fn test_sixel_at_position() -> Result<()> {
let mut harness = TuiTestHarness::new(80, 24)?;
harness.state_mut().feed(b"\x1b[5;10H");
harness.state_mut().feed(b"\x1bPq\"1;1;100;50#0~\x1b\\");
let region = harness.sixel_at(4, 9);
assert!(region.is_some());
let region = region.unwrap();
assert_eq!(region.width, 100);
assert_eq!(region.height, 50);
assert!(harness.sixel_at(0, 0).is_none());
assert!(harness.sixel_at(10, 10).is_none());
Ok(())
}
#[cfg(feature = "sixel")]
#[test]
fn test_assert_sixel_within_bounds_success() -> Result<()> {
let mut harness = TuiTestHarness::new(80, 24)?;
harness.state_mut().feed(b"\x1b[5;10H");
harness.state_mut().feed(b"\x1bPq\"1;1;10;10#0~\x1b\\");
let area = (0, 0, 80, 24);
assert!(harness.assert_sixel_within_bounds(area).is_ok());
let area = (3, 8, 20, 15);
assert!(harness.assert_sixel_within_bounds(area).is_ok());
Ok(())
}
#[cfg(feature = "sixel")]
#[test]
fn test_assert_sixel_within_bounds_failure() -> Result<()> {
let mut harness = TuiTestHarness::new(80, 24)?;
harness.state_mut().feed(b"\x1b[5;10H");
harness.state_mut().feed(b"\x1bPq\"1;1;10;10#0~\x1b\\");
let area = (0, 0, 5, 5);
let result = harness.assert_sixel_within_bounds(area);
assert!(result.is_err());
if let Err(crate::error::TermTestError::SixelValidation(msg)) = result {
assert!(msg.contains("outside area"));
} else {
panic!("Expected SixelValidation error");
}
Ok(())
}
#[cfg(feature = "sixel")]
#[test]
fn test_has_sixel_in_area() -> Result<()> {
let mut harness = TuiTestHarness::new(80, 24)?;
let area = (0, 0, 80, 24);
assert!(!harness.has_sixel_in_area(area));
harness.state_mut().feed(b"\x1b[5;10H");
harness.state_mut().feed(b"\x1bPq\"1;1;10;10#0~\x1b\\");
assert!(harness.has_sixel_in_area((0, 0, 80, 24)));
assert!(harness.has_sixel_in_area((0, 0, 20, 20)));
assert!(!harness.has_sixel_in_area((20, 20, 10, 10)));
Ok(())
}
#[cfg(feature = "sixel")]
#[test]
fn test_verify_sixel_cleared() -> Result<()> {
let mut harness = TuiTestHarness::new(80, 24)?;
harness.state_mut().feed(b"\x1b[5;10H");
harness.state_mut().feed(b"\x1bPq\"1;1;100;50#0~\x1b\\");
assert_eq!(harness.sixel_count(), 1);
let new_state = crate::screen::ScreenState::new(80, 24);
*harness.state_mut() = new_state;
let before = harness.sixel_count();
assert_eq!(before, 0);
Ok(())
}
#[cfg(feature = "sixel")]
#[test]
fn test_assert_preview_has_sixel_success() -> Result<()> {
let mut harness = TuiTestHarness::new(80, 24)?;
harness.state_mut().feed(b"\x1b[8;45H"); harness.state_mut().feed(b"\x1bPq\"1;1;100;50#0~\x1b\\");
assert!(harness.assert_preview_has_sixel().is_ok());
Ok(())
}
#[cfg(feature = "sixel")]
#[test]
fn test_assert_preview_has_sixel_failure() -> Result<()> {
let mut harness = TuiTestHarness::new(80, 24)?;
harness.state_mut().feed(b"\x1b[2;2H"); harness.state_mut().feed(b"\x1bPq\"1;1;100;50#0~\x1b\\");
let result = harness.assert_preview_has_sixel();
assert!(result.is_err());
Ok(())
}
#[cfg(feature = "sixel")]
#[test]
fn test_assert_preview_has_sixel_in_custom() -> Result<()> {
let mut harness = TuiTestHarness::new(120, 40)?;
let custom_area = (10, 50, 60, 25);
harness.state_mut().feed(b"\x1b[15;60H"); harness.state_mut().feed(b"\x1bPq\"1;1;40;30#0~\x1b\\");
assert!(harness.assert_preview_has_sixel_in(custom_area).is_ok());
let wrong_area = (0, 0, 20, 20);
assert!(harness.assert_preview_has_sixel_in(wrong_area).is_err());
Ok(())
}
#[cfg(feature = "sixel")]
#[test]
fn test_multiple_sixels_in_area() -> Result<()> {
let mut harness = TuiTestHarness::new(100, 40)?;
let preview_area = (5, 30, 60, 30);
harness.state_mut().feed(b"\x1b[10;40H");
harness.state_mut().feed(b"\x1bPq\"1;1;80;60#0~\x1b\\");
harness.state_mut().feed(b"\x1b[20;50H");
harness.state_mut().feed(b"\x1bPq\"1;1;100;80#0~\x1b\\");
assert_eq!(harness.sixel_count(), 2);
assert!(harness.has_sixel_in_area(preview_area));
Ok(())
}
#[cfg(feature = "sixel")]
#[test]
fn test_sixel_at_screen_edge() -> Result<()> {
let mut harness = TuiTestHarness::new(80, 24)?;
harness.state_mut().feed(b"\x1b[1;1H"); harness.state_mut().feed(b"\x1bPq\"1;1;50;30#0~\x1b\\");
assert_eq!(harness.sixel_count(), 1);
assert!(harness.sixel_at(0, 0).is_some());
let region = harness.sixel_at(0, 0).unwrap();
assert_eq!(region.start_row, 0);
assert_eq!(region.start_col, 0);
Ok(())
}
#[cfg(feature = "sixel")]
#[test]
fn test_empty_sixel_regions() -> Result<()> {
let harness = TuiTestHarness::new(80, 24)?;
assert_eq!(harness.sixel_count(), 0);
assert!(harness.sixel_regions().is_empty());
assert!(harness.sixel_at(0, 0).is_none());
let area = (0, 0, 80, 24);
assert!(harness.assert_sixel_within_bounds(area).is_ok());
assert!(!harness.has_sixel_in_area(area));
Ok(())
}
}