Skip to main content

slt/
test_utils.rs

1use crate::buffer::Buffer;
2use crate::context::Context;
3use crate::event::{Event, KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseKind};
4use crate::layout;
5use crate::rect::Rect;
6use crate::style::Theme;
7
8pub struct EventBuilder {
9    events: Vec<Event>,
10}
11
12impl EventBuilder {
13    pub fn new() -> Self {
14        Self { events: Vec::new() }
15    }
16
17    pub fn key(mut self, c: char) -> Self {
18        self.events.push(Event::Key(KeyEvent {
19            code: KeyCode::Char(c),
20            modifiers: KeyModifiers::NONE,
21        }));
22        self
23    }
24
25    pub fn key_code(mut self, code: KeyCode) -> Self {
26        self.events.push(Event::Key(KeyEvent {
27            code,
28            modifiers: KeyModifiers::NONE,
29        }));
30        self
31    }
32
33    pub fn key_with(mut self, code: KeyCode, modifiers: KeyModifiers) -> Self {
34        self.events.push(Event::Key(KeyEvent { code, modifiers }));
35        self
36    }
37
38    pub fn click(mut self, x: u32, y: u32) -> Self {
39        self.events.push(Event::Mouse(MouseEvent {
40            kind: MouseKind::Down(MouseButton::Left),
41            x,
42            y,
43            modifiers: KeyModifiers::NONE,
44        }));
45        self
46    }
47
48    pub fn scroll_up(mut self, x: u32, y: u32) -> Self {
49        self.events.push(Event::Mouse(MouseEvent {
50            kind: MouseKind::ScrollUp,
51            x,
52            y,
53            modifiers: KeyModifiers::NONE,
54        }));
55        self
56    }
57
58    pub fn scroll_down(mut self, x: u32, y: u32) -> Self {
59        self.events.push(Event::Mouse(MouseEvent {
60            kind: MouseKind::ScrollDown,
61            x,
62            y,
63            modifiers: KeyModifiers::NONE,
64        }));
65        self
66    }
67
68    pub fn paste(mut self, text: impl Into<String>) -> Self {
69        self.events.push(Event::Paste(text.into()));
70        self
71    }
72
73    pub fn resize(mut self, width: u32, height: u32) -> Self {
74        self.events.push(Event::Resize(width, height));
75        self
76    }
77
78    pub fn build(self) -> Vec<Event> {
79        self.events
80    }
81}
82
83impl Default for EventBuilder {
84    fn default() -> Self {
85        Self::new()
86    }
87}
88
89pub struct TestBackend {
90    buffer: Buffer,
91    width: u32,
92    height: u32,
93}
94
95impl TestBackend {
96    pub fn new(width: u32, height: u32) -> Self {
97        let area = Rect::new(0, 0, width, height);
98        Self {
99            buffer: Buffer::empty(area),
100            width,
101            height,
102        }
103    }
104
105    /// Run a closure as if it were one frame, render to internal buffer
106    pub fn render(&mut self, f: impl FnOnce(&mut Context)) {
107        let mut ctx = Context::new(
108            Vec::new(),
109            self.width,
110            self.height,
111            0,
112            0,
113            0,
114            Vec::new(),
115            Vec::new(),
116            Vec::new(),
117            false,
118            Theme::dark(),
119            None,
120            false,
121        );
122        f(&mut ctx);
123        let mut tree = layout::build_tree(&ctx.commands);
124        let area = Rect::new(0, 0, self.width, self.height);
125        layout::compute(&mut tree, area);
126        self.buffer.reset();
127        layout::render(&tree, &mut self.buffer);
128    }
129
130    /// Render with specific events (for testing keyboard/mouse interaction)
131    pub fn render_with_events(
132        &mut self,
133        events: Vec<Event>,
134        focus_index: usize,
135        prev_focus_count: usize,
136        f: impl FnOnce(&mut Context),
137    ) {
138        let mut ctx = Context::new(
139            events,
140            self.width,
141            self.height,
142            0,
143            focus_index,
144            prev_focus_count,
145            Vec::new(),
146            Vec::new(),
147            Vec::new(),
148            false,
149            Theme::dark(),
150            None,
151            false,
152        );
153        ctx.process_focus_keys();
154        f(&mut ctx);
155        let mut tree = layout::build_tree(&ctx.commands);
156        let area = Rect::new(0, 0, self.width, self.height);
157        layout::compute(&mut tree, area);
158        self.buffer.reset();
159        layout::render(&tree, &mut self.buffer);
160    }
161
162    pub fn run_with_events(&mut self, events: Vec<Event>, f: impl FnOnce(&mut crate::Context)) {
163        self.render_with_events(events, 0, 0, f);
164    }
165
166    /// Get the rendered text content of row y (trimmed trailing spaces)
167    pub fn line(&self, y: u32) -> String {
168        let mut s = String::new();
169        for x in 0..self.width {
170            s.push_str(&self.buffer.get(x, y).symbol);
171        }
172        s.trim_end().to_string()
173    }
174
175    /// Assert that row y contains `expected` as a substring
176    pub fn assert_line(&self, y: u32, expected: &str) {
177        let line = self.line(y);
178        assert_eq!(
179            line, expected,
180            "Line {y}: expected {expected:?}, got {line:?}"
181        );
182    }
183
184    /// Assert that row y contains `expected` as a substring
185    pub fn assert_line_contains(&self, y: u32, expected: &str) {
186        let line = self.line(y);
187        assert!(
188            line.contains(expected),
189            "Line {y}: expected to contain {expected:?}, got {line:?}"
190        );
191    }
192
193    /// Assert that any line in the buffer contains `expected`
194    pub fn assert_contains(&self, expected: &str) {
195        for y in 0..self.height {
196            if self.line(y).contains(expected) {
197                return;
198            }
199        }
200        let mut all_lines = String::new();
201        for y in 0..self.height {
202            all_lines.push_str(&format!("{}: {}\n", y, self.line(y)));
203        }
204        panic!("Buffer does not contain {expected:?}.\nBuffer:\n{all_lines}");
205    }
206
207    pub fn buffer(&self) -> &Buffer {
208        &self.buffer
209    }
210
211    pub fn width(&self) -> u32 {
212        self.width
213    }
214
215    pub fn height(&self) -> u32 {
216        self.height
217    }
218
219    /// Return the full rendered buffer as a multi-line string.
220    ///
221    /// Each row is trimmed of trailing spaces and joined with newlines.
222    /// Useful for snapshot testing with `insta::assert_snapshot!`.
223    pub fn to_string_trimmed(&self) -> String {
224        let mut lines = Vec::with_capacity(self.height as usize);
225        for y in 0..self.height {
226            lines.push(self.line(y));
227        }
228        while lines.last().is_some_and(|l| l.is_empty()) {
229            lines.pop();
230        }
231        lines.join("\n")
232    }
233}
234
235impl std::fmt::Display for TestBackend {
236    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
237        write!(f, "{}", self.to_string_trimmed())
238    }
239}