use crate::buffer::Buffer;
use crate::component::Component;
use crate::focus::FocusManager;
use crate::render::{render_view, RenderContext};
use crate::scope::{Scope, StateStorage};
use crate::terminal::Terminal;
use crate::view::{ButtonNode, CheckboxNode, ListNode, TextInputNode, TextNode, View};
use crate::EventSource;
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use std::cell::RefCell;
use std::collections::VecDeque;
use std::rc::Rc;
use std::time::Duration;
pub struct TestApp<C: Component> {
root: C,
storage: Rc<StateStorage>,
focus: FocusManager,
width: u16,
height: u16,
}
impl<C: Component> TestApp<C> {
pub fn new(root: C) -> Self {
Self {
root,
storage: Rc::new(StateStorage::new()),
focus: FocusManager::new(),
width: 80,
height: 24,
}
}
pub fn with_size(mut self, width: u16, height: u16) -> Self {
self.width = width;
self.height = height;
self
}
fn render(&self) -> View {
let cx = Scope::with_storage(Rc::clone(&self.storage));
self.root.render(cx)
}
pub fn render_to_string(&mut self) -> String {
let view = self.render();
self.focus.collect_focusables(&view);
let mut buffer = Buffer::new(self.width, self.height);
let area = buffer.rect();
let scroll_offsets: Vec<(u16, u16)> = (0..self.focus.focus_index() + 10)
.map(|i| self.focus.scroll_offset(i))
.collect();
let cursor_offsets: Vec<usize> = (0..self.focus.focus_index() + 10)
.map(|i| self.focus.cursor_offset(i))
.collect();
let mut ctx = RenderContext::new(self.focus.focus_index(), true, scroll_offsets, cursor_offsets, area);
render_view(&mut buffer, &view, area, &mut ctx);
ctx.render_pending_dropdowns(&mut buffer);
self.storage.flush_effects();
buffer.to_string()
}
pub fn find_all_text(&self) -> Vec<String> {
let view = self.render();
let mut texts = Vec::new();
Self::collect_text(&view, &mut texts);
texts
}
pub fn find_text(&self, needle: &str) -> Option<String> {
self.find_all_text()
.into_iter()
.find(|t| t.contains(needle))
}
pub fn has_text(&self, needle: &str) -> bool {
self.find_text(needle).is_some()
}
pub fn find_all_buttons(&self) -> Vec<String> {
let view = self.render();
let mut buttons = Vec::new();
Self::collect_buttons(&view, &mut buttons);
buttons
}
pub fn find_button(&self, label: &str) -> Option<String> {
self.find_all_buttons().into_iter().find(|l| l == label)
}
pub fn focus_index(&self) -> usize {
self.focus.focus_index()
}
pub fn focusable_count(&mut self) -> usize {
let view = self.render();
self.focus.collect_focusables(&view);
self.focus.focusable_count()
}
pub fn focus_next(&mut self) {
let view = self.render();
self.focus.collect_focusables(&view);
self.focus.focus_next();
}
pub fn focus_prev(&mut self) {
let view = self.render();
self.focus.collect_focusables(&view);
self.focus.focus_prev();
}
pub fn activate(&mut self) {
let view = self.render();
self.focus.collect_focusables(&view);
self.focus.activate();
}
pub fn press_button(&mut self, label: &str) -> bool {
let view = self.render();
self.focus.collect_focusables(&view);
if let Some(idx) = self.find_button_index(&view, label) {
while self.focus.focus_index() != idx {
self.focus.focus_next();
}
self.focus.activate();
true
} else {
false
}
}
pub fn list_up(&mut self) {
let view = self.render();
self.focus.collect_focusables(&view);
self.focus.list_select_prev();
}
pub fn list_down(&mut self) {
let view = self.render();
self.focus.collect_focusables(&view);
self.focus.list_select_next();
}
pub fn type_char(&mut self, c: char) {
let view = self.render();
self.focus.collect_focusables(&view);
self.focus
.set_default_textarea_wrap_width(self.width.saturating_sub(4));
if self.focus.is_focused_text_area() {
self.focus.text_area_key(c);
} else {
self.focus.text_input_key(c);
}
}
pub fn type_str(&mut self, s: &str) {
for c in s.chars() {
self.type_char(c);
}
}
pub fn backspace(&mut self) {
let view = self.render();
self.focus.collect_focusables(&view);
if self.focus.is_focused_text_area() {
self.focus.text_area_backspace();
} else {
self.focus.text_input_backspace();
}
}
pub fn enter(&mut self) {
let view = self.render();
self.focus.collect_focusables(&view);
if self.focus.is_focused_text_area() {
self.focus.text_area_enter();
}
}
pub fn scroll_up(&mut self, amount: u16) {
let view = self.render();
self.focus.collect_focusables(&view);
self.focus.scroll_up(amount);
}
pub fn scroll_down(&mut self, amount: u16) {
let view = self.render();
self.focus.collect_focusables(&view);
self.focus.scroll_down(amount, 100);
}
fn collect_text(view: &View, texts: &mut Vec<String>) {
match view {
View::Text(TextNode { content, .. }) => {
texts.push(content.clone());
}
View::VStack(node) => {
for child in &node.children {
Self::collect_text(child, texts);
}
}
View::HStack(node) => {
for child in &node.children {
Self::collect_text(child, texts);
}
}
View::Box(node) => {
if let Some(child) = &node.child {
Self::collect_text(child, texts);
}
}
View::Button(ButtonNode { label, .. }) => {
texts.push(label.clone());
}
View::List(ListNode { items, .. }) => {
texts.extend(items.clone());
}
View::TextInput(TextInputNode {
value, placeholder, ..
}) => {
if value.is_empty() {
texts.push(placeholder.clone());
} else {
texts.push(value.clone());
}
}
View::Checkbox(CheckboxNode { label, .. }) => {
texts.push(label.clone());
}
View::ErrorBoundary(node) => {
Self::collect_text(&node.child, texts);
}
_ => {}
}
}
fn collect_buttons(view: &View, buttons: &mut Vec<String>) {
match view {
View::Button(ButtonNode { label, .. }) => {
buttons.push(label.clone());
}
View::VStack(node) => {
for child in &node.children {
Self::collect_buttons(child, buttons);
}
}
View::HStack(node) => {
for child in &node.children {
Self::collect_buttons(child, buttons);
}
}
View::Box(node) => {
if let Some(child) = &node.child {
Self::collect_buttons(child, buttons);
}
}
View::ErrorBoundary(node) => {
Self::collect_buttons(&node.child, buttons);
}
_ => {}
}
}
fn find_button_index(&self, view: &View, label: &str) -> Option<usize> {
let mut index = 0;
Self::find_button_index_recursive(view, label, &mut index)
}
fn find_button_index_recursive(view: &View, label: &str, index: &mut usize) -> Option<usize> {
match view {
View::Button(ButtonNode {
label: btn_label, ..
}) => {
if btn_label == label {
Some(*index)
} else {
*index += 1;
None
}
}
View::Box(node) => {
if node.scroll {
*index += 1;
}
if let Some(child) = &node.child {
Self::find_button_index_recursive(child, label, index)
} else {
None
}
}
View::VStack(node) => {
for child in &node.children {
if let Some(idx) = Self::find_button_index_recursive(child, label, index) {
return Some(idx);
}
}
None
}
View::HStack(node) => {
for child in &node.children {
if let Some(idx) = Self::find_button_index_recursive(child, label, index) {
return Some(idx);
}
}
None
}
View::ErrorBoundary(node) => {
Self::find_button_index_recursive(&node.child, label, index)
}
View::List(_) | View::TextInput(_) | View::Checkbox(_) => {
*index += 1;
None
}
_ => None,
}
}
pub fn assert_visible(&mut self, needle: &str) {
let rendered = self.render_to_string();
if !rendered.contains(needle) {
panic!(
"\n\nassertion failed: expected {:?} to be visible\n\nRendered output ({}x{}):\n{}\n",
needle, self.width, self.height, rendered
);
}
}
pub fn assert_not_visible(&mut self, needle: &str) {
let rendered = self.render_to_string();
if rendered.contains(needle) {
panic!(
"\n\nassertion failed: expected {:?} to NOT be visible\n\nRendered output ({}x{}):\n{}\n",
needle, self.width, self.height, rendered
);
}
}
pub fn visible_items(&mut self, items: &[&str]) -> Vec<String> {
let rendered = self.render_to_string();
items
.iter()
.filter(|item| rendered.contains(*item))
.map(|s| s.to_string())
.collect()
}
pub fn rendered_lines(&mut self) -> Vec<String> {
self.render_to_string()
.lines()
.map(|s| s.to_string())
.collect()
}
pub fn find_line_containing(&mut self, needle: &str) -> Option<usize> {
self.rendered_lines()
.iter()
.position(|line| line.contains(needle))
}
pub fn viewport_height(&self) -> u16 {
self.height
}
pub fn viewport_width(&self) -> u16 {
self.width
}
}
#[macro_export]
macro_rules! assert_snapshot {
($app:expr) => {
let rendered = $app.render_to_string();
println!("Snapshot:\n{}", rendered);
};
($app:expr, $name:expr) => {
let rendered = $app.render_to_string();
println!("Snapshot [{}]:\n{}", $name, rendered);
};
}
pub struct TestEventSource {
events: RefCell<VecDeque<Event>>,
exhausted: RefCell<bool>,
last_buffer: RefCell<String>,
}
impl TestEventSource {
pub fn new(events: Vec<Event>) -> Self {
Self {
events: RefCell::new(events.into()),
exhausted: RefCell::new(false),
last_buffer: RefCell::new(String::new()),
}
}
pub fn last_buffer(&self) -> String {
self.last_buffer.borrow().clone()
}
}
impl EventSource for TestEventSource {
fn poll_event(&self, _timeout: Duration) -> std::io::Result<Option<Event>> {
let mut events = self.events.borrow_mut();
if let Some(event) = events.pop_front() {
Ok(Some(event))
} else if !*self.exhausted.borrow() {
*self.exhausted.borrow_mut() = true;
Ok(Some(Event::Key(KeyEvent::new(
KeyCode::Char('q'),
KeyModifiers::CONTROL,
))))
} else {
Ok(None)
}
}
fn on_frame_rendered(&self, terminal: &Terminal) {
*self.last_buffer.borrow_mut() = terminal.buffer_string();
}
}
pub struct StreamTestEventSource {
deadline: std::time::Instant,
exhausted: RefCell<bool>,
frames: RefCell<Vec<String>>,
}
impl StreamTestEventSource {
pub fn new(duration: Duration) -> Self {
Self {
deadline: std::time::Instant::now() + duration,
exhausted: RefCell::new(false),
frames: RefCell::new(Vec::new()),
}
}
pub fn frames(&self) -> Vec<String> {
self.frames.borrow().clone()
}
}
impl EventSource for StreamTestEventSource {
fn poll_event(&self, timeout: Duration) -> std::io::Result<Option<Event>> {
if std::time::Instant::now() >= self.deadline {
if !*self.exhausted.borrow() {
*self.exhausted.borrow_mut() = true;
return Ok(Some(Event::Key(KeyEvent::new(
KeyCode::Char('q'),
KeyModifiers::CONTROL,
))));
}
return Ok(None);
}
std::thread::sleep(timeout);
Ok(None)
}
fn on_frame_rendered(&self, terminal: &Terminal) {
self.frames.borrow_mut().push(terminal.buffer_string());
}
}