1use crate::buffer::Buffer;
28use crate::component::Component;
29use crate::focus::FocusManager;
30use crate::render::{render_view, RenderContext};
31use crate::scope::{Scope, StateStorage};
32use crate::view::{ButtonNode, CheckboxNode, ListNode, TextInputNode, TextNode, View};
33use std::rc::Rc;
34
35pub struct TestApp<C: Component> {
40 root: C,
41 storage: Rc<StateStorage>,
42 focus: FocusManager,
43 width: u16,
44 height: u16,
45}
46
47impl<C: Component> TestApp<C> {
48 pub fn new(root: C) -> Self {
50 Self {
51 root,
52 storage: Rc::new(StateStorage::new()),
53 focus: FocusManager::new(),
54 width: 80,
55 height: 24,
56 }
57 }
58
59 pub fn with_size(mut self, width: u16, height: u16) -> Self {
61 self.width = width;
62 self.height = height;
63 self
64 }
65
66 fn render(&self) -> View {
68 let cx = Scope::with_storage(Rc::clone(&self.storage));
69 self.root.render(cx)
70 }
71
72 pub fn render_to_string(&mut self) -> String {
74 let view = self.render();
75 self.focus.collect_focusables(&view);
76
77 let mut buffer = Buffer::new(self.width, self.height);
78 let area = buffer.rect();
79
80 let scroll_offsets: Vec<(u16, u16)> = (0..self.focus.focus_index() + 10)
81 .map(|i| self.focus.scroll_offset(i))
82 .collect();
83 let cursor_offsets: Vec<usize> = (0..self.focus.focus_index() + 10)
84 .map(|i| self.focus.cursor_offset(i))
85 .collect();
86
87 let mut ctx = RenderContext::new(self.focus.focus_index(), true, scroll_offsets, cursor_offsets, area);
89 render_view(&mut buffer, &view, area, &mut ctx);
90 ctx.render_pending_dropdowns(&mut buffer);
91
92 self.storage.flush_effects();
94
95 buffer.to_string()
96 }
97
98 pub fn find_all_text(&self) -> Vec<String> {
100 let view = self.render();
101 let mut texts = Vec::new();
102 Self::collect_text(&view, &mut texts);
103 texts
104 }
105
106 pub fn find_text(&self, needle: &str) -> Option<String> {
108 self.find_all_text()
109 .into_iter()
110 .find(|t| t.contains(needle))
111 }
112
113 pub fn has_text(&self, needle: &str) -> bool {
115 self.find_text(needle).is_some()
116 }
117
118 pub fn find_all_buttons(&self) -> Vec<String> {
120 let view = self.render();
121 let mut buttons = Vec::new();
122 Self::collect_buttons(&view, &mut buttons);
123 buttons
124 }
125
126 pub fn find_button(&self, label: &str) -> Option<String> {
128 self.find_all_buttons().into_iter().find(|l| l == label)
129 }
130
131 pub fn focus_index(&self) -> usize {
133 self.focus.focus_index()
134 }
135
136 pub fn focusable_count(&mut self) -> usize {
138 let view = self.render();
139 self.focus.collect_focusables(&view);
140 self.focus.focusable_count()
141 }
142
143 pub fn focus_next(&mut self) {
145 let view = self.render();
146 self.focus.collect_focusables(&view);
147 self.focus.focus_next();
148 }
149
150 pub fn focus_prev(&mut self) {
152 let view = self.render();
153 self.focus.collect_focusables(&view);
154 self.focus.focus_prev();
155 }
156
157 pub fn activate(&mut self) {
159 let view = self.render();
160 self.focus.collect_focusables(&view);
161 self.focus.activate();
162 }
163
164 pub fn press_button(&mut self, label: &str) -> bool {
168 let view = self.render();
169 self.focus.collect_focusables(&view);
170
171 if let Some(idx) = self.find_button_index(&view, label) {
173 while self.focus.focus_index() != idx {
175 self.focus.focus_next();
176 }
177 self.focus.activate();
179 true
180 } else {
181 false
182 }
183 }
184
185 pub fn list_up(&mut self) {
187 let view = self.render();
188 self.focus.collect_focusables(&view);
189 self.focus.list_select_prev();
190 }
191
192 pub fn list_down(&mut self) {
194 let view = self.render();
195 self.focus.collect_focusables(&view);
196 self.focus.list_select_next();
197 }
198
199 pub fn type_char(&mut self, c: char) {
201 let view = self.render();
202 self.focus.collect_focusables(&view);
203 self.focus
205 .set_default_textarea_wrap_width(self.width.saturating_sub(4));
206 if self.focus.is_focused_text_area() {
207 self.focus.text_area_key(c);
208 } else {
209 self.focus.text_input_key(c);
210 }
211 }
212
213 pub fn type_str(&mut self, s: &str) {
215 for c in s.chars() {
216 self.type_char(c);
217 }
218 }
219
220 pub fn backspace(&mut self) {
222 let view = self.render();
223 self.focus.collect_focusables(&view);
224 if self.focus.is_focused_text_area() {
225 self.focus.text_area_backspace();
226 } else {
227 self.focus.text_input_backspace();
228 }
229 }
230
231 pub fn enter(&mut self) {
233 let view = self.render();
234 self.focus.collect_focusables(&view);
235 if self.focus.is_focused_text_area() {
236 self.focus.text_area_enter();
237 }
238 }
239
240 pub fn scroll_up(&mut self, amount: u16) {
242 let view = self.render();
243 self.focus.collect_focusables(&view);
244 self.focus.scroll_up(amount);
245 }
246
247 pub fn scroll_down(&mut self, amount: u16) {
249 let view = self.render();
250 self.focus.collect_focusables(&view);
251 self.focus.scroll_down(amount, 100);
252 }
253
254 fn collect_text(view: &View, texts: &mut Vec<String>) {
256 match view {
257 View::Text(TextNode { content, .. }) => {
258 texts.push(content.clone());
259 }
260 View::VStack(node) => {
261 for child in &node.children {
262 Self::collect_text(child, texts);
263 }
264 }
265 View::HStack(node) => {
266 for child in &node.children {
267 Self::collect_text(child, texts);
268 }
269 }
270 View::Box(node) => {
271 if let Some(child) = &node.child {
272 Self::collect_text(child, texts);
273 }
274 }
275 View::Button(ButtonNode { label, .. }) => {
276 texts.push(label.clone());
277 }
278 View::List(ListNode { items, .. }) => {
279 texts.extend(items.clone());
280 }
281 View::TextInput(TextInputNode {
282 value, placeholder, ..
283 }) => {
284 if value.is_empty() {
285 texts.push(placeholder.clone());
286 } else {
287 texts.push(value.clone());
288 }
289 }
290 View::Checkbox(CheckboxNode { label, .. }) => {
291 texts.push(label.clone());
292 }
293 _ => {}
294 }
295 }
296
297 fn collect_buttons(view: &View, buttons: &mut Vec<String>) {
299 match view {
300 View::Button(ButtonNode { label, .. }) => {
301 buttons.push(label.clone());
302 }
303 View::VStack(node) => {
304 for child in &node.children {
305 Self::collect_buttons(child, buttons);
306 }
307 }
308 View::HStack(node) => {
309 for child in &node.children {
310 Self::collect_buttons(child, buttons);
311 }
312 }
313 View::Box(node) => {
314 if let Some(child) = &node.child {
315 Self::collect_buttons(child, buttons);
316 }
317 }
318 _ => {}
319 }
320 }
321
322 fn find_button_index(&self, view: &View, label: &str) -> Option<usize> {
324 let mut index = 0;
325 Self::find_button_index_recursive(view, label, &mut index)
326 }
327
328 fn find_button_index_recursive(view: &View, label: &str, index: &mut usize) -> Option<usize> {
329 match view {
330 View::Button(ButtonNode {
331 label: btn_label, ..
332 }) => {
333 if btn_label == label {
334 Some(*index)
335 } else {
336 *index += 1;
337 None
338 }
339 }
340 View::Box(node) => {
341 if node.scroll {
342 *index += 1;
343 }
344 if let Some(child) = &node.child {
345 Self::find_button_index_recursive(child, label, index)
346 } else {
347 None
348 }
349 }
350 View::VStack(node) => {
351 for child in &node.children {
352 if let Some(idx) = Self::find_button_index_recursive(child, label, index) {
353 return Some(idx);
354 }
355 }
356 None
357 }
358 View::HStack(node) => {
359 for child in &node.children {
360 if let Some(idx) = Self::find_button_index_recursive(child, label, index) {
361 return Some(idx);
362 }
363 }
364 None
365 }
366 View::List(_) | View::TextInput(_) | View::Checkbox(_) => {
367 *index += 1;
368 None
369 }
370 _ => None,
371 }
372 }
373
374 pub fn assert_visible(&mut self, needle: &str) {
379 let rendered = self.render_to_string();
380 if !rendered.contains(needle) {
381 panic!(
382 "\n\nassertion failed: expected {:?} to be visible\n\nRendered output ({}x{}):\n{}\n",
383 needle, self.width, self.height, rendered
384 );
385 }
386 }
387
388 pub fn assert_not_visible(&mut self, needle: &str) {
391 let rendered = self.render_to_string();
392 if rendered.contains(needle) {
393 panic!(
394 "\n\nassertion failed: expected {:?} to NOT be visible\n\nRendered output ({}x{}):\n{}\n",
395 needle, self.width, self.height, rendered
396 );
397 }
398 }
399
400 pub fn visible_items(&mut self, items: &[&str]) -> Vec<String> {
403 let rendered = self.render_to_string();
404 items
405 .iter()
406 .filter(|item| rendered.contains(*item))
407 .map(|s| s.to_string())
408 .collect()
409 }
410
411 pub fn rendered_lines(&mut self) -> Vec<String> {
415 self.render_to_string()
416 .lines()
417 .map(|s| s.to_string())
418 .collect()
419 }
420
421 pub fn find_line_containing(&mut self, needle: &str) -> Option<usize> {
424 self.rendered_lines()
425 .iter()
426 .position(|line| line.contains(needle))
427 }
428
429 pub fn viewport_height(&self) -> u16 {
433 self.height
434 }
435
436 pub fn viewport_width(&self) -> u16 {
438 self.width
439 }
440}
441
442#[macro_export]
446macro_rules! assert_snapshot {
447 ($app:expr) => {
448 let rendered = $app.render_to_string();
449 println!("Snapshot:\n{}", rendered);
451 };
452 ($app:expr, $name:expr) => {
453 let rendered = $app.render_to_string();
454 println!("Snapshot [{}]:\n{}", $name, rendered);
455 };
456}