Skip to main content

slt/
test_utils.rs

1//! Headless testing utilities.
2//!
3//! [`TestBackend`] renders a UI closure to an in-memory buffer without a real
4//! terminal. [`EventBuilder`] constructs event sequences for simulating user
5//! input. Together they enable snapshot and assertion-based UI testing.
6
7use 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
16/// Builder for constructing a sequence of input [`Event`]s.
17///
18/// Chain calls to [`key`](EventBuilder::key), [`click`](EventBuilder::click),
19/// [`scroll_up`](EventBuilder::scroll_up), etc., then call
20/// [`build`](EventBuilder::build) to get the final `Vec<Event>`.
21///
22/// # Example
23///
24/// ```
25/// use slt::EventBuilder;
26/// use slt::KeyCode;
27///
28/// let events = EventBuilder::new()
29///     .key('a')
30///     .key_code(KeyCode::Enter)
31///     .build();
32/// assert_eq!(events.len(), 2);
33/// ```
34pub struct EventBuilder {
35    events: Vec<Event>,
36}
37
38impl EventBuilder {
39    /// Create an empty event builder.
40    pub fn new() -> Self {
41        Self { events: Vec::new() }
42    }
43
44    /// Append a character key-press event.
45    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    /// Append a special key-press event (arrows, Enter, Esc, etc.).
55    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    /// Append a key-press event with modifier keys (Ctrl, Shift, Alt).
65    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    /// Append a left mouse click at terminal position `(x, y)`.
75    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    /// Append a left mouse button release at terminal position `(x, y)`.
88    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    /// Append a mouse drag (movement with the left button held) at `(x, y)`.
94    pub fn drag(mut self, x: u32, y: u32) -> Self {
95        self.events.push(Event::mouse_drag(x, y));
96        self
97    }
98
99    /// Append a key-release event for character `c`.
100    ///
101    /// Only meaningful on terminals that emit release events
102    /// (e.g. with the Kitty keyboard protocol enabled).
103    pub fn key_release(mut self, c: char) -> Self {
104        self.events.push(Event::key_release(c));
105        self
106    }
107
108    /// Append a terminal focus-gained event.
109    pub fn focus_gained(mut self) -> Self {
110        self.events.push(Event::FocusGained);
111        self
112    }
113
114    /// Append a terminal focus-lost event.
115    pub fn focus_lost(mut self) -> Self {
116        self.events.push(Event::FocusLost);
117        self
118    }
119
120    /// Append a scroll-up event at `(x, y)`.
121    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    /// Append a scroll-down event at `(x, y)`.
134    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    /// Append a bracketed-paste event.
147    pub fn paste(mut self, text: impl Into<String>) -> Self {
148        self.events.push(Event::Paste(text.into()));
149        self
150    }
151
152    /// Append a terminal resize event.
153    pub fn resize(mut self, width: u32, height: u32) -> Self {
154        self.events.push(Event::Resize(width, height));
155        self
156    }
157
158    /// Consume the builder and return the event sequence.
159    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
170/// Headless rendering backend for tests.
171///
172/// Renders a UI closure to an in-memory [`Buffer`] without a real terminal.
173/// Use [`render`](TestBackend::render) to run one frame, then inspect the
174/// output with [`line`](TestBackend::line), [`assert_contains`](TestBackend::assert_contains),
175/// or [`to_string_trimmed`](TestBackend::to_string_trimmed).
176/// Session state persists across renders, so multi-frame tests can exercise
177/// hooks, focus, and previous-frame hit testing.
178///
179/// # Example
180///
181/// ```
182/// use slt::TestBackend;
183///
184/// let mut backend = TestBackend::new(40, 10);
185/// backend.render(|ui| {
186///     ui.text("hello");
187/// });
188/// backend.assert_contains("hello");
189/// ```
190pub struct TestBackend {
191    buffer: Buffer,
192    width: u32,
193    height: u32,
194    frame_state: FrameState,
195    /// Frame history. `None` = recording disabled (zero overhead).
196    /// `Some(_)` = recording enabled — every [`render`](TestBackend::render)
197    /// call appends a [`FrameRecord`].
198    frames: Option<Vec<FrameRecord>>,
199}
200
201/// Snapshot of a single rendered frame, captured by
202/// [`TestBackend::record_frames`].
203///
204/// Stores the styled snapshot string (via [`Buffer::snapshot_format`]) plus a
205/// per-row trimmed text view for ergonomic substring assertions. Both are
206/// produced from the same buffer and are guaranteed to refer to the same
207/// frame.
208///
209/// Cheap to clone; useful for replaying a failing test by inspecting
210/// intermediate frames.
211#[derive(Clone, Debug, PartialEq, Eq)]
212pub struct FrameRecord {
213    /// Styled snapshot of the buffer at this frame, in the stable
214    /// [`Buffer::snapshot_format`] vocabulary.
215    pub snapshot: String,
216    /// Plain-text view of each buffer row, trailing spaces trimmed.
217    /// Mirrors [`TestBackend::line`] for every row.
218    pub lines: Vec<String>,
219}
220
221impl FrameRecord {
222    /// Return the frame as a multi-line string (rows joined with `\n`,
223    /// trailing empty rows preserved). Mirrors [`TestBackend::to_string_trimmed`]
224    /// on the originating buffer.
225    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    /// Return the trimmed text of row `y` from this frame, or empty if `y`
234    /// is past the buffer height.
235    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    /// Assert any row in this frame contains `expected`. Panics with a
243    /// row-by-row dump on failure.
244    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    /// Create a test backend with the given terminal dimensions.
260    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    /// Enable frame recording.
272    ///
273    /// After this call, every subsequent [`render`](TestBackend::render),
274    /// [`render_with_events`](TestBackend::render_with_events), and
275    /// [`run_with_events`](TestBackend::run_with_events) call appends a
276    /// [`FrameRecord`] to the internal history. Disabled by default so tests
277    /// that don't need history pay zero memory overhead.
278    ///
279    /// Returns `self` for chaining.
280    ///
281    /// # Example
282    ///
283    /// ```
284    /// use slt::TestBackend;
285    ///
286    /// let mut tb = TestBackend::new(20, 3).record_frames();
287    /// for n in 0..3 {
288    ///     tb.render(|ui| {
289    ///         ui.text(format!("frame {n}"));
290    ///     });
291    /// }
292    /// assert_eq!(tb.frames().len(), 3);
293    /// tb.frames()[0].assert_contains("frame 0");
294    /// tb.frames()[2].assert_contains("frame 2");
295    /// ```
296    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    /// Return all captured frame snapshots in chronological order.
304    ///
305    /// Returns an empty slice if [`record_frames`](TestBackend::record_frames)
306    /// was never called on this backend.
307    pub fn frames(&self) -> &[FrameRecord] {
308        self.frames.as_deref().unwrap_or(&[])
309    }
310
311    /// Capture the current buffer state into the recording, if enabled.
312    ///
313    /// No-op when recording is off — keeps the hot path allocation-free
314    /// for the common case.
315    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    /// Run a UI closure for one frame and render to the internal buffer.
360    pub fn render(&mut self, f: impl FnOnce(&mut Context)) {
361        self.render_frame(Vec::new(), |_| {}, f);
362    }
363
364    /// Render with injected events and focus state for interaction testing.
365    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    /// Convenience wrapper: render with events using default focus state.
383    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    /// Get the rendered text content of row y (trimmed trailing spaces)
388    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    /// Assert that row y contains `expected` as a substring
397    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    /// Assert that row y contains `expected` as a substring
406    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    /// Assert that any line in the buffer contains `expected`
415    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    /// Access the underlying render buffer.
429    pub fn buffer(&self) -> &Buffer {
430        &self.buffer
431    }
432
433    /// Terminal width used for this backend.
434    pub fn width(&self) -> u32 {
435        self.width
436    }
437
438    /// Terminal height used for this backend.
439    pub fn height(&self) -> u32 {
440        self.height
441    }
442
443    /// Return the full rendered buffer as a multi-line string.
444    ///
445    /// Each row is trimmed of trailing spaces and joined with newlines.
446    /// Useful for snapshot testing with `insta::assert_snapshot!`.
447    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    // ---- Negative assertions (#232) ---------------------------------------
459
460    /// Assert that no row in the buffer contains `expected` as a substring.
461    ///
462    /// Panics with the offending row indices and contents on failure.
463    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    /// Assert that row `y` does NOT contain `expected` as a substring.
482    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    /// Assert that row `y` is entirely blank (contains no non-space content).
491    ///
492    /// Useful for verifying that cleared, padded, or overflow-suppressed rows
493    /// render as empty.
494    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    /// Assert that the cell at `(x, y)` carries exactly the `expected` style.
500    ///
501    /// Useful for focused color/modifier regression checks without committing
502    /// to a full-buffer snapshot. Panics with `(x, y)`, the actual style, and
503    /// the expected style on mismatch.
504    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    // ---- Multi-step sequences + type_string (#230) ------------------------
513
514    /// Begin building a multi-step interaction sequence.
515    ///
516    /// Each [`tick`](TestSequence::tick) (or [`key`](TestSequence::key))
517    /// appends an event batch + render closure pair.
518    /// [`run`](TestSequence::run) executes them in order, advancing
519    /// `FrameState` naturally between steps so callers don't need to thread
520    /// `focus_index` / `prev_focus_count` manually.
521    ///
522    /// # Example
523    ///
524    /// ```
525    /// use slt::{KeyCode, TestBackend};
526    ///
527    /// let mut tb = TestBackend::new(20, 3);
528    /// tb.sequence()
529    ///     .tick(|ui| { ui.text("ready"); })
530    ///     .key(KeyCode::Esc, |ui| { ui.text("after esc"); })
531    ///     .run();
532    /// tb.assert_contains("after esc");
533    /// ```
534    pub fn sequence(&mut self) -> TestSequence<'_> {
535        TestSequence {
536            backend: self,
537            steps: Vec::new(),
538        }
539    }
540
541    /// Simulate typing `s` one character at a time, rendering with `render`
542    /// between each character.
543    ///
544    /// Each character produces a [`KeyCode::Char`] event with no modifiers.
545    /// Focus state is preserved across characters.
546    ///
547    /// # Example
548    ///
549    /// ```
550    /// use slt::TestBackend;
551    ///
552    /// let mut tb = TestBackend::new(20, 3);
553    /// let mut typed = String::new();
554    /// tb.type_string("hi", |ui| {
555    ///     ui.text(&typed);
556    /// });
557    /// // 2 characters → 2 frames rendered.
558    /// drop(typed);
559    /// ```
560    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            // Use render_frame directly so frame recording is preserved and
568            // FrameState advances naturally between characters.
569            self.render_frame(events, |_| {}, &mut render);
570        }
571    }
572}
573
574/// A single step in a [`TestSequence`].
575///
576/// Holds the event batch to inject, plus a render closure to execute. Created
577/// internally by [`TestSequence::tick`], [`TestSequence::key`],
578/// [`TestSequence::events`], etc.
579struct TestStep<'a> {
580    events: Vec<Event>,
581    render: Box<dyn FnOnce(&mut Context) + 'a>,
582}
583
584/// Builder returned by [`TestBackend::sequence`].
585///
586/// Chain step builders (`tick`, `key`, `type_string`, `events`) and finalize
587/// with [`run`](TestSequence::run). Steps execute sequentially, advancing
588/// `FrameState` between them so focus and hooks evolve naturally without the
589/// caller having to thread state.
590pub struct TestSequence<'a> {
591    backend: &'a mut TestBackend,
592    steps: Vec<TestStep<'a>>,
593}
594
595impl<'a> TestSequence<'a> {
596    /// Append a step that renders without injecting any events.
597    ///
598    /// Equivalent to a single frame tick — useful for letting hooks /
599    /// animations advance between input steps.
600    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    /// Append a step that fires a single key-press event with no modifiers.
609    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    /// Append a step that types `s` as a sequence of `KeyCode::Char` events
623    /// **before** invoking `render`.
624    ///
625    /// Unlike [`TestBackend::type_string`], this collapses every typed
626    /// character into a single render step — useful when the per-character
627    /// frame state is not the assertion target. For per-keystroke rendering,
628    /// chain individual `.key(...)` calls.
629    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    /// Append a step with an arbitrary event batch.
648    ///
649    /// Useful for mouse interactions, paste events, or sequences built
650    /// with [`EventBuilder`].
651    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    /// Execute every queued step in order. Returns control to the caller
660    /// (the [`TestBackend`] is borrowed mutably for the lifetime of the
661    /// sequence builder). Use [`TestBackend::buffer`] / `.frames()` /
662    /// `.assert_*` after `run()` returns.
663    pub fn run(self) {
664        let backend = self.backend;
665        for step in self.steps {
666            let TestStep { events, render } = step;
667            // Adapt FnOnce(&mut Context) into the &mut FnMut(&mut Context)
668            // shape that render_frame's internal trampoline already expects.
669            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    /// Regression test for issue #131: `mouse_up` produces `MouseKind::Up(Left)`.
692    #[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    /// Regression test for issue #131: `drag` produces a drag mouse event.
707    #[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    /// Regression test for issue #131: `key_release` produces a release key event.
722    #[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    /// Regression test for issue #131: focus_gained / focus_lost chain through builder.
736    #[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    // ---- #229 record_frames -------------------------------------------------
743
744    #[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        // Styled snapshot should encode the bold modifier somewhere.
775        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        // record_frames() called twice must not wipe prior history.
785        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        // The frame should have all 4 rows recorded.
801        assert_eq!(frame.lines.len(), 4);
802        // to_string_trimmed drops the trailing empty rows like TestBackend.
803        let s = frame.to_string_trimmed();
804        assert!(!s.ends_with('\n'));
805        assert!(s.starts_with("hello"));
806    }
807
808    // ---- #230 sequence + type_string ----------------------------------------
809
810    #[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        // We can't easily observe the key event without a stateful widget,
833        // but we can confirm the sequence builder ran the render closure.
834        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        // Sequence's type_string is one step → one frame, not three.
852        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    // ---- #232 negative assertions ------------------------------------------
890
891    #[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        // Line 0 has "first" but not "second".
920        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        // Row 1 is untouched after rendering one text → blank.
940        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}