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::view::{ButtonNode, CheckboxNode, ListNode, TextInputNode, TextNode, View};
use std::rc::Rc;
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());
}
_ => {}
}
}
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);
}
}
_ => {}
}
}
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::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);
};
}