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 pub fn input(&mut self, app: &mut impl App, ev: InputEvent) -> Vec<UiEvent> {
209 let out = self.core().handle_input(ev);
210 self.deliver(app, out.clone());
211 out
212 }
213
214 fn done(&mut self, app: &mut impl App, out: Vec<UiEvent>) -> Vec<UiEvent> {
215 if self.framing {
216 self.frame(app);
217 }
218 out
219 }
220
221 /// The pointer to a point, nothing pressed.
222 pub fn move_to(&mut self, app: &mut impl App, x: f32, y: f32) -> Vec<UiEvent> {
223 let out = self.input(app, InputEvent::CursorMoved(Vec2::new(x, y)));
224 self.done(app, out)
225 }
226
227 /// The pointer to the middle of the node `key` names.
228 pub fn hover(&mut self, app: &mut impl App, key: Key) -> Vec<UiEvent> {
229 let Some(c) = self.rect_of(key).map(|r| r.center()) else {
230 return Vec::new();
231 };
232 self.move_to(app, c.x, c.y)
233 }
234
235 fn press_at(&mut self, app: &mut impl App, at: Vec2, clicks: u8) -> Vec<UiEvent> {
236 let mut out = self.input(app, InputEvent::CursorMoved(at));
237 out.extend(self.input(app, InputEvent::mouse_down(clicks)));
238 out.extend(self.input(app, InputEvent::mouse_up()));
239 out
240 }
241
242 /// A primary click at a point: move, press, release.
243 pub fn click(&mut self, app: &mut impl App, x: f32, y: f32) -> Vec<UiEvent> {
244 let out = self.press_at(app, Vec2::new(x, y), 1);
245 self.done(app, out)
246 }
247
248 /// Two clicks at a point, the second counted as the second, as the OS
249 /// counts a double click into the press.
250 pub fn double_click(&mut self, app: &mut impl App, x: f32, y: f32) -> Vec<UiEvent> {
251 let at = Vec2::new(x, y);
252 let mut out = self.press_at(app, at, 1);
253 out.extend(self.press_at(app, at, 2));
254 self.done(app, out)
255 }
256
257 /// A click on the node `key` names, the way assistive technology
258 /// presses it — no geometry needed.
259 pub fn click_key(&mut self, app: &mut impl App, key: Key) -> Vec<UiEvent> {
260 let out = self.input(
261 app,
262 InputEvent::Access(AccessRequest::new(key, AccessAction::Click)),
263 );
264 self.done(app, out)
265 }
266
267 /// A press at `from`, the pointer taken past the click slop and on to
268 /// `to`, the release: an `on_drag` node hears start, moves and end,
269 /// and the release is no click.
270 pub fn drag(&mut self, app: &mut impl App, from: Vec2, to: Vec2) -> Vec<UiEvent> {
271 let mut out = self.input(app, InputEvent::CursorMoved(from));
272 out.extend(self.input(app, InputEvent::mouse_down(1)));
273 let past = Vec2::new(from.x + 8.0, from.y + 8.0);
274 out.extend(self.input(app, InputEvent::CursorMoved(past)));
275 out.extend(self.input(app, InputEvent::CursorMoved(to)));
276 if self.framing {
277 self.frame(app);
278 }
279 out.extend(self.input(app, InputEvent::mouse_up()));
280 self.done(app, out)
281 }
282
283 /// The wheel over a point.
284 pub fn wheel(&mut self, app: &mut impl App, x: f32, y: f32, dx: f32, dy: f32) -> Vec<UiEvent> {
285 let mut out = self.input(app, InputEvent::CursorMoved(Vec2::new(x, y)));
286 out.extend(self.input(app, InputEvent::Scroll(Vec2::new(dx, dy))));
287 self.done(app, out)
288 }
289
290 /// One event of a scroll gesture over a point: `begins` on its first,
291 /// and the rest go to the target that first one picked, wherever the
292 /// pointer or the content has gone since, as a native swipe latches.
293 /// [`Self::wheel`] is a gesture of its own.
294 pub fn scroll_gesture(
295 &mut self,
296 app: &mut impl App,
297 x: f32,
298 y: f32,
299 delta: Vec2,
300 begins: bool,
301 ) -> Vec<UiEvent> {
302 let mut out = self.input(app, InputEvent::CursorMoved(Vec2::new(x, y)));
303 out.extend(self.input(app, InputEvent::ScrollGesture { delta, begins }));
304 self.done(app, out)
305 }
306
307 /// A non-primary button pressed and released at a point: what an
308 /// `on_button` node claiming it hears as `press` and `release`.
309 pub fn button_click(
310 &mut self,
311 app: &mut impl App,
312 x: f32,
313 y: f32,
314 button: MouseButton,
315 ) -> Vec<UiEvent> {
316 let mut out = self.input(app, InputEvent::CursorMoved(Vec2::new(x, y)));
317 out.extend(self.input(app, InputEvent::MouseDown { button, clicks: 1 }));
318 out.extend(self.input(app, InputEvent::MouseUp { button }));
319 self.done(app, out)
320 }
321
322 /// A key pressed and released, by the name a binding spells it
323 /// (`"a"`, `"enter"`, `"f2"`): the raw press — carrying the character
324 /// (or the space) as its text when no chord modifier is held, as a
325 /// keyboard's would — then the editor event the same press means
326 /// (`KeyPress::edit_event`, the one table every driver sends from),
327 /// then the release.
328 pub fn key(&mut self, app: &mut impl App, name: &str, mods: KeyMods) -> Vec<UiEvent> {
329 let code = KeyCode::from_name(name).unwrap_or(KeyCode::Unknown);
330 let mut press = KeyPress::new(code, mods);
331 let chord = mods.ctrl || mods.alt || mods.super_key;
332 match code {
333 KeyCode::Char(c) if !chord => press = press.with_text(c.to_string()),
334 KeyCode::Space if !chord => press = press.with_text(" "),
335 _ => {}
336 }
337 let mut out = self.input(app, InputEvent::KeyDown(press.clone()));
338 if let Some(ev) = press.edit_event() {
339 out.extend(self.input(app, ev));
340 }
341 out.extend(self.input(app, InputEvent::KeyUp(press.released())));
342 self.done(app, out)
343 }
344
345 /// `keys(app, "jj ww")`: [`Self::key`] for each character, unmodified,
346 /// a space being the space key.
347 pub fn keys(&mut self, app: &mut impl App, seq: &str) -> Vec<UiEvent> {
348 let mut out = Vec::new();
349 for c in seq.chars() {
350 let name = if c == ' ' {
351 "space".to_string()
352 } else {
353 c.to_string()
354 };
355 out.extend(self.key(app, &name, KeyMods::NONE));
356 }
357 out
358 }
359
360 /// Typed text, as the OS delivers it to the focused editor.
361 pub fn text(&mut self, app: &mut impl App, s: &str) -> Vec<UiEvent> {
362 let out = self.input(app, InputEvent::Text(s.to_string()));
363 self.done(app, out)
364 }
365
366 /// Text that did not come from a key press — an IME's commit — as the
367 /// OS delivers it: to a focused editor, or to a key sink as `text`.
368 pub fn commit(&mut self, app: &mut impl App, s: &str) -> Vec<UiEvent> {
369 let out = self.input(app, InputEvent::Commit(s.to_string()));
370 self.done(app, out)
371 }
372
373 /// Focuses the node `key` names, as Tab or a screen reader would.
374 pub fn focus(&mut self, app: &mut impl App, key: Key) -> Vec<UiEvent> {
375 let out = self.input(
376 app,
377 InputEvent::Access(AccessRequest::new(key, AccessAction::Focus)),
378 );
379 self.done(app, out)
380 }
381
382 /// `Ok` when `cond` holds, else `Err(what)`, so a scripted drive reads
383 /// as a list of checks. Prints `ok <what>` on success.
384 pub fn check(&self, cond: bool, what: &str) -> Result<(), String> {
385 if cond {
386 println!(" ok {what}");
387 Ok(())
388 } else {
389 Err(what.to_string())
390 }
391 }
392
393 /// Everything delivered so far, one line each: `frame key payload`.
394 pub fn log(&self) -> &[String] {
395 &self.log
396 }
397
398 fn deliver(&mut self, app: &mut impl App, events: Vec<UiEvent>) {
399 for ev in &events {
400 self.log.push(format!(
401 "{:>4} {:08x} {}",
402 self.frame,
403 ev.key.0 as u32,
404 crate::devtools::fmt_value(&ev.payload)
405 ));
406 }
407 let core = self.core.borrow_mut();
408 self.exts.route(events, |ev| app.on_event_with(ev, core));
409 }
410}