Skip to main content

kui_native/
testing.rs

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