use ftui::Frame;
use ftui_core::geometry::Rect;
use ftui_render::buffer::Buffer;
use ftui_render::cell::Cell;
use ftui_render::grapheme_pool::GraphemePool;
use tracing::{debug, info};
pub fn create_test_buffer(width: u16, height: u16) -> (Buffer, GraphemePool) {
debug!("TEST HARNESS: creating test buffer {}x{}", width, height);
(Buffer::new(width, height), GraphemePool::new())
}
pub fn buffer_to_string(buffer: &Buffer, pool: &GraphemePool) -> String {
let mut out = String::new();
let width = buffer.width();
let height = buffer.height();
for y in 0..height {
for x in 0..width {
if let Some(cell) = buffer.get(x, y) {
if cell.content.is_continuation() {
continue;
}
if cell.content.is_empty() {
out.push(' ');
} else if let Some(ch) = cell.content.as_char() {
out.push(ch);
} else if let Some(id) = cell.content.grapheme_id() {
if let Some(s) = pool.get(id) {
out.push_str(s);
} else {
out.push(' ');
}
} else {
out.push(' ');
}
} else {
out.push(' ');
}
}
out.push('\n');
}
out
}
fn set_string_in_buffer(buffer: &mut Buffer, x: u16, y: u16, s: &str) {
for (i, ch) in s.chars().enumerate() {
buffer.set(x + i as u16, y, Cell::from_char(ch));
}
}
pub fn render_to_string<F>(width: u16, height: u16, mut draw: F) -> String
where
F: FnMut(&mut Frame),
{
debug!("TEST HARNESS: render_to_string {}x{}", width, height);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(width, height, &mut pool);
draw(&mut frame);
buffer_to_string(&frame.buffer, &pool)
}
pub fn render_to_area<F>(area: Rect, mut draw: F) -> String
where
F: FnMut(&mut Frame, Rect),
{
debug!(
"TEST HARNESS: render_to_area x={} y={} w={} h={}",
area.x, area.y, area.width, area.height
);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(area.width, area.height, &mut pool);
let normalized = Rect::new(0, 0, area.width, area.height);
draw(&mut frame, normalized);
buffer_to_string(&frame.buffer, &pool)
}
pub fn assert_rendered_contains(content: &str, expected: &str) {
if !content.contains(expected) {
info!(
"ASSERT FAILED: expected '{}' not found in rendered content",
expected
);
info!("CONTENT:\n{}", content);
panic!(
"Expected rendered content to contain '{}' but it was not found",
expected
);
}
debug!("ASSERT PASS: found '{}' in rendered content", expected);
}
pub fn assert_rendered_not_contains(content: &str, unexpected: &str) {
if content.contains(unexpected) {
info!(
"ASSERT FAILED: unexpected '{}' found in rendered content",
unexpected
);
info!("CONTENT:\n{}", content);
panic!(
"Expected rendered content to NOT contain '{}' but it was found",
unexpected
);
}
debug!(
"ASSERT PASS: '{}' correctly absent from rendered content",
unexpected
);
}
pub fn assert_rendered_contains_all(content: &str, expected: &[&str]) {
for exp in expected {
assert_rendered_contains(content, exp);
}
}
pub fn get_test_terminal_size() -> (u16, u16) {
let width = std::env::var("COLUMNS")
.ok()
.and_then(|v| v.parse::<u16>().ok())
.unwrap_or(80);
let height = std::env::var("LINES")
.ok()
.and_then(|v| v.parse::<u16>().ok())
.unwrap_or(24);
(width.max(40), height.max(12))
}
pub fn init_test_logging() {
let _ = tracing_subscriber::fmt()
.with_test_writer()
.with_max_level(tracing::Level::DEBUG)
.try_init();
}
pub fn snapshot_state(state: &super::TuiState, width: u16, height: u16) -> String {
render_to_string(width, height, |f| super::widgets::render(f, state))
}
#[cfg(test)]
mod tests {
use super::*;
use ftui_widgets::Widget;
use ftui_widgets::block::Block;
use ftui_widgets::borders::Borders;
use tracing::info;
#[test]
fn test_create_test_buffer_default_size() {
init_test_logging();
info!("TEST START: test_create_test_buffer_default_size");
let (buffer, _pool) = create_test_buffer(80, 24);
assert_eq!(buffer.width(), 80);
assert_eq!(buffer.height(), 24);
info!("TEST PASS: test_create_test_buffer_default_size");
}
#[test]
fn test_create_test_buffer_custom_size() {
init_test_logging();
info!("TEST START: test_create_test_buffer_custom_size");
let (buffer, _pool) = create_test_buffer(120, 40);
assert_eq!(buffer.width(), 120);
assert_eq!(buffer.height(), 40);
info!("TEST PASS: test_create_test_buffer_custom_size");
}
#[test]
fn test_create_test_buffer_minimum_size() {
init_test_logging();
info!("TEST START: test_create_test_buffer_minimum_size");
let (buffer, _pool) = create_test_buffer(1, 1);
assert_eq!(buffer.width(), 1);
assert_eq!(buffer.height(), 1);
info!("TEST PASS: test_create_test_buffer_minimum_size");
}
#[test]
fn test_buffer_to_string_empty() {
init_test_logging();
info!("TEST START: test_buffer_to_string_empty");
let (buffer, pool) = create_test_buffer(5, 2);
let result = buffer_to_string(&buffer, &pool);
info!("VERIFY: buffer lines count = {}", result.lines().count());
assert_eq!(result.lines().count(), 2);
assert!(result.lines().all(|l| l.len() == 5));
info!("TEST PASS: test_buffer_to_string_empty");
}
#[test]
fn test_buffer_to_string_with_content() {
init_test_logging();
info!("TEST START: test_buffer_to_string_with_content");
let (mut buffer, pool) = create_test_buffer(10, 1);
set_string_in_buffer(&mut buffer, 0, 0, "Hello");
let result = buffer_to_string(&buffer, &pool);
info!("VERIFY: buffer contains Hello: {}", result.trim());
assert!(result.contains("Hello"));
info!("TEST PASS: test_buffer_to_string_with_content");
}
#[test]
fn test_buffer_to_string_multiline() {
init_test_logging();
info!("TEST START: test_buffer_to_string_multiline");
let (mut buffer, pool) = create_test_buffer(10, 3);
set_string_in_buffer(&mut buffer, 0, 0, "Line 1");
set_string_in_buffer(&mut buffer, 0, 1, "Line 2");
set_string_in_buffer(&mut buffer, 0, 2, "Line 3");
let result = buffer_to_string(&buffer, &pool);
info!("VERIFY: buffer has 3 lines");
assert!(result.contains("Line 1"));
assert!(result.contains("Line 2"));
assert!(result.contains("Line 3"));
assert_eq!(result.lines().count(), 3);
info!("TEST PASS: test_buffer_to_string_multiline");
}
#[test]
fn test_render_to_string_basic() {
init_test_logging();
info!("TEST START: test_render_to_string_basic");
let content = render_to_string(20, 5, |f| {
let block = Block::new().title("Test").borders(Borders::ALL);
block.render(f.bounds(), f);
});
info!("VERIFY: rendered content contains title");
assert!(content.contains("Test"));
info!("TEST PASS: test_render_to_string_basic");
}
#[test]
fn test_render_to_string_at_various_sizes() {
init_test_logging();
info!("TEST START: test_render_to_string_at_various_sizes");
for (w, h) in [(40, 12), (80, 24), (120, 40)] {
let content = render_to_string(w, h, |f| {
let block = Block::new().title("Size Test").borders(Borders::ALL);
block.render(f.bounds(), f);
});
info!(
"VERIFY: render {}x{} produces {} lines",
w,
h,
content.lines().count()
);
assert!(content.lines().count() >= h as usize);
}
info!("TEST PASS: test_render_to_string_at_various_sizes");
}
#[test]
fn test_render_to_area() {
init_test_logging();
info!("TEST START: test_render_to_area");
let area = Rect::new(0, 0, 30, 5);
let content = render_to_area(area, |f, rect| {
let block = Block::new().title("Area").borders(Borders::ALL);
block.render(rect, f);
});
info!("VERIFY: rendered area contains title");
assert!(content.contains("Area"));
info!("TEST PASS: test_render_to_area");
}
#[test]
fn test_assert_rendered_contains_pass() {
init_test_logging();
info!("TEST START: test_assert_rendered_contains_pass");
let content = "Hello World";
assert_rendered_contains(content, "Hello");
assert_rendered_contains(content, "World");
assert_rendered_contains(content, "llo Wo");
info!("TEST PASS: test_assert_rendered_contains_pass");
}
#[test]
#[should_panic(expected = "Expected rendered content to contain")]
fn test_assert_rendered_contains_fail() {
init_test_logging();
info!("TEST START: test_assert_rendered_contains_fail");
let content = "Hello World";
assert_rendered_contains(content, "Goodbye");
}
#[test]
fn test_assert_rendered_not_contains_pass() {
init_test_logging();
info!("TEST START: test_assert_rendered_not_contains_pass");
let content = "Hello World";
assert_rendered_not_contains(content, "Goodbye");
assert_rendered_not_contains(content, "xyz");
info!("TEST PASS: test_assert_rendered_not_contains_pass");
}
#[test]
#[should_panic(expected = "Expected rendered content to NOT contain")]
fn test_assert_rendered_not_contains_fail() {
init_test_logging();
info!("TEST START: test_assert_rendered_not_contains_fail");
let content = "Hello World";
assert_rendered_not_contains(content, "Hello");
}
#[test]
fn test_assert_rendered_contains_all_pass() {
init_test_logging();
info!("TEST START: test_assert_rendered_contains_all_pass");
let content = "Hello World Foo Bar";
assert_rendered_contains_all(content, &["Hello", "World", "Foo", "Bar"]);
info!("TEST PASS: test_assert_rendered_contains_all_pass");
}
#[test]
#[should_panic(expected = "Expected rendered content to contain")]
fn test_assert_rendered_contains_all_fail() {
init_test_logging();
info!("TEST START: test_assert_rendered_contains_all_fail");
let content = "Hello World";
assert_rendered_contains_all(content, &["Hello", "Missing"]);
}
#[test]
fn test_get_test_terminal_size_defaults() {
init_test_logging();
info!("TEST START: test_get_test_terminal_size_defaults");
let (w, h) = get_test_terminal_size();
info!("VERIFY: terminal size w={} h={}", w, h);
assert!(w >= 40);
assert!(h >= 12);
info!("TEST PASS: test_get_test_terminal_size_defaults");
}
#[test]
fn test_init_test_logging_idempotent() {
init_test_logging();
init_test_logging();
init_test_logging();
info!("TEST PASS: test_init_test_logging_idempotent");
}
}