Driving a screen
A harness that drives a screen the way a person does, finding controls by the
text on the panel, and the functions that feed the fake host one frame of input
at a time. Either way a test reaches focus, routing and dispatch as a device
does, under cargo test, and neither is compiled unless the crate's testing
feature is on.
Testing a screen without a screen is the guide, and its second layer is this page at work. Installing the fake host, and the numbers it answers with, is Testing; what was drawn, and how to assert on it, is Recording. This page is what each piece does.
Topics
Section titled “Topics”Driving a screen like a person
Section titled “Driving a screen like a person”testing::Ui | A screen under test, with the app and host that drive it. |
testing::Drive | A host that a test can feed input to. |
Feeding input and reading state
Section titled “Feeding input and reading state”testing::press | Reports one button press to the next frame the runtime reads input. |
testing::hold | Reports the edge and leaves the button down, as a finger does. |
testing::release | Lifts whatever hold put down. |
testing::next_frame | Ends the frame the fake is reporting input for. |
testing::set_swipe | Reports one swipe to the next frame the runtime reads input, so navigation can be tested without a finger. |
testing::set_millis | Moves the fake clock, so repeat timing is deterministic. |
testing::set_has_left_right_keys | Says whether the device the fake stands for has a Left/Right pair, so a control that branches on it can be tested both ways. |
testing::set_swipe_moves_selection | Chooses which way a swipe moves focus, so both readings can be tested. |
testing::Ui
Section titled “testing::Ui”A screen under test, with the app and host that drive it.
pub struct Ui<H: Host + Drive + 'static>Ui builds an App on a real backend, wrapped in a
Recorder, and finds controls by the text on the panel.
tap_text looks up where that string was painted and
taps the middle of it, so a test goes through hit testing, focus, scrolling and
dispatch exactly as a thumb does. Layout comes from the backend, so a test about
a particular panel size runs at that size.
Every action is one frame: the clock moves on 16 ms, the input is delivered
through Drive, the log is reset, the app
ticks, and it repaints if anything asked. frame and
visible_text read the last frame that painted.
Ui is not built for bare-metal targets (target_os = "none"): it needs std
for the lock.
The fake host does not implement Drive. A backend that does is
xpui-embedded-graphics's Backend, with its own testing feature on, so the
harness is written generic over the host and a test in that crate passes its
backend in.
Example — opening a screen from a menu
use xpui::host::Host;use xpui::testing::{Drive, Ui};use xpui::{Button, NavigationScreen, Screen, Text, View, vstack};
struct Menu;
impl Screen for Menu { type Message = (); fn body(&self) -> impl View<()> { NavigationScreen::new(vstack![0; Text::new("Lists")]) } fn update(&mut self, _message: ()) {}}
/// `backend` is a host that implements `Drive`, already sized for its panel.fn opens_the_list_screen<H: Host + Drive + 'static>(backend: &'static H) { let mut ui = Ui::new(Menu, backend); ui.tap_text("Lists"); assert!(ui.visible_text().iter().any(|line| line == "Rows, subtitles, values"));
ui.press(Button::Back); assert_eq!(ui.depth(), 1);}Assert on what is on the panel, not on the depth. depth() == 2 says
something opened; visible_text says the right thing did.
Starting
Section titled “Starting”testing::Ui::new
Section titled “testing::Ui::new”Installs host behind a recorder, starts screen, and paints once.
pub fn new<S: Screen + 'static>(screen: S, host: &'static H) -> Ui<H>| Parameter | Meaning |
|---|---|
screen | The root screen, taken by value. The App owns it from here. |
host | The backend. It is installed as the host, wrapped so its draw calls are recorded. |
The first paint is not optional: the runtime ignores taps until it has painted,
so without it the first tap of every test would be swallowed. The App
installs itself as the navigator.
Acting
Section titled “Acting”Each of these is one frame, and returns the Ui so actions chain:
ui.tap_text("Wi-Fi").press(Button::Back).
testing::Ui::tap_text
Section titled “testing::Ui::tap_text”Taps the middle of whatever control shows label.
pub fn tap_text(&mut self, label: &str) -> &mut SelfPanics if nothing shows that text, listing what is on the panel, and panics if
the text is in more than one place, rather than choosing by draw order. Use
tap_nth_text for a string that repeats.
testing::Ui::tap_nth_text
Section titled “testing::Ui::tap_nth_text”Taps the indexth place label appears, in draw order.
pub fn tap_nth_text(&mut self, label: &str, index: usize) -> &mut Self| Parameter | Meaning |
|---|---|
label | The text, matched whole. |
index | Zero-based, in the order rects_of_text returns. Panics when there are fewer. |
testing::Ui::tap_at
Section titled “testing::Ui::tap_at”Taps a panel coordinate.
pub fn tap_at(&mut self, point: Point) -> &mut SelfFor what has no text: a background, a scrim, a place outside a dialog.
testing::Ui::press
Section titled “testing::Ui::press”Presses and releases button in one frame.
pub fn press(&mut self, button: Button) -> &mut Selftesting::Ui::swipe
Section titled “testing::Ui::swipe”Swipes in direction.
pub fn swipe(&mut self, direction: SwipeDir) -> &mut SelfReading the frame
Section titled “Reading the frame”testing::Ui::visible_text
Section titled “testing::Ui::visible_text”Every string the last painted frame actually showed, in draw order.
pub fn visible_text(&self) -> Vec<String>Text clipped away is left out, since a ScrollView draws all its content and
lets the clip hide what does not fit. Each string appears once, however many
times it was drawn.
testing::Ui::rects_of_text
Section titled “testing::Ui::rects_of_text”Every place label is painted, as something tappable.
pub fn rects_of_text(&self, label: &str) -> Vec<Rect>A place is left out when it is clipped away, or thinner than half the theme's minimum touch size. A row and the label inside it count once, as the label. While a dialog is up only the dialog is searched, because it captures input: a row behind it is visible and cannot be tapped. Header text is visible and never tappable.
testing::Ui::rect_of_text
Section titled “testing::Ui::rect_of_text”Where label is, when it is in exactly one place.
pub fn rect_of_text(&self, label: &str) -> Option<Rect>None covers both "not on the panel" and "in more than one place".
testing::Ui::changed
Section titled “testing::Ui::changed”Whether the last action repainted, and painted something different.
pub fn changed(&self) -> boolDraw calls are compared, not pixels, so the same ink in different places counts as a change.
testing::Ui::frame
Section titled “testing::Ui::frame”The draw calls of the last painted frame, for assertions this harness has no word for.
pub fn frame(&self) -> &[DrawOp]Reading the app
Section titled “Reading the app”testing::Ui::depth
Section titled “testing::Ui::depth”How many screens are on the stack.
pub fn depth(&self) -> usizetesting::Ui::is_running
Section titled “testing::Ui::is_running”Whether any screen is left.
pub fn is_running(&self) -> boolReaching the host
Section titled “Reaching the host”testing::Ui::host
Section titled “testing::Ui::host”The host underneath, for reading pixels or anything else backend-shaped.
pub fn host(&self) -> &'static HSee also: testing::Drive, testing::Recorder, the guide's second layer
testing::Drive
Section titled “testing::Drive”A host that a test can feed input to.
pub trait DriveInputSource only reads input. A backend already has a way to write it, for its
simulator or its event loop; Drive names those methods so
Ui can reach them without knowing which backend it drives.
xpui-embedded-graphics's Backend implements it under that crate's testing
feature. TestHost does not.
Required methods
Section titled “Required methods”testing::Drive::begin
Section titled “testing::Drive::begin”Starts a frame at millis, clearing the previous frame's edges.
fn begin(&self, millis: u32)Without it a press stays "just pressed" forever, and every frame acts on it
again. Ui calls it before each action's input, with a clock that moves on
16 ms a frame.
testing::Drive::inject_press
Section titled “testing::Drive::inject_press”Reports button as pressed this frame.
fn inject_press(&self, button: Button)testing::Drive::inject_release
Section titled “testing::Drive::inject_release”Reports button as released this frame.
fn inject_release(&self, button: Button)testing::Drive::inject_tap
Section titled “testing::Drive::inject_tap”Reports a completed tap at point.
fn inject_tap(&self, point: Point)testing::Drive::inject_swipe
Section titled “testing::Drive::inject_swipe”Reports a completed swipe.
fn inject_swipe(&self, direction: SwipeDir)See also: testing::Ui
Feeding input and reading state
Section titled “Feeding input and reading state”The fake reports only the input a test put there. Each function below writes
what the fake's InputSource reads on the next frame; reset
clears all of it.
A press or a swipe lasts one frame, and reading it consumes nothing. The
runtime may ask the same question twice in a frame and gets the same answer.
The frame ends at next_frame, at reset, or at the
first press, hold, release, set_swipe or set_millis after the frame
has read input. So each write describes the frame that reads it, and two writes
with no frame between them land in the same one.
| Test | What each frame sees |
|---|---|
press(Down), tick, press(Up), tick | Down, then Up |
hold(Down), tick, set_millis(600), tick | the press, then Down still held with no new press |
press(Confirm), tick, tick | Confirm twice: nothing was written between them |
press(Confirm), tick, next_frame(), tick | Confirm, then a quiet frame |
testing::press
Section titled “testing::press”Reports one button press to the next frame the runtime reads input.
pub fn press(button: Button)It reads the same for the whole frame, however often that frame asks, and is
cleared when the frame ends. A second press before a frame reads the first
replaces it.
Example — moving the focus down a list
use xpui::{App, Button, List, ListRow, NavigationScreen, Screen, View, testing};
struct Networks;
impl Screen for Networks { type Message = usize; fn body(&self) -> impl View<usize> { NavigationScreen::new( List::new() .push(ListRow::new("Home").on_tap(0)) .push(ListRow::new("Office").on_tap(1)), ) } fn update(&mut self, _index: usize) {}}
testing::install();let mut app = App::new(Networks);app.render();
testing::press(Button::Down);app.tick();testing::reset();app.render_if_dirty();assert_eq!(testing::drawn_lists(), [(2, 1)], "the focus is on Office");See also: testing::hold, testing::set_swipe
testing::hold
Section titled “testing::hold”Reports the edge and leaves the button down, as a finger does.
pub fn hold(button: Button)press alone is a key tapped so briefly that no frame saw it
held, which is not what hardware sends. A held key is what auto-repeat reads:
hold, then move the clock with set_millis and tick.
It stays down until release or reset.
testing::release
Section titled “testing::release”Lifts whatever hold put down.
pub fn release()testing::next_frame
Section titled “testing::next_frame”Ends the frame the fake is reporting input for.
pub fn next_frame()A pending press or swipe is cleared; a button put down with
hold stays down, and nothing else changes. Needed only
between two frames with no input written between them, since any write after a
read already ends the frame.
Example — a quiet frame after a press
use xpui::{Button, Input, testing};
testing::install();testing::reset();
testing::press(Button::Confirm);assert!(Input::was_pressed(Button::Confirm));assert!(Input::was_pressed(Button::Confirm), "reading consumes nothing");
testing::next_frame();assert!(!Input::was_pressed(Button::Confirm), "an edge lasts one frame");testing::set_swipe
Section titled “testing::set_swipe”Reports one swipe to the next frame the runtime reads input, so navigation can be tested without a finger.
pub fn set_swipe(direction: SwipeDir)It reads the same for the whole frame, however often that frame asks, and is cleared when the frame ends, as a press is.
testing::set_millis
Section titled “testing::set_millis”Moves the fake clock, so repeat timing is deterministic.
pub fn set_millis(value: u32)The clock reads 0 until this is called, and never moves by itself.
reset leaves it alone.
testing::set_has_left_right_keys
Section titled “testing::set_has_left_right_keys”Says whether the device the fake stands for has a Left/Right pair, so a control that branches on it can be tested both ways.
pub fn set_has_left_right_keys(present: bool)false until set, and reset to false by reset.
testing::set_swipe_moves_selection
Section titled “testing::set_swipe_moves_selection”Chooses which way a swipe moves focus, so both readings can be tested.
pub fn set_swipe_moves_selection(enabled: bool)| Parameter | Meaning |
|---|---|
enabled | false, the default, moves the content: swiping up walks down the list. true moves the focus with the swipe. |
false until set, and reset to false by reset.