161 lines
4.0 KiB
Rust
161 lines
4.0 KiB
Rust
#[cfg(test)]
|
|
#[allow(dead_code)]
|
|
pub mod test_utils {
|
|
use chrono::DateTime;
|
|
use ratatui::Frame;
|
|
use ratatui::Terminal;
|
|
use ratatui::backend::TestBackend;
|
|
use ratatui::buffer::Buffer;
|
|
use std::cell::Cell;
|
|
|
|
use crate::app::App;
|
|
|
|
thread_local! {
|
|
static TIMESTAMP: Cell<i64> = const { Cell::new(0) };
|
|
}
|
|
|
|
pub struct Utc;
|
|
|
|
impl Utc {
|
|
pub fn now() -> DateTime<chrono::Utc> {
|
|
TIMESTAMP
|
|
.with(|timestamp| {
|
|
let ts = timestamp.get();
|
|
DateTime::<chrono::Utc>::from_timestamp(if ts != 0 { ts } else { 1684618200 }, 0)
|
|
})
|
|
.expect("a valid timestamp set")
|
|
}
|
|
}
|
|
|
|
pub fn set_timestamp(timestamp: i64) {
|
|
TIMESTAMP.set(timestamp / 1000);
|
|
}
|
|
|
|
pub fn create_test_backend(width: u16, height: u16) -> TestBackend {
|
|
TestBackend::new(width, height)
|
|
}
|
|
|
|
pub fn create_test_terminal(width: u16, height: u16) -> Terminal<TestBackend> {
|
|
let backend = create_test_backend(width, height);
|
|
Terminal::new(backend).unwrap()
|
|
}
|
|
|
|
pub enum TerminalSize {
|
|
Small,
|
|
Medium,
|
|
Large,
|
|
}
|
|
|
|
impl TerminalSize {
|
|
pub fn to_cartesian(&self) -> (u16, u16) {
|
|
match self {
|
|
Self::Small => (80, 30),
|
|
Self::Medium => (120, 40),
|
|
Self::Large => (165, 50),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Renders a UI component and returns the output as a string
|
|
///
|
|
/// # Arguments
|
|
/// * `size` - Terminal T-shirt size (Small, Medium, or Large)
|
|
/// * `render_fn` - Function that renders to the frame
|
|
///
|
|
/// # Example
|
|
/// ```rust
|
|
/// let output = render_to_string(TerminalSize::Medium, |f| {
|
|
/// Block::default().title("Test").render(f.area(), f.buffer_mut());
|
|
/// });
|
|
/// ```
|
|
pub fn render_to_string<F>(size: TerminalSize, mut render_fn: F) -> String
|
|
where
|
|
F: FnMut(&mut Frame),
|
|
{
|
|
let (width, height) = size.to_cartesian();
|
|
let mut terminal = create_test_terminal(width, height);
|
|
|
|
terminal
|
|
.draw(|f| {
|
|
render_fn(f);
|
|
})
|
|
.unwrap();
|
|
|
|
buffer_to_string(terminal.backend().buffer(), width, height)
|
|
}
|
|
|
|
/// Renders a UI component with an App instance and returns the output as a string
|
|
///
|
|
/// This is the primary helper for testing UI components that need app state.
|
|
///
|
|
/// # Arguments
|
|
/// * `size` - Terminal T-shirt size (Small, Medium, or Large)
|
|
/// * `app` - Mutable reference to App instance
|
|
/// * `render_fn` - Function that renders to the frame with app
|
|
///
|
|
/// # Example
|
|
/// ```rust
|
|
/// let mut app = App::test_default();
|
|
/// app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
|
|
///
|
|
/// let output = render_to_string_with_app(TerminalSize::Medium, &mut app, |f, app| {
|
|
/// LibraryUi::draw(f, app, f.area());
|
|
/// });
|
|
///
|
|
/// insta::assert_snapshot!(output);
|
|
/// ```
|
|
pub fn render_to_string_with_app<F>(size: TerminalSize, app: &mut App, mut render_fn: F) -> String
|
|
where
|
|
F: FnMut(&mut Frame, &mut App),
|
|
{
|
|
let (width, height) = size.to_cartesian();
|
|
let mut terminal = create_test_terminal(width, height);
|
|
|
|
terminal
|
|
.draw(|f| {
|
|
render_fn(f, app);
|
|
})
|
|
.unwrap();
|
|
|
|
buffer_to_string(terminal.backend().buffer(), width, height)
|
|
}
|
|
|
|
fn buffer_to_string(buffer: &Buffer, width: u16, height: u16) -> String {
|
|
let mut result = String::new();
|
|
|
|
for y in 0..height {
|
|
for x in 0..width {
|
|
let cell = buffer.cell((x, y)).expect("Cell should exist");
|
|
result.push_str(cell.symbol());
|
|
}
|
|
if y < height - 1 {
|
|
result.push('\n');
|
|
}
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
pub fn output_contains(output: &str, text: &str) -> bool {
|
|
output.contains(text)
|
|
}
|
|
|
|
pub fn output_contains_all(output: &str, texts: &[&str]) -> bool {
|
|
texts.iter().all(|text| output.contains(text))
|
|
}
|
|
|
|
pub fn count_lines(output: &str) -> usize {
|
|
output.lines().count()
|
|
}
|
|
|
|
pub fn verify_dimensions(output: &str, max_width: usize, max_height: usize) -> bool {
|
|
let lines: Vec<&str> = output.lines().collect();
|
|
|
|
if lines.len() > max_height {
|
|
return false;
|
|
}
|
|
|
|
lines.iter().all(|line| line.chars().count() <= max_width)
|
|
}
|
|
}
|