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::terminal::Terminal;
33use crate::view::{ButtonNode, CheckboxNode, ListNode, TextInputNode, TextNode, View};
34use crate::EventSource;
35use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
36use std::cell::RefCell;
37use std::collections::VecDeque;
38use std::rc::Rc;
39use std::time::Duration;
40
41pub struct TestApp<C: Component> {
46 root: C,
47 storage: Rc<StateStorage>,
48 focus: FocusManager,
49 width: u16,
50 height: u16,
51}
52
53impl<C: Component> TestApp<C> {
54 pub fn new(root: C) -> Self {
56 Self {
57 root,
58 storage: Rc::new(StateStorage::new()),
59 focus: FocusManager::new(),
60 width: 80,
61 height: 24,
62 }
63 }
64
65 pub fn with_size(mut self, width: u16, height: u16) -> Self {
67 self.width = width;
68 self.height = height;
69 self
70 }
71
72 fn render(&self) -> View {
74 let cx = Scope::with_storage(Rc::clone(&self.storage));
75 self.root.render(cx)
76 }
77
78 pub fn render_to_string(&mut self) -> String {
80 let view = self.render();
81 self.focus.collect_focusables(&view);
82
83 let mut buffer = Buffer::new(self.width, self.height);
84 let area = buffer.rect();
85
86 let scroll_offsets: Vec<(u16, u16)> = (0..self.focus.focus_index() + 10)
87 .map(|i| self.focus.scroll_offset(i))
88 .collect();
89 let cursor_offsets: Vec<usize> = (0..self.focus.focus_index() + 10)
90 .map(|i| self.focus.cursor_offset(i))
91 .collect();
92
93 let mut ctx = RenderContext::new(self.focus.focus_index(), true, scroll_offsets, cursor_offsets, area);
95 render_view(&mut buffer, &view, area, &mut ctx);
96 ctx.render_pending_dropdowns(&mut buffer);
97
98 self.storage.flush_effects();
100
101 buffer.to_string()
102 }
103
104 pub fn find_all_text(&self) -> Vec<String> {
106 let view = self.render();
107 let mut texts = Vec::new();
108 Self::collect_text(&view, &mut texts);
109 texts
110 }
111
112 pub fn find_text(&self, needle: &str) -> Option<String> {
114 self.find_all_text()
115 .into_iter()
116 .find(|t| t.contains(needle))
117 }
118
119 pub fn has_text(&self, needle: &str) -> bool {
121 self.find_text(needle).is_some()
122 }
123
124 pub fn find_all_buttons(&self) -> Vec<String> {
126 let view = self.render();
127 let mut buttons = Vec::new();
128 Self::collect_buttons(&view, &mut buttons);
129 buttons
130 }
131
132 pub fn find_button(&self, label: &str) -> Option<String> {
134 self.find_all_buttons().into_iter().find(|l| l == label)
135 }
136
137 pub fn focus_index(&self) -> usize {
139 self.focus.focus_index()
140 }
141
142 pub fn focusable_count(&mut self) -> usize {
144 let view = self.render();
145 self.focus.collect_focusables(&view);
146 self.focus.focusable_count()
147 }
148
149 pub fn focus_next(&mut self) {
151 let view = self.render();
152 self.focus.collect_focusables(&view);
153 self.focus.focus_next();
154 }
155
156 pub fn focus_prev(&mut self) {
158 let view = self.render();
159 self.focus.collect_focusables(&view);
160 self.focus.focus_prev();
161 }
162
163 pub fn activate(&mut self) {
165 let view = self.render();
166 self.focus.collect_focusables(&view);
167 self.focus.activate();
168 }
169
170 pub fn press_button(&mut self, label: &str) -> bool {
174 let view = self.render();
175 self.focus.collect_focusables(&view);
176
177 if let Some(idx) = self.find_button_index(&view, label) {
179 while self.focus.focus_index() != idx {
181 self.focus.focus_next();
182 }
183 self.focus.activate();
185 true
186 } else {
187 false
188 }
189 }
190
191 pub fn list_up(&mut self) {
193 let view = self.render();
194 self.focus.collect_focusables(&view);
195 self.focus.list_select_prev();
196 }
197
198 pub fn list_down(&mut self) {
200 let view = self.render();
201 self.focus.collect_focusables(&view);
202 self.focus.list_select_next();
203 }
204
205 pub fn type_char(&mut self, c: char) {
207 let view = self.render();
208 self.focus.collect_focusables(&view);
209 self.focus
211 .set_default_textarea_wrap_width(self.width.saturating_sub(4));
212 if self.focus.is_focused_text_area() {
213 self.focus.text_area_key(c);
214 } else {
215 self.focus.text_input_key(c);
216 }
217 }
218
219 pub fn type_str(&mut self, s: &str) {
221 for c in s.chars() {
222 self.type_char(c);
223 }
224 }
225
226 pub fn backspace(&mut self) {
228 let view = self.render();
229 self.focus.collect_focusables(&view);
230 if self.focus.is_focused_text_area() {
231 self.focus.text_area_backspace();
232 } else {
233 self.focus.text_input_backspace();
234 }
235 }
236
237 pub fn enter(&mut self) {
239 let view = self.render();
240 self.focus.collect_focusables(&view);
241 if self.focus.is_focused_text_area() {
242 self.focus.text_area_enter();
243 }
244 }
245
246 pub fn scroll_up(&mut self, amount: u16) {
248 let view = self.render();
249 self.focus.collect_focusables(&view);
250 self.focus.scroll_up(amount);
251 }
252
253 pub fn scroll_down(&mut self, amount: u16) {
255 let view = self.render();
256 self.focus.collect_focusables(&view);
257 self.focus.scroll_down(amount, 100);
258 }
259
260 fn collect_text(view: &View, texts: &mut Vec<String>) {
262 match view {
263 View::Text(TextNode { content, .. }) => {
264 texts.push(content.clone());
265 }
266 View::VStack(node) => {
267 for child in &node.children {
268 Self::collect_text(child, texts);
269 }
270 }
271 View::HStack(node) => {
272 for child in &node.children {
273 Self::collect_text(child, texts);
274 }
275 }
276 View::Box(node) => {
277 if let Some(child) = &node.child {
278 Self::collect_text(child, texts);
279 }
280 }
281 View::Button(ButtonNode { label, .. }) => {
282 texts.push(label.clone());
283 }
284 View::List(ListNode { items, .. }) => {
285 texts.extend(items.clone());
286 }
287 View::TextInput(TextInputNode {
288 value, placeholder, ..
289 }) => {
290 if value.is_empty() {
291 texts.push(placeholder.clone());
292 } else {
293 texts.push(value.clone());
294 }
295 }
296 View::Checkbox(CheckboxNode { label, .. }) => {
297 texts.push(label.clone());
298 }
299 View::ErrorBoundary(node) => {
300 Self::collect_text(&node.child, texts);
301 }
302 _ => {}
303 }
304 }
305
306 fn collect_buttons(view: &View, buttons: &mut Vec<String>) {
308 match view {
309 View::Button(ButtonNode { label, .. }) => {
310 buttons.push(label.clone());
311 }
312 View::VStack(node) => {
313 for child in &node.children {
314 Self::collect_buttons(child, buttons);
315 }
316 }
317 View::HStack(node) => {
318 for child in &node.children {
319 Self::collect_buttons(child, buttons);
320 }
321 }
322 View::Box(node) => {
323 if let Some(child) = &node.child {
324 Self::collect_buttons(child, buttons);
325 }
326 }
327 View::ErrorBoundary(node) => {
328 Self::collect_buttons(&node.child, buttons);
329 }
330 _ => {}
331 }
332 }
333
334 fn find_button_index(&self, view: &View, label: &str) -> Option<usize> {
336 let mut index = 0;
337 Self::find_button_index_recursive(view, label, &mut index)
338 }
339
340 fn find_button_index_recursive(view: &View, label: &str, index: &mut usize) -> Option<usize> {
341 match view {
342 View::Button(ButtonNode {
343 label: btn_label, ..
344 }) => {
345 if btn_label == label {
346 Some(*index)
347 } else {
348 *index += 1;
349 None
350 }
351 }
352 View::Box(node) => {
353 if node.scroll {
354 *index += 1;
355 }
356 if let Some(child) = &node.child {
357 Self::find_button_index_recursive(child, label, index)
358 } else {
359 None
360 }
361 }
362 View::VStack(node) => {
363 for child in &node.children {
364 if let Some(idx) = Self::find_button_index_recursive(child, label, index) {
365 return Some(idx);
366 }
367 }
368 None
369 }
370 View::HStack(node) => {
371 for child in &node.children {
372 if let Some(idx) = Self::find_button_index_recursive(child, label, index) {
373 return Some(idx);
374 }
375 }
376 None
377 }
378 View::ErrorBoundary(node) => {
379 Self::find_button_index_recursive(&node.child, label, index)
380 }
381 View::List(_) | View::TextInput(_) | View::Checkbox(_) => {
382 *index += 1;
383 None
384 }
385 _ => None,
386 }
387 }
388
389 pub fn assert_visible(&mut self, needle: &str) {
394 let rendered = self.render_to_string();
395 if !rendered.contains(needle) {
396 panic!(
397 "\n\nassertion failed: expected {:?} to be visible\n\nRendered output ({}x{}):\n{}\n",
398 needle, self.width, self.height, rendered
399 );
400 }
401 }
402
403 pub fn assert_not_visible(&mut self, needle: &str) {
406 let rendered = self.render_to_string();
407 if rendered.contains(needle) {
408 panic!(
409 "\n\nassertion failed: expected {:?} to NOT be visible\n\nRendered output ({}x{}):\n{}\n",
410 needle, self.width, self.height, rendered
411 );
412 }
413 }
414
415 pub fn visible_items(&mut self, items: &[&str]) -> Vec<String> {
418 let rendered = self.render_to_string();
419 items
420 .iter()
421 .filter(|item| rendered.contains(*item))
422 .map(|s| s.to_string())
423 .collect()
424 }
425
426 pub fn rendered_lines(&mut self) -> Vec<String> {
430 self.render_to_string()
431 .lines()
432 .map(|s| s.to_string())
433 .collect()
434 }
435
436 pub fn find_line_containing(&mut self, needle: &str) -> Option<usize> {
439 self.rendered_lines()
440 .iter()
441 .position(|line| line.contains(needle))
442 }
443
444 pub fn viewport_height(&self) -> u16 {
448 self.height
449 }
450
451 pub fn viewport_width(&self) -> u16 {
453 self.width
454 }
455}
456
457#[macro_export]
461macro_rules! assert_snapshot {
462 ($app:expr) => {
463 let rendered = $app.render_to_string();
464 println!("Snapshot:\n{}", rendered);
466 };
467 ($app:expr, $name:expr) => {
468 let rendered = $app.render_to_string();
469 println!("Snapshot [{}]:\n{}", $name, rendered);
470 };
471}
472
473pub struct TestEventSource {
482 events: RefCell<VecDeque<Event>>,
483 exhausted: RefCell<bool>,
484 last_buffer: RefCell<String>,
485}
486
487impl TestEventSource {
488 pub fn new(events: Vec<Event>) -> Self {
490 Self {
491 events: RefCell::new(events.into()),
492 exhausted: RefCell::new(false),
493 last_buffer: RefCell::new(String::new()),
494 }
495 }
496
497 pub fn last_buffer(&self) -> String {
499 self.last_buffer.borrow().clone()
500 }
501}
502
503impl EventSource for TestEventSource {
504 fn poll_event(&self, _timeout: Duration) -> std::io::Result<Option<Event>> {
505 let mut events = self.events.borrow_mut();
506 if let Some(event) = events.pop_front() {
507 Ok(Some(event))
508 } else if !*self.exhausted.borrow() {
509 *self.exhausted.borrow_mut() = true;
511 Ok(Some(Event::Key(KeyEvent::new(
512 KeyCode::Char('q'),
513 KeyModifiers::CONTROL,
514 ))))
515 } else {
516 Ok(None)
518 }
519 }
520
521 fn on_frame_rendered(&self, terminal: &Terminal) {
522 *self.last_buffer.borrow_mut() = terminal.buffer_string();
523 }
524}
525
526pub struct StreamTestEventSource {
537 deadline: std::time::Instant,
538 exhausted: RefCell<bool>,
539 frames: RefCell<Vec<String>>,
541}
542
543impl StreamTestEventSource {
544 pub fn new(duration: Duration) -> Self {
546 Self {
547 deadline: std::time::Instant::now() + duration,
548 exhausted: RefCell::new(false),
549 frames: RefCell::new(Vec::new()),
550 }
551 }
552
553 pub fn frames(&self) -> Vec<String> {
555 self.frames.borrow().clone()
556 }
557}
558
559impl EventSource for StreamTestEventSource {
560 fn poll_event(&self, timeout: Duration) -> std::io::Result<Option<Event>> {
561 if std::time::Instant::now() >= self.deadline {
562 if !*self.exhausted.borrow() {
563 *self.exhausted.borrow_mut() = true;
564 return Ok(Some(Event::Key(KeyEvent::new(
565 KeyCode::Char('q'),
566 KeyModifiers::CONTROL,
567 ))));
568 }
569 return Ok(None);
570 }
571 std::thread::sleep(timeout);
573 Ok(None)
574 }
575
576 fn on_frame_rendered(&self, terminal: &Terminal) {
577 self.frames.borrow_mut().push(terminal.buffer_string());
578 }
579}