Skip to main content

kui_native/
testing.rs

1//! A headless driver for an [`App`]: clicks, keys, typed text and drags
2//! through the real `view` and `on_event`, with no window and no GPU.
3//!
4//! [`Drive`] owns (or borrows) a [`Core`] and a viewport. A test frames the
5//! app, finds nodes by the label they were keyed under, sends gestures and
6//! reads back what the frame drew. It runs under plain `cargo test`.
7//!
8//! ```rust
9//! use kui_native::testing::Drive;
10//! use kui_native::widgets;
11//! use kui_native::{App, Core, Message, NodeSpec, TextStyle, Ui, UiEvent};
12//!
13//! #[derive(Message, Clone, Debug, PartialEq)]
14//! enum Msg {
15//!     Add,
16//! }
17//!
18//! #[derive(Default)]
19//! struct Counter {
20//!     count: u32,
21//! }
22//!
23//! impl App for Counter {
24//!     fn view(&mut self, ui: &mut Ui<'_>) {
25//!         ui.with(NodeSpec::column().fill().gap(8.0), |ui| {
26//!             widgets::button(ui, "Add", Msg::Add);
27//!             ui.text_in_keyed(
28//!                 "summary",
29//!                 NodeSpec::row(),
30//!                 &format!("{} added", self.count),
31//!                 TextStyle::new(12.0),
32//!             );
33//!         });
34//!     }
35//!
36//!     fn on_event(&mut self, ev: UiEvent) {
37//!         if ev.message::<Msg>() == Some(Msg::Add) {
38//!             self.count += 1;
39//!         }
40//!     }
41//! }
42//!
43//! let mut app = Counter::default();
44//! // `framing` builds a frame after every gesture, so the view has caught
45//! // up before the next assert.
46//! let mut d = Drive::new(Core::new(), 320.0, 200.0).framing();
47//! d.frame(&mut app);
48//!
49//! // Click by label, the way a screen reader presses: no geometry needed.
50//! let add = d.key_of("Add").expect("an Add button");
51//! d.click_key(&mut app, add);
52//! assert_eq!(app.count, 1);
53//! assert_eq!(d.texts_under("summary"), ["1 added"]);
54//! // Nothing the core saw was misdeclared.
55//! assert!(d.warnings().is_empty());
56//! ```
57//!
58//! Without [`Drive::framing`], a gesture sends its inputs and stops, and
59//! the test calls [`Drive::frame`] when it wants the view to catch up.
60//! Events reach the app as the windowed runner sends them: an extension's
61//! to the extension, the rest to `on_event`, each one logged
62//! ([`Drive::log`]).
63
64use std::borrow::{Borrow, BorrowMut};
65
66use crate::{
67    AccessAction, AccessRequest, App, Core, Extension, Extensions, InputEvent, Key, KeyCode,
68    KeyMods, KeyPress, MouseButton, Rect, Size, UiEvent, Vec2,
69};
70
71/// The headless driver; see the [module docs](self) for an example.
72///
73/// `C` is the core it drives: its own `Core` by default, or a `&mut Core`
74/// borrowed from a harness that built one.
75pub struct Drive<C: BorrowMut<Core> = Core> {
76    /// The core being driven.
77    pub core: C,
78    /// The extensions filling the frame's slots, routed as the runner
79    /// routes them. Empty unless [`Drive::extension`] loaded one.
80    pub exts: Extensions,
81    /// The viewport every frame lays out against, logical px.
82    pub viewport: Size,
83    /// The display's scale: 1 unless a test sets it.
84    pub scale: f32,
85    framing: bool,
86    frame: u64,
87    now: f64,
88    log: Vec<String>,
89}
90
91impl<C: BorrowMut<Core>> Drive<C> {
92    /// A drive over `core` with a `w` by `h` viewport. Turns the node
93    /// snapshot on (`Core::set_inspect`) so [`Self::texts_under`] and
94    /// [`Self::rect_of`] have a frame to read.
95    pub fn new(mut core: C, w: f32, h: f32) -> Self {
96        core.borrow_mut().set_inspect(true);
97        Drive {
98            core,
99            exts: Extensions::new(),
100            viewport: Size::new(w, h),
101            scale: 1.0,
102            framing: false,
103            frame: 0,
104            now: 0.0,
105            log: Vec::new(),
106        }
107    }
108
109    /// Frames after every gesture (a click, a key, typed text, the wheel,
110    /// a hover, a drag), and once while a drag is held.
111    pub fn framing(mut self) -> Self {
112        self.framing = true;
113        self
114    }
115
116    fn core(&mut self) -> &mut Core {
117        self.core.borrow_mut()
118    }
119
120    /// Loads an extension under `ns`, as `Launcher::extension_as` would.
121    pub fn extension(&mut self, ns: &str, ext: impl Extension + 'static) -> Result<(), String> {
122        self.exts.push_as(ns, Box::new(ext))
123    }
124
125    /// Builds one frame from `app.view`, then hands `app` whatever the
126    /// frame produced on its own (a `resize`, a `layout`, a window event).
127    pub fn frame(&mut self, app: &mut impl App) {
128        self.frame += 1;
129        let (viewport, scale, now) = (self.viewport, self.scale, self.now);
130        let core = self.core.borrow_mut();
131        core.set_time(now);
132        let mut ui = core.frame_with(viewport, scale, &mut self.exts);
133        app.view(&mut ui);
134        ui.finish();
135        let pending = self.core().take_pending_events();
136        self.deliver(app, pending);
137    }
138
139    /// Moves the frame clock `secs` forward; the next `frame` sees it.
140    pub fn advance(&mut self, secs: f64) {
141        self.now += secs;
142    }
143
144    /// The frames built so far.
145    pub fn frames(&self) -> u64 {
146        self.frame
147    }
148
149    /// The key of the node opened under the key label `label` — the
150    /// name the view gave it (`with_keyed("see-date", ..)`, a `key`
151    /// prop) — from the last frame. Not the accessible name a reader
152    /// hears, which is [`Self::key_named`]'s.
153    pub fn key_of(&mut self, label: &str) -> Option<Key> {
154        self.core().key_of(label)
155    }
156
157    /// The key of the first node in the last frame whose accessible name
158    /// is `name` — its `label` row, else its own text, else a control's
159    /// derived name (a button's text) — so a test presses "the button
160    /// named Like" as a reader would. Two with the name raise
161    /// `ambiguous-name` ([`Self::warnings`]); see `Core::key_named`.
162    pub fn key_named(&mut self, name: &str) -> Option<Key> {
163        self.core().key_named(name)
164    }
165
166    /// Where the last frame put the node `key` names, in logical px:
167    /// where it is hit when it takes input (clipped, on top), else where
168    /// layout put it.
169    pub fn rect_of(&self, key: Key) -> Option<Rect> {
170        let core: &Core = self.core.borrow();
171        let hit = core
172            .interaction
173            .hits()
174            .iter()
175            .rev()
176            .find(|h| h.key == key)
177            .map(|h| h.rect);
178        hit.or_else(|| core.nodes().iter().find(|n| n.key == key).map(|n| n.rect))
179    }
180
181    /// The text of every text node under the first node opened under the
182    /// key label `label` in the last frame, in tree order; empty when none
183    /// was. The key label, as [`Self::key_of`] reads it, not the `label`
184    /// row a reader hears.
185    pub fn texts_under(&self, label: &str) -> Vec<String> {
186        let nodes = Borrow::<Core>::borrow(&self.core).nodes();
187        let Some(at) = nodes.iter().position(|n| n.label.as_deref() == Some(label)) else {
188            return Vec::new();
189        };
190        nodes[at + 1..]
191            .iter()
192            .take_while(|n| n.depth > nodes[at].depth)
193            .filter_map(|n| n.text.clone())
194            .collect()
195    }
196
197    /// Every warning the core raised since the last call, as
198    /// `code: message`: a test asserts it empty.
199    pub fn warnings(&mut self) -> Vec<String> {
200        self.core()
201            .take_warnings()
202            .into_iter()
203            .map(|w| format!("{}: {}", w.code, w.message))
204            .collect()
205    }
206
207    /// One input, its events to `app`, and back to the caller too.
208    /// The core reads the clock as [`Self::advance`] left it, as the
209    /// runner stamps it before input, so a handler after an `advance`
210    /// reads the advanced time and not the last frame's.
211    pub fn input(&mut self, app: &mut impl App, ev: InputEvent) -> Vec<UiEvent> {
212        let now = self.now;
213        self.core().set_time(now);
214        let out = self.core().handle_input(ev);
215        self.deliver(app, out.clone());
216        out
217    }
218
219    fn done(&mut self, app: &mut impl App, out: Vec<UiEvent>) -> Vec<UiEvent> {
220        if self.framing {
221            self.frame(app);
222        }
223        out
224    }
225
226    /// The pointer to a point, nothing pressed.
227    pub fn move_to(&mut self, app: &mut impl App, x: f32, y: f32) -> Vec<UiEvent> {
228        let out = self.input(app, InputEvent::CursorMoved(Vec2::new(x, y)));
229        self.done(app, out)
230    }
231
232    /// The pointer to the middle of the node `key` names.
233    pub fn hover(&mut self, app: &mut impl App, key: Key) -> Vec<UiEvent> {
234        let Some(c) = self.rect_of(key).map(|r| r.center()) else {
235            return Vec::new();
236        };
237        self.move_to(app, c.x, c.y)
238    }
239
240    fn press_at(&mut self, app: &mut impl App, at: Vec2, clicks: u8) -> Vec<UiEvent> {
241        let mut out = self.input(app, InputEvent::CursorMoved(at));
242        out.extend(self.input(app, InputEvent::mouse_down(clicks)));
243        out.extend(self.input(app, InputEvent::mouse_up()));
244        out
245    }
246
247    /// A primary click at a point: move, press, release.
248    pub fn click(&mut self, app: &mut impl App, x: f32, y: f32) -> Vec<UiEvent> {
249        let out = self.press_at(app, Vec2::new(x, y), 1);
250        self.done(app, out)
251    }
252
253    /// Two clicks at a point, the second counted as the second, as the OS
254    /// counts a double click into the press.
255    pub fn double_click(&mut self, app: &mut impl App, x: f32, y: f32) -> Vec<UiEvent> {
256        let at = Vec2::new(x, y);
257        let mut out = self.press_at(app, at, 1);
258        out.extend(self.press_at(app, at, 2));
259        self.done(app, out)
260    }
261
262    /// A click on the node `key` names, the way assistive technology
263    /// presses it — no geometry needed.
264    pub fn click_key(&mut self, app: &mut impl App, key: Key) -> Vec<UiEvent> {
265        let out = self.input(
266            app,
267            InputEvent::Access(AccessRequest::new(key, AccessAction::Click)),
268        );
269        self.done(app, out)
270    }
271
272    /// A press at `from`, the pointer taken past the click slop and on to
273    /// `to`, the release: an `on_drag` node hears start, moves and end,
274    /// and the release is no click.
275    pub fn drag(&mut self, app: &mut impl App, from: Vec2, to: Vec2) -> Vec<UiEvent> {
276        let mut out = self.input(app, InputEvent::CursorMoved(from));
277        out.extend(self.input(app, InputEvent::mouse_down(1)));
278        let past = Vec2::new(from.x + 8.0, from.y + 8.0);
279        out.extend(self.input(app, InputEvent::CursorMoved(past)));
280        out.extend(self.input(app, InputEvent::CursorMoved(to)));
281        if self.framing {
282            self.frame(app);
283        }
284        out.extend(self.input(app, InputEvent::mouse_up()));
285        self.done(app, out)
286    }
287
288    /// The wheel over a point.
289    pub fn wheel(&mut self, app: &mut impl App, x: f32, y: f32, dx: f32, dy: f32) -> Vec<UiEvent> {
290        let mut out = self.input(app, InputEvent::CursorMoved(Vec2::new(x, y)));
291        out.extend(self.input(app, InputEvent::Scroll(Vec2::new(dx, dy))));
292        self.done(app, out)
293    }
294
295    /// One event of a scroll gesture over a point: `begins` on its first,
296    /// and the rest go to the target that first one picked, wherever the
297    /// pointer or the content has gone since, as a native swipe latches.
298    /// [`Self::wheel`] is a gesture of its own.
299    pub fn scroll_gesture(
300        &mut self,
301        app: &mut impl App,
302        x: f32,
303        y: f32,
304        delta: Vec2,
305        begins: bool,
306    ) -> Vec<UiEvent> {
307        let mut out = self.input(app, InputEvent::CursorMoved(Vec2::new(x, y)));
308        out.extend(self.input(app, InputEvent::ScrollGesture { delta, begins }));
309        self.done(app, out)
310    }
311
312    /// A non-primary button pressed and released at a point: what an
313    /// `on_button` node claiming it hears as `press` and `release`.
314    pub fn button_click(
315        &mut self,
316        app: &mut impl App,
317        x: f32,
318        y: f32,
319        button: MouseButton,
320    ) -> Vec<UiEvent> {
321        let mut out = self.input(app, InputEvent::CursorMoved(Vec2::new(x, y)));
322        out.extend(self.input(app, InputEvent::MouseDown { button, clicks: 1 }));
323        out.extend(self.input(app, InputEvent::MouseUp { button }));
324        self.done(app, out)
325    }
326
327    /// A key pressed and released, by the name a binding spells it
328    /// (`"a"`, `"enter"`, `"f2"`): the raw press — carrying the character
329    /// (or the space) as its text when no chord modifier is held, as a
330    /// keyboard's would — then the editor event the same press means
331    /// (`KeyPress::edit_event`, the one table every driver sends from),
332    /// then the release.
333    pub fn key(&mut self, app: &mut impl App, name: &str, mods: KeyMods) -> Vec<UiEvent> {
334        let code = KeyCode::from_name(name).unwrap_or(KeyCode::Unknown);
335        let mut press = KeyPress::new(code, mods);
336        let chord = mods.ctrl || mods.alt || mods.super_key;
337        match code {
338            KeyCode::Char(c) if !chord => press = press.with_text(c.to_string()),
339            KeyCode::Space if !chord => press = press.with_text(" "),
340            _ => {}
341        }
342        let mut out = self.input(app, InputEvent::KeyDown(press.clone()));
343        if let Some(ev) = press.edit_event() {
344            out.extend(self.input(app, ev));
345        }
346        out.extend(self.input(app, InputEvent::KeyUp(press.released())));
347        self.done(app, out)
348    }
349
350    /// `keys(app, "jj ww")`: [`Self::key`] for each character, unmodified,
351    /// a space being the space key.
352    pub fn keys(&mut self, app: &mut impl App, seq: &str) -> Vec<UiEvent> {
353        let mut out = Vec::new();
354        for c in seq.chars() {
355            let name = if c == ' ' {
356                "space".to_string()
357            } else {
358                c.to_string()
359            };
360            out.extend(self.key(app, &name, KeyMods::NONE));
361        }
362        out
363    }
364
365    /// Typed text, as the OS delivers it to the focused editor.
366    pub fn text(&mut self, app: &mut impl App, s: &str) -> Vec<UiEvent> {
367        let out = self.input(app, InputEvent::Text(s.to_string()));
368        self.done(app, out)
369    }
370
371    /// Text that did not come from a key press — an IME's commit — as the
372    /// OS delivers it: to a focused editor, or to a key sink as `text`.
373    pub fn commit(&mut self, app: &mut impl App, s: &str) -> Vec<UiEvent> {
374        let out = self.input(app, InputEvent::Commit(s.to_string()));
375        self.done(app, out)
376    }
377
378    /// Focuses the node `key` names, as Tab or a screen reader would.
379    pub fn focus(&mut self, app: &mut impl App, key: Key) -> Vec<UiEvent> {
380        let out = self.input(
381            app,
382            InputEvent::Access(AccessRequest::new(key, AccessAction::Focus)),
383        );
384        self.done(app, out)
385    }
386
387    /// `Ok` when `cond` holds, else `Err(what)`, so a scripted drive reads
388    /// as a list of checks. Prints `ok <what>` on success.
389    pub fn check(&self, cond: bool, what: &str) -> Result<(), String> {
390        if cond {
391            println!("  ok   {what}");
392            Ok(())
393        } else {
394            Err(what.to_string())
395        }
396    }
397
398    /// Everything delivered so far, one line each: `frame key payload`.
399    pub fn log(&self) -> &[String] {
400        &self.log
401    }
402
403    fn deliver(&mut self, app: &mut impl App, events: Vec<UiEvent>) {
404        for ev in &events {
405            self.log.push(format!(
406                "{:>4} {:08x} {}",
407                self.frame,
408                ev.key.0 as u32,
409                crate::devtools::fmt_value(&ev.payload)
410            ));
411        }
412        let core = self.core.borrow_mut();
413        self.exts.route(events, |ev| app.on_event_with(ev, core));
414    }
415}