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}