use ratatui::layout::Position;
use crate::error;
use tokio_util::sync::CancellationToken;
use crate::app::{App, BoxedSubscription, Command, Runtime, RuntimeConfig, Subscription};
use crate::backend::CaptureBackend;
use crate::input::{Event, EventQueue};
pub struct AppHarness<A: App> {
runtime: Runtime<A, CaptureBackend>,
}
impl<A: App> AppHarness<A> {
pub fn new(width: u16, height: u16) -> error::Result<Self> {
let runtime = Runtime::virtual_terminal(width, height)?;
Ok(Self { runtime })
}
pub fn with_config(width: u16, height: u16, config: RuntimeConfig) -> error::Result<Self> {
let runtime = Runtime::virtual_terminal_with_config(width, height, config)?;
Ok(Self { runtime })
}
pub fn with_state(
width: u16,
height: u16,
state: A::State,
init_cmd: Command<A::Message>,
) -> error::Result<Self> {
let runtime = Runtime::virtual_terminal_with_state(width, height, state, init_cmd)?;
Ok(Self { runtime })
}
pub fn with_state_and_config(
width: u16,
height: u16,
state: A::State,
init_cmd: Command<A::Message>,
config: RuntimeConfig,
) -> error::Result<Self> {
let runtime = Runtime::virtual_terminal_with_state_and_config(
width, height, state, init_cmd, config,
)?;
Ok(Self { runtime })
}
pub fn state(&self) -> &A::State {
self.runtime.state()
}
pub fn state_mut(&mut self) -> &mut A::State {
self.runtime.state_mut()
}
pub fn screen(&self) -> String {
self.runtime.display()
}
pub fn screen_ansi(&self) -> String {
self.runtime.display_ansi()
}
pub fn cell_at(&self, x: u16, y: u16) -> Option<&crate::backend::EnhancedCell> {
self.runtime.backend().cell(x, y)
}
pub fn snapshot(&self) -> super::Snapshot {
super::Snapshot::new(self.runtime.backend().snapshot(), Default::default())
}
pub fn backend(&self) -> &CaptureBackend {
self.runtime.backend()
}
pub fn backend_mut(&mut self) -> &mut CaptureBackend {
self.runtime.backend_mut()
}
pub fn dispatch(&mut self, msg: A::Message) {
self.runtime.dispatch(msg);
self.runtime.process_pending();
}
pub fn dispatch_all(&mut self, messages: impl IntoIterator<Item = A::Message>) {
for msg in messages {
self.dispatch(msg);
}
}
pub fn message_sender(&self) -> tokio::sync::mpsc::Sender<A::Message> {
self.runtime.message_sender()
}
pub fn subscribe(&mut self, subscription: impl Subscription<A::Message>) {
self.runtime.subscribe(subscription);
}
pub fn subscribe_all(&mut self, subscriptions: Vec<BoxedSubscription<A::Message>>) {
self.runtime.subscribe_all(subscriptions);
}
pub fn events(&mut self) -> &mut EventQueue {
self.runtime.events()
}
pub fn push_event(&mut self, event: Event) {
self.runtime.events().push(event);
}
pub fn type_str(&mut self, s: &str) {
self.runtime.events().type_str(s);
}
pub fn enter(&mut self) {
self.runtime.events().enter();
}
pub fn escape(&mut self) {
self.runtime.events().escape();
}
pub fn tab(&mut self) {
self.runtime.events().tab();
}
pub fn ctrl(&mut self, c: char) {
self.runtime.events().ctrl(c);
}
pub fn click(&mut self, x: u16, y: u16) {
self.runtime.events().click(x, y);
}
pub fn process_events(&mut self) {
self.runtime.process_all_events();
}
pub fn tick(&mut self) -> error::Result<()> {
self.runtime.tick()
}
pub fn run_ticks(&mut self, ticks: usize) -> error::Result<()> {
self.runtime.run_ticks(ticks)
}
pub fn render(&mut self) -> error::Result<()> {
self.runtime.render()
}
pub fn should_quit(&self) -> bool {
self.runtime.should_quit()
}
pub fn quit(&mut self) {
self.runtime.quit();
}
pub fn cancellation_token(&self) -> CancellationToken {
self.runtime.cancellation_token()
}
pub fn contains_text(&self, needle: &str) -> bool {
self.runtime.contains_text(needle)
}
pub fn find_text(&self, needle: &str) -> Vec<Position> {
self.runtime.find_text(needle)
}
pub fn row(&self, y: u16) -> String {
self.runtime.backend().row_content(y)
}
pub fn assert_contains(&self, needle: &str) {
if !self.contains_text(needle) {
panic!(
"Expected screen to contain '{}', but it was not found.\n\nScreen:\n{}",
needle,
self.screen()
);
}
}
pub fn assert_not_contains(&self, needle: &str) {
if self.contains_text(needle) {
panic!(
"Expected screen to NOT contain '{}', but it was found.\n\nScreen:\n{}",
needle,
self.screen()
);
}
}
}
#[cfg(any(test, feature = "test-utils"))]
use std::time::Duration;
#[cfg(any(test, feature = "test-utils"))]
impl<A: App> AppHarness<A> {
pub async fn advance_time(&mut self, duration: Duration) {
let step = Duration::from_millis(10);
let mut remaining = duration;
while remaining > Duration::ZERO {
let advance_by = remaining.min(step);
tokio::time::advance(advance_by).await;
tokio::time::sleep(Duration::ZERO).await;
tokio::task::yield_now().await;
remaining = remaining.saturating_sub(advance_by);
}
self.runtime.process_pending();
}
pub async fn sleep(&mut self, duration: Duration) {
self.advance_time(duration).await;
}
pub async fn wait_for<F>(&mut self, condition: F, timeout: Duration) -> bool
where
F: Fn(&A::State) -> bool,
{
let step = Duration::from_millis(10);
let mut elapsed = Duration::ZERO;
while elapsed < timeout {
if condition(self.runtime.state()) {
return true;
}
self.advance_time(step).await;
elapsed += step;
}
condition(self.runtime.state())
}
pub async fn wait_for_text(&mut self, needle: &str, timeout: Duration) -> bool {
let step = Duration::from_millis(10);
let mut elapsed = Duration::ZERO;
while elapsed < timeout {
self.runtime.render().ok();
if self.contains_text(needle) {
return true;
}
self.advance_time(step).await;
elapsed += step;
}
self.runtime.render().ok();
self.contains_text(needle)
}
}
#[cfg(test)]
mod tests;