1use crate::buffer::Buffer;
8use crate::context::Context;
9use crate::event::{
10 Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEvent, MouseKind,
11};
12use crate::rect::Rect;
13use crate::style::Style;
14use crate::{run_frame_kernel, FrameState, RunConfig};
15
16pub struct EventBuilder {
35 events: Vec<Event>,
36}
37
38impl EventBuilder {
39 pub fn new() -> Self {
41 Self { events: Vec::new() }
42 }
43
44 pub fn key(mut self, c: char) -> Self {
46 self.events.push(Event::Key(KeyEvent {
47 code: KeyCode::Char(c),
48 modifiers: KeyModifiers::NONE,
49 kind: KeyEventKind::Press,
50 }));
51 self
52 }
53
54 pub fn key_code(mut self, code: KeyCode) -> Self {
56 self.events.push(Event::Key(KeyEvent {
57 code,
58 modifiers: KeyModifiers::NONE,
59 kind: KeyEventKind::Press,
60 }));
61 self
62 }
63
64 pub fn key_with(mut self, code: KeyCode, modifiers: KeyModifiers) -> Self {
66 self.events.push(Event::Key(KeyEvent {
67 code,
68 modifiers,
69 kind: KeyEventKind::Press,
70 }));
71 self
72 }
73
74 pub fn click(mut self, x: u32, y: u32) -> Self {
76 self.events.push(Event::Mouse(MouseEvent {
77 kind: MouseKind::Down(MouseButton::Left),
78 x,
79 y,
80 modifiers: KeyModifiers::NONE,
81 pixel_x: None,
82 pixel_y: None,
83 }));
84 self
85 }
86
87 pub fn mouse_up(mut self, x: u32, y: u32) -> Self {
89 self.events.push(Event::mouse_up(x, y));
90 self
91 }
92
93 pub fn drag(mut self, x: u32, y: u32) -> Self {
95 self.events.push(Event::mouse_drag(x, y));
96 self
97 }
98
99 pub fn key_release(mut self, c: char) -> Self {
104 self.events.push(Event::key_release(c));
105 self
106 }
107
108 pub fn focus_gained(mut self) -> Self {
110 self.events.push(Event::FocusGained);
111 self
112 }
113
114 pub fn focus_lost(mut self) -> Self {
116 self.events.push(Event::FocusLost);
117 self
118 }
119
120 pub fn scroll_up(mut self, x: u32, y: u32) -> Self {
122 self.events.push(Event::Mouse(MouseEvent {
123 kind: MouseKind::ScrollUp,
124 x,
125 y,
126 modifiers: KeyModifiers::NONE,
127 pixel_x: None,
128 pixel_y: None,
129 }));
130 self
131 }
132
133 pub fn scroll_down(mut self, x: u32, y: u32) -> Self {
135 self.events.push(Event::Mouse(MouseEvent {
136 kind: MouseKind::ScrollDown,
137 x,
138 y,
139 modifiers: KeyModifiers::NONE,
140 pixel_x: None,
141 pixel_y: None,
142 }));
143 self
144 }
145
146 pub fn paste(mut self, text: impl Into<String>) -> Self {
148 self.events.push(Event::Paste(text.into()));
149 self
150 }
151
152 pub fn resize(mut self, width: u32, height: u32) -> Self {
154 self.events.push(Event::Resize(width, height));
155 self
156 }
157
158 pub fn build(self) -> Vec<Event> {
160 self.events
161 }
162}
163
164impl Default for EventBuilder {
165 fn default() -> Self {
166 Self::new()
167 }
168}
169
170pub struct TestBackend {
191 buffer: Buffer,
192 width: u32,
193 height: u32,
194 frame_state: FrameState,
195 frames: Option<Vec<FrameRecord>>,
199}
200
201#[derive(Clone, Debug, PartialEq, Eq)]
212pub struct FrameRecord {
213 pub snapshot: String,
216 pub lines: Vec<String>,
219}
220
221impl FrameRecord {
222 pub fn to_string_trimmed(&self) -> String {
226 let mut lines = self.lines.clone();
227 while lines.last().is_some_and(|l| l.is_empty()) {
228 lines.pop();
229 }
230 lines.join("\n")
231 }
232
233 pub fn line(&self, y: u32) -> &str {
236 self.lines
237 .get(y as usize)
238 .map(|s| s.as_str())
239 .unwrap_or_default()
240 }
241
242 pub fn assert_contains(&self, expected: &str) {
245 for line in &self.lines {
246 if line.contains(expected) {
247 return;
248 }
249 }
250 let mut detail = String::new();
251 for (y, line) in self.lines.iter().enumerate() {
252 detail.push_str(&format!(" {y}: {line}\n"));
253 }
254 panic!("FrameRecord does not contain {expected:?}.\nFrame:\n{detail}");
255 }
256}
257
258impl TestBackend {
259 pub fn new(width: u32, height: u32) -> Self {
261 let area = Rect::new(0, 0, width, height);
262 Self {
263 buffer: Buffer::empty(area),
264 width,
265 height,
266 frame_state: FrameState::default(),
267 frames: None,
268 }
269 }
270
271 pub fn record_frames(mut self) -> Self {
297 if self.frames.is_none() {
298 self.frames = Some(Vec::new());
299 }
300 self
301 }
302
303 pub fn frames(&self) -> &[FrameRecord] {
308 self.frames.as_deref().unwrap_or(&[])
309 }
310
311 fn capture_frame(&mut self) {
316 if let Some(frames) = self.frames.as_mut() {
317 let snapshot = self.buffer.snapshot_format();
318 let mut lines = Vec::with_capacity(self.height as usize);
319 for y in 0..self.height {
320 let mut s = String::new();
321 for x in 0..self.width {
322 s.push_str(&self.buffer.get(x, y).symbol);
323 }
324 lines.push(s.trim_end().to_string());
325 }
326 frames.push(FrameRecord { snapshot, lines });
327 }
328 }
329
330 fn render_frame(
331 &mut self,
332 events: Vec<Event>,
333 setup_state: impl FnOnce(&mut FrameState),
334 f: impl FnOnce(&mut Context),
335 ) {
336 setup_state(&mut self.frame_state);
337
338 self.buffer.reset();
339 let mut once = Some(f);
340 let mut render = |ui: &mut Context| {
341 if let Some(f) = once.take() {
342 f(ui);
343 } else {
344 panic!("render closure called twice");
345 }
346 };
347 let _ = run_frame_kernel(
348 &mut self.buffer,
349 &mut self.frame_state,
350 &RunConfig::default(),
351 (self.width, self.height),
352 events,
353 false,
354 &mut render,
355 );
356 self.capture_frame();
357 }
358
359 pub fn render(&mut self, f: impl FnOnce(&mut Context)) {
361 self.render_frame(Vec::new(), |_| {}, f);
362 }
363
364 pub fn render_with_events(
366 &mut self,
367 events: Vec<Event>,
368 focus_index: usize,
369 prev_focus_count: usize,
370 f: impl FnOnce(&mut Context),
371 ) {
372 self.render_frame(
373 events,
374 |state| {
375 state.focus.focus_index = focus_index;
376 state.focus.prev_focus_count = prev_focus_count;
377 },
378 f,
379 );
380 }
381
382 pub fn run_with_events(&mut self, events: Vec<Event>, f: impl FnOnce(&mut crate::Context)) {
384 self.render_with_events(events, 0, 0, f);
385 }
386
387 pub fn line(&self, y: u32) -> String {
389 let mut s = String::new();
390 for x in 0..self.width {
391 s.push_str(&self.buffer.get(x, y).symbol);
392 }
393 s.trim_end().to_string()
394 }
395
396 pub fn assert_line(&self, y: u32, expected: &str) {
398 let line = self.line(y);
399 assert_eq!(
400 line, expected,
401 "Line {y}: expected {expected:?}, got {line:?}"
402 );
403 }
404
405 pub fn assert_line_contains(&self, y: u32, expected: &str) {
407 let line = self.line(y);
408 assert!(
409 line.contains(expected),
410 "Line {y}: expected to contain {expected:?}, got {line:?}"
411 );
412 }
413
414 pub fn assert_contains(&self, expected: &str) {
416 for y in 0..self.height {
417 if self.line(y).contains(expected) {
418 return;
419 }
420 }
421 let mut all_lines = String::new();
422 for y in 0..self.height {
423 all_lines.push_str(&format!("{}: {}\n", y, self.line(y)));
424 }
425 panic!("Buffer does not contain {expected:?}.\nBuffer:\n{all_lines}");
426 }
427
428 pub fn buffer(&self) -> &Buffer {
430 &self.buffer
431 }
432
433 pub fn width(&self) -> u32 {
435 self.width
436 }
437
438 pub fn height(&self) -> u32 {
440 self.height
441 }
442
443 pub fn to_string_trimmed(&self) -> String {
448 let mut lines = Vec::with_capacity(self.height as usize);
449 for y in 0..self.height {
450 lines.push(self.line(y));
451 }
452 while lines.last().is_some_and(|l| l.is_empty()) {
453 lines.pop();
454 }
455 lines.join("\n")
456 }
457
458 pub fn assert_not_contains(&self, expected: &str) {
464 let mut offending: Vec<(u32, String)> = Vec::new();
465 for y in 0..self.height {
466 let line = self.line(y);
467 if line.contains(expected) {
468 offending.push((y, line));
469 }
470 }
471 if !offending.is_empty() {
472 let detail = offending
473 .iter()
474 .map(|(y, l)| format!(" row {y}: {l:?}"))
475 .collect::<Vec<_>>()
476 .join("\n");
477 panic!("Buffer unexpectedly contains {expected:?}:\n{detail}");
478 }
479 }
480
481 pub fn assert_line_not_contains(&self, y: u32, expected: &str) {
483 let line = self.line(y);
484 assert!(
485 !line.contains(expected),
486 "Line {y}: expected NOT to contain {expected:?}, but got {line:?}"
487 );
488 }
489
490 pub fn assert_empty_line(&self, y: u32) {
495 let line = self.line(y);
496 assert!(line.is_empty(), "Line {y}: expected empty, got {line:?}");
497 }
498
499 pub fn assert_style_at(&self, x: u32, y: u32, expected: Style) {
505 let actual = self.buffer.get(x, y).style;
506 assert_eq!(
507 actual, expected,
508 "Style mismatch at ({x}, {y}): expected {expected:?}, got {actual:?}"
509 );
510 }
511
512 pub fn sequence(&mut self) -> TestSequence<'_> {
535 TestSequence {
536 backend: self,
537 steps: Vec::new(),
538 }
539 }
540
541 pub fn type_string(&mut self, s: &str, mut render: impl FnMut(&mut Context)) {
561 for ch in s.chars() {
562 let events = vec![Event::Key(KeyEvent {
563 code: KeyCode::Char(ch),
564 modifiers: KeyModifiers::NONE,
565 kind: KeyEventKind::Press,
566 })];
567 self.render_frame(events, |_| {}, &mut render);
570 }
571 }
572}
573
574struct TestStep<'a> {
580 events: Vec<Event>,
581 render: Box<dyn FnOnce(&mut Context) + 'a>,
582}
583
584pub struct TestSequence<'a> {
591 backend: &'a mut TestBackend,
592 steps: Vec<TestStep<'a>>,
593}
594
595impl<'a> TestSequence<'a> {
596 pub fn tick(mut self, f: impl FnOnce(&mut Context) + 'a) -> Self {
601 self.steps.push(TestStep {
602 events: Vec::new(),
603 render: Box::new(f),
604 });
605 self
606 }
607
608 pub fn key(mut self, code: KeyCode, f: impl FnOnce(&mut Context) + 'a) -> Self {
610 let events = vec![Event::Key(KeyEvent {
611 code,
612 modifiers: KeyModifiers::NONE,
613 kind: KeyEventKind::Press,
614 })];
615 self.steps.push(TestStep {
616 events,
617 render: Box::new(f),
618 });
619 self
620 }
621
622 pub fn type_string(mut self, s: &str, f: impl FnOnce(&mut Context) + 'a) -> Self {
630 let events = s
631 .chars()
632 .map(|c| {
633 Event::Key(KeyEvent {
634 code: KeyCode::Char(c),
635 modifiers: KeyModifiers::NONE,
636 kind: KeyEventKind::Press,
637 })
638 })
639 .collect();
640 self.steps.push(TestStep {
641 events,
642 render: Box::new(f),
643 });
644 self
645 }
646
647 pub fn events(mut self, events: Vec<Event>, f: impl FnOnce(&mut Context) + 'a) -> Self {
652 self.steps.push(TestStep {
653 events,
654 render: Box::new(f),
655 });
656 self
657 }
658
659 pub fn run(self) {
664 let backend = self.backend;
665 for step in self.steps {
666 let TestStep { events, render } = step;
667 let mut once = Some(render);
670 let f = move |ui: &mut Context| {
671 if let Some(f) = once.take() {
672 f(ui);
673 }
674 };
675 backend.render_frame(events, |_| {}, f);
676 }
677 }
678}
679
680impl std::fmt::Display for TestBackend {
681 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
682 write!(f, "{}", self.to_string_trimmed())
683 }
684}
685
686#[cfg(test)]
687mod tests {
688 use super::*;
689 use crate::event::{KeyEventKind, MouseKind};
690
691 #[test]
693 fn event_builder_mouse_up_produces_up_event() {
694 let events = EventBuilder::new().mouse_up(5, 3).build();
695 assert_eq!(events.len(), 1);
696 match &events[0] {
697 Event::Mouse(m) => {
698 assert!(matches!(m.kind, MouseKind::Up(MouseButton::Left)));
699 assert_eq!(m.x, 5);
700 assert_eq!(m.y, 3);
701 }
702 _ => panic!("expected mouse event"),
703 }
704 }
705
706 #[test]
708 fn event_builder_drag_produces_drag_event() {
709 let events = EventBuilder::new().drag(10, 5).build();
710 assert_eq!(events.len(), 1);
711 match &events[0] {
712 Event::Mouse(m) => {
713 assert!(matches!(m.kind, MouseKind::Drag(MouseButton::Left)));
714 assert_eq!(m.x, 10);
715 assert_eq!(m.y, 5);
716 }
717 _ => panic!("expected mouse event"),
718 }
719 }
720
721 #[test]
723 fn event_builder_key_release_produces_release_event() {
724 let events = EventBuilder::new().key_release('a').build();
725 assert_eq!(events.len(), 1);
726 match &events[0] {
727 Event::Key(k) => {
728 assert_eq!(k.code, KeyCode::Char('a'));
729 assert!(matches!(k.kind, KeyEventKind::Release));
730 }
731 _ => panic!("expected key event"),
732 }
733 }
734
735 #[test]
737 fn event_builder_focus_events_chaining() {
738 let events = EventBuilder::new().focus_lost().focus_gained().build();
739 assert_eq!(events, vec![Event::FocusLost, Event::FocusGained]);
740 }
741
742 #[test]
745 fn record_frames_disabled_returns_empty_slice() {
746 let mut tb = TestBackend::new(10, 2);
747 tb.render(|ui| {
748 ui.text("hi");
749 });
750 assert!(tb.frames().is_empty());
751 }
752
753 #[test]
754 fn record_frames_captures_each_render() {
755 let mut tb = TestBackend::new(20, 2).record_frames();
756 for n in 0..3 {
757 tb.render(|ui| {
758 ui.text(format!("frame {n}"));
759 });
760 }
761 assert_eq!(tb.frames().len(), 3);
762 tb.frames()[0].assert_contains("frame 0");
763 tb.frames()[1].assert_contains("frame 1");
764 tb.frames()[2].assert_contains("frame 2");
765 }
766
767 #[test]
768 fn record_frames_stores_styled_snapshot() {
769 let mut tb = TestBackend::new(10, 1).record_frames();
770 tb.render(|ui| {
771 ui.text("hi").bold();
772 });
773 let frame = &tb.frames()[0];
774 assert!(
776 frame.snapshot.contains("bold"),
777 "snapshot missing bold marker: {:?}",
778 frame.snapshot
779 );
780 }
781
782 #[test]
783 fn record_frames_idempotent_when_called_twice() {
784 let tb = TestBackend::new(10, 1).record_frames();
786 let mut tb = tb.record_frames();
787 tb.render(|ui| {
788 ui.text("a");
789 });
790 assert_eq!(tb.frames().len(), 1);
791 }
792
793 #[test]
794 fn frame_record_to_string_trimmed_drops_trailing_blank_rows() {
795 let mut tb = TestBackend::new(10, 4).record_frames();
796 tb.render(|ui| {
797 ui.text("hello");
798 });
799 let frame = &tb.frames()[0];
800 assert_eq!(frame.lines.len(), 4);
802 let s = frame.to_string_trimmed();
804 assert!(!s.ends_with('\n'));
805 assert!(s.starts_with("hello"));
806 }
807
808 #[test]
811 fn sequence_runs_multiple_steps_in_order() {
812 let mut tb = TestBackend::new(20, 2).record_frames();
813 tb.sequence()
814 .tick(|ui| {
815 ui.text("step-1");
816 })
817 .tick(|ui| {
818 ui.text("step-2");
819 })
820 .tick(|ui| {
821 ui.text("step-3");
822 })
823 .run();
824 assert_eq!(tb.frames().len(), 3);
825 tb.frames()[0].assert_contains("step-1");
826 tb.frames()[1].assert_contains("step-2");
827 tb.frames()[2].assert_contains("step-3");
828 }
829
830 #[test]
831 fn sequence_key_step_injects_event() {
832 let mut tb = TestBackend::new(20, 2);
835 tb.sequence()
836 .key(KeyCode::Esc, |ui| {
837 ui.text("after-esc");
838 })
839 .run();
840 tb.assert_contains("after-esc");
841 }
842
843 #[test]
844 fn sequence_type_string_collapses_into_single_step() {
845 let mut tb = TestBackend::new(20, 2).record_frames();
846 tb.sequence()
847 .type_string("abc", |ui| {
848 ui.text("done");
849 })
850 .run();
851 assert_eq!(tb.frames().len(), 1);
853 tb.frames()[0].assert_contains("done");
854 }
855
856 #[test]
857 fn sequence_events_step_takes_arbitrary_batch() {
858 let mut tb = TestBackend::new(20, 2);
859 let events = EventBuilder::new()
860 .key('a')
861 .key_code(KeyCode::Enter)
862 .build();
863 tb.sequence()
864 .events(events, |ui| {
865 ui.text("ran");
866 })
867 .run();
868 tb.assert_contains("ran");
869 }
870
871 #[test]
872 fn type_string_renders_one_frame_per_char() {
873 let mut tb = TestBackend::new(20, 2).record_frames();
874 tb.type_string("abc", |ui| {
875 ui.text("char");
876 });
877 assert_eq!(tb.frames().len(), 3);
878 }
879
880 #[test]
881 fn type_string_handles_empty_input() {
882 let mut tb = TestBackend::new(20, 2).record_frames();
883 tb.type_string("", |ui| {
884 ui.text("never-called");
885 });
886 assert_eq!(tb.frames().len(), 0);
887 }
888
889 #[test]
892 fn assert_not_contains_passes_when_absent() {
893 let mut tb = TestBackend::new(20, 2);
894 tb.render(|ui| {
895 ui.text("hello world");
896 });
897 tb.assert_not_contains("error");
898 }
899
900 #[test]
901 #[should_panic(expected = "Buffer unexpectedly contains")]
902 fn assert_not_contains_panics_when_present() {
903 let mut tb = TestBackend::new(20, 2);
904 tb.render(|ui| {
905 ui.text("error: fail");
906 });
907 tb.assert_not_contains("error");
908 }
909
910 #[test]
911 fn assert_line_not_contains_passes_when_other_row_has_substring() {
912 let mut tb = TestBackend::new(20, 3);
913 tb.render(|ui| {
914 let _ = ui.col(|ui| {
915 ui.text("first");
916 ui.text("second");
917 });
918 });
919 tb.assert_line_not_contains(0, "second");
921 }
922
923 #[test]
924 #[should_panic(expected = "Line 0: expected NOT to contain")]
925 fn assert_line_not_contains_panics_when_present() {
926 let mut tb = TestBackend::new(20, 1);
927 tb.render(|ui| {
928 ui.text("hello");
929 });
930 tb.assert_line_not_contains(0, "ello");
931 }
932
933 #[test]
934 fn assert_empty_line_passes_for_blank_row() {
935 let mut tb = TestBackend::new(20, 2);
936 tb.render(|ui| {
937 ui.text("only-row-0");
938 });
939 tb.assert_empty_line(1);
941 }
942
943 #[test]
944 #[should_panic(expected = "Line 0: expected empty")]
945 fn assert_empty_line_panics_when_non_blank() {
946 let mut tb = TestBackend::new(20, 2);
947 tb.render(|ui| {
948 ui.text("not-empty");
949 });
950 tb.assert_empty_line(0);
951 }
952
953 #[test]
954 fn assert_style_at_passes_for_matching_style() {
955 use crate::style::{Color, Modifiers};
956 let mut tb = TestBackend::new(10, 1);
957 tb.render(|ui| {
958 ui.text("x").fg(Color::Red);
959 });
960 let expected = Style {
961 fg: Some(Color::Red),
962 bg: None,
963 modifiers: Modifiers::NONE,
964 };
965 tb.assert_style_at(0, 0, expected);
966 }
967
968 #[test]
969 #[should_panic(expected = "Style mismatch")]
970 fn assert_style_at_panics_on_mismatch() {
971 use crate::style::Color;
972 let mut tb = TestBackend::new(10, 1);
973 tb.render(|ui| {
974 ui.text("x").fg(Color::Red);
975 });
976 let expected = Style::new().fg(Color::Blue);
977 tb.assert_style_at(0, 0, expected);
978 }
979}