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 a node was opened under `label`, from the last frame.
150    pub fn key_of(&mut self, label: &str) -> Option<Key> {
151        self.core().key_of(label)
152    }
153
154    /// Where the last frame put the node `key` names, in logical px:
155    /// where it is hit when it takes input (clipped, on top), else where
156    /// layout put it.
157    pub fn rect_of(&self, key: Key) -> Option<Rect> {
158        let core: &Core = self.core.borrow();
159        let hit = core
160            .interaction
161            .hits()
162            .iter()
163            .rev()
164            .find(|h| h.key == key)
165            .map(|h| h.rect);
166        hit.or_else(|| core.nodes().iter().find(|n| n.key == key).map(|n| n.rect))
167    }
168
169    /// The text of every text node under the first node labelled `label`
170    /// in the last frame, in tree order; empty when none is labelled so.
171    pub fn texts_under(&self, label: &str) -> Vec<String> {
172        let nodes = Borrow::<Core>::borrow(&self.core).nodes();
173        let Some(at) = nodes.iter().position(|n| n.label.as_deref() == Some(label)) else {
174            return Vec::new();
175        };
176        nodes[at + 1..]
177            .iter()
178            .take_while(|n| n.depth > nodes[at].depth)
179            .filter_map(|n| n.text.clone())
180            .collect()
181    }
182
183    /// Every warning the core raised since the last call, as
184    /// `code: message`: a test asserts it empty.
185    pub fn warnings(&mut self) -> Vec<String> {
186        self.core()
187            .take_warnings()
188            .into_iter()
189            .map(|w| format!("{}: {}", w.code, w.message))
190            .collect()
191    }
192
193    /// One input, its events to `app`, and back to the caller too.
194    pub fn input(&mut self, app: &mut impl App, ev: InputEvent) -> Vec<UiEvent> {
195        let out = self.core().handle_input(ev);
196        self.deliver(app, out.clone());
197        out
198    }
199
200    fn done(&mut self, app: &mut impl App, out: Vec<UiEvent>) -> Vec<UiEvent> {
201        if self.framing {
202            self.frame(app);
203        }
204        out
205    }
206
207    /// The pointer to a point, nothing pressed.
208    pub fn move_to(&mut self, app: &mut impl App, x: f32, y: f32) -> Vec<UiEvent> {
209        let out = self.input(app, InputEvent::CursorMoved(Vec2::new(x, y)));
210        self.done(app, out)
211    }
212
213    /// The pointer to the middle of the node `key` names.
214    pub fn hover(&mut self, app: &mut impl App, key: Key) -> Vec<UiEvent> {
215        let Some(c) = self.rect_of(key).map(|r| r.center()) else {
216            return Vec::new();
217        };
218        self.move_to(app, c.x, c.y)
219    }
220
221    fn press_at(&mut self, app: &mut impl App, at: Vec2, clicks: u8) -> Vec<UiEvent> {
222        let mut out = self.input(app, InputEvent::CursorMoved(at));
223        out.extend(self.input(app, InputEvent::mouse_down(clicks)));
224        out.extend(self.input(app, InputEvent::mouse_up()));
225        out
226    }
227
228    /// A primary click at a point: move, press, release.
229    pub fn click(&mut self, app: &mut impl App, x: f32, y: f32) -> Vec<UiEvent> {
230        let out = self.press_at(app, Vec2::new(x, y), 1);
231        self.done(app, out)
232    }
233
234    /// Two clicks at a point, the second counted as the second, as the OS
235    /// counts a double click into the press.
236    pub fn double_click(&mut self, app: &mut impl App, x: f32, y: f32) -> Vec<UiEvent> {
237        let at = Vec2::new(x, y);
238        let mut out = self.press_at(app, at, 1);
239        out.extend(self.press_at(app, at, 2));
240        self.done(app, out)
241    }
242
243    /// A click on the node `key` names, the way assistive technology
244    /// presses it — no geometry needed.
245    pub fn click_key(&mut self, app: &mut impl App, key: Key) -> Vec<UiEvent> {
246        let out = self.input(
247            app,
248            InputEvent::Access(AccessRequest::new(key, AccessAction::Click)),
249        );
250        self.done(app, out)
251    }
252
253    /// A press at `from`, the pointer taken past the click slop and on to
254    /// `to`, the release: an `on_drag` node hears start, moves and end,
255    /// and the release is no click.
256    pub fn drag(&mut self, app: &mut impl App, from: Vec2, to: Vec2) -> Vec<UiEvent> {
257        let mut out = self.input(app, InputEvent::CursorMoved(from));
258        out.extend(self.input(app, InputEvent::mouse_down(1)));
259        let past = Vec2::new(from.x + 8.0, from.y + 8.0);
260        out.extend(self.input(app, InputEvent::CursorMoved(past)));
261        out.extend(self.input(app, InputEvent::CursorMoved(to)));
262        if self.framing {
263            self.frame(app);
264        }
265        out.extend(self.input(app, InputEvent::mouse_up()));
266        self.done(app, out)
267    }
268
269    /// The wheel over a point.
270    pub fn wheel(&mut self, app: &mut impl App, x: f32, y: f32, dx: f32, dy: f32) -> Vec<UiEvent> {
271        let mut out = self.input(app, InputEvent::CursorMoved(Vec2::new(x, y)));
272        out.extend(self.input(app, InputEvent::Scroll(Vec2::new(dx, dy))));
273        self.done(app, out)
274    }
275
276    /// One event of a scroll gesture over a point: `begins` on its first,
277    /// and the rest go to the target that first one picked, wherever the
278    /// pointer or the content has gone since, as a native swipe latches.
279    /// [`Self::wheel`] is a gesture of its own.
280    pub fn scroll_gesture(
281        &mut self,
282        app: &mut impl App,
283        x: f32,
284        y: f32,
285        delta: Vec2,
286        begins: bool,
287    ) -> Vec<UiEvent> {
288        let mut out = self.input(app, InputEvent::CursorMoved(Vec2::new(x, y)));
289        out.extend(self.input(app, InputEvent::ScrollGesture { delta, begins }));
290        self.done(app, out)
291    }
292
293    /// A non-primary button pressed and released at a point: what an
294    /// `on_button` node claiming it hears as `press` and `release`.
295    pub fn button_click(
296        &mut self,
297        app: &mut impl App,
298        x: f32,
299        y: f32,
300        button: MouseButton,
301    ) -> Vec<UiEvent> {
302        let mut out = self.input(app, InputEvent::CursorMoved(Vec2::new(x, y)));
303        out.extend(self.input(app, InputEvent::MouseDown { button, clicks: 1 }));
304        out.extend(self.input(app, InputEvent::MouseUp { button }));
305        self.done(app, out)
306    }
307
308    /// A key pressed and released, by the name a binding spells it
309    /// (`"a"`, `"enter"`, `"f2"`): the raw press — carrying the character
310    /// (or the space) as its text when no chord modifier is held, as a
311    /// keyboard's would — then the editor event the same press means
312    /// (`KeyPress::edit_event`, the one table every driver sends from),
313    /// then the release.
314    pub fn key(&mut self, app: &mut impl App, name: &str, mods: KeyMods) -> Vec<UiEvent> {
315        let code = KeyCode::from_name(name).unwrap_or(KeyCode::Unknown);
316        let mut press = KeyPress::new(code, mods);
317        let chord = mods.ctrl || mods.alt || mods.super_key;
318        match code {
319            KeyCode::Char(c) if !chord => press = press.with_text(c.to_string()),
320            KeyCode::Space if !chord => press = press.with_text(" "),
321            _ => {}
322        }
323        let mut out = self.input(app, InputEvent::KeyDown(press.clone()));
324        if let Some(ev) = press.edit_event() {
325            out.extend(self.input(app, ev));
326        }
327        out.extend(self.input(app, InputEvent::KeyUp(press.released())));
328        self.done(app, out)
329    }
330
331    /// `keys(app, "jj ww")`: [`Self::key`] for each character, unmodified,
332    /// a space being the space key.
333    pub fn keys(&mut self, app: &mut impl App, seq: &str) -> Vec<UiEvent> {
334        let mut out = Vec::new();
335        for c in seq.chars() {
336            let name = if c == ' ' {
337                "space".to_string()
338            } else {
339                c.to_string()
340            };
341            out.extend(self.key(app, &name, KeyMods::NONE));
342        }
343        out
344    }
345
346    /// Typed text, as the OS delivers it to the focused editor.
347    pub fn text(&mut self, app: &mut impl App, s: &str) -> Vec<UiEvent> {
348        let out = self.input(app, InputEvent::Text(s.to_string()));
349        self.done(app, out)
350    }
351
352    /// Text that did not come from a key press — an IME's commit — as the
353    /// OS delivers it: to a focused editor, or to a key sink as `text`.
354    pub fn commit(&mut self, app: &mut impl App, s: &str) -> Vec<UiEvent> {
355        let out = self.input(app, InputEvent::Commit(s.to_string()));
356        self.done(app, out)
357    }
358
359    /// Focuses the node `key` names, as Tab or a screen reader would.
360    pub fn focus(&mut self, app: &mut impl App, key: Key) -> Vec<UiEvent> {
361        let out = self.input(
362            app,
363            InputEvent::Access(AccessRequest::new(key, AccessAction::Focus)),
364        );
365        self.done(app, out)
366    }
367
368    /// `Ok` when `cond` holds, else `Err(what)`, so a scripted drive reads
369    /// as a list of checks. Prints `ok <what>` on success.
370    pub fn check(&self, cond: bool, what: &str) -> Result<(), String> {
371        if cond {
372            println!("  ok   {what}");
373            Ok(())
374        } else {
375            Err(what.to_string())
376        }
377    }
378
379    /// Everything delivered so far, one line each: `frame key payload`.
380    pub fn log(&self) -> &[String] {
381        &self.log
382    }
383
384    fn deliver(&mut self, app: &mut impl App, events: Vec<UiEvent>) {
385        for ev in &events {
386            self.log.push(format!(
387                "{:>4} {:08x} {}",
388                self.frame,
389                ev.key.0 as u32,
390                crate::devtools::fmt_value(&ev.payload)
391            ));
392        }
393        let core = self.core.borrow_mut();
394        self.exts.route(events, |ev| app.on_event_with(ev, core));
395    }
396}