Skip to content

Recording

What the framework drew, as data. Every draw call made on the fake host, or on a real backend wrapped in a testing::Recorder, lands in one log, in the order it was made. A test reads the log whole with testing::ops_log, keeps one kind of call with a draw accessor, or renders it as text and compares it with a golden file, so call order, clipping and the state a widget was drawn in can be asserted without a pixel. Like the rest of xpui::testing, none of it is compiled unless the crate's testing feature is on.

Draw calls is the guide's layer of test built on it. Testing installs the fake host, resets the log and drives a screen. This page is what each piece of the recording does.

testing::RecorderWraps a host, recording every draw call before passing it on.
testing::DrawOpOne call the framework made on the host.
testing::RectKindHow a rectangle was painted.
testing::ops_logEverything drawn since the last reset, in order.
testing::renderEvery op as text, one per line, which is exactly a golden file's body.
testing::assert_snapshotAsserts that everything drawn since the last reset matches the golden.
Draw accessorsOne view onto the log per kind of call: text, rectangles, headers, lists, dialogs, hints, sliders, bars, indicators and clips.
testing::TextDrawOne recorded draw_text: position, text, font id and style.
testing::RectDrawOne recorded rectangle: x, y, width, height, and how it was painted.
testing::RowCellsThe title, subtitle and value of a list row, as the theme asked for them.

Wraps a host, recording every draw call before passing it on.

pub struct Recorder<H: Host + 'static>

Every call is forwarded, and nothing is answered from the recording, so a screen measures, lays out and paints exactly as it would without the wrapper. The record lands in the same log as the fake's, so ops_log, the draw accessors and snapshots all read a real backend's frame. Ui installs one for you.

A repaint request is counted by updates, as the fake counts its own, and then forwarded to the backend. A dither is recorded as the fake records it, with light in black, so a snapshot marks the same shade through either host. Wrap a real backend, never the fake itself: that records every call twice.

Example — recording a real backend

Rust
use xpui::host::Host;
use xpui::testing::Recorder;
/// `backend` is the real host, already sized for the panel under test.
fn install_recording<H: Host + 'static>(backend: &'static H) {
let recorded = Recorder::wrap(backend);
// Safety: before the first frame, and never concurrently with one.
unsafe { xpui::host::install(recorded) };
}

Wraps inner and leaks the wrapper, because a host must be 'static.

pub fn wrap(inner: &'static H) -> &'static Recorder<H>

The installed host outlives every screen that could draw through it, and a test process exits anyway.

The host underneath, for anything this wrapper does not model — reading the framebuffer, say.

pub fn inner(&self) -> &'static H

See also: testing::Ui, testing::ops_log

One call the framework made on the host.

pub enum DrawOp

The log is a Vec<DrawOp> in call order, so it answers what no per-kind list can: whether the header was drawn before the content, and whether a clip was lifted before the chrome. DrawOp is Clone, Debug and PartialEq, so a test can compare a whole frame.

VariantMeaningFields
testing::DrawOp::ClearThe whole panel cleared to background.—
testing::DrawOp::TextA line of text.origin, the top-left corner, not a baseline; text; font, the host's font id; style
testing::DrawOp::RectA rectangle, painted one of four ways.rect; kind, a RectKind; black, ink or background for a filled one, and light for a dither
testing::DrawOp::LineA one-pixel line.from, to
testing::DrawOp::ImageA 1-bit bitmap.origin; size; bytes, how many were handed over, without the pixels
testing::DrawOp::IconAn icon the host owns.origin; icon, which icon and at what size
testing::DrawOp::ClipA clip set on the host, or lifted when None.Option<Rect>
testing::DrawOp::HeaderThe header band.title, None for an empty band; subtitle
testing::DrawOp::SubHeaderA section heading.rect, the band; label; right, a right-aligned value
testing::DrawOp::HintsThe button hints in meaning order: back, confirm, previous, next.[Option<String>; 4]: None is the host's standard label, "<edit>", "<done>" or "<cancel>" a word the host owns, and "" a blank slot
testing::DrawOp::ProgressBarA determinate progress bar.rect; current, out of total; total
testing::DrawOp::SliderA themed slider.rect; value, out of max; max; state, idle, focused or editing
testing::DrawOp::ScrollIndicatorThe scroll indicator beside a scrolling region.rect; content, how tall; visible, how much shows; offset, how far down
testing::DrawOp::ListA themed list.rect; selected, -1 for none; rows, each a RowCells
testing::DrawOp::OptionPopupA themed option dialog.title; selected, the highlighted option; options, each label in order

Example — asserting on the order of a frame

Rust
use xpui::testing::{self, DrawOp};
use xpui::{App, NavigationScreen, Screen, Text, View, vstack};
struct Storage;
impl Screen for Storage {
type Message = ();
fn body(&self) -> impl View<()> {
NavigationScreen::new(vstack![0; Text::new("182 KB free")])
}
fn update(&mut self, _message: ()) {}
fn title(&self) -> Option<&'static str> {
Some("Storage")
}
}
testing::install();
let mut app = App::new(Storage);
testing::reset();
app.render();
let log = testing::ops_log();
let header = log.iter().position(|op| matches!(op, DrawOp::Header { .. }));
let content = log
.iter()
.position(|op| matches!(op, DrawOp::Text { text, .. } if text == "182 KB free"));
assert!(header.is_some() && content.is_some());

One line of a snapshot.

pub fn to_line(&self) -> String

Flat and readable rather than serialised, so a reviewer sees what changed in the diff. A kind name, padded to twelve columns, is followed by the call's arguments; a rectangle is (x,y WxH) and an absent string is -. A list and a dialog continue on indented lines, one per row or option.

Rust
use xpui::testing::{DrawOp, RectKind};
use xpui::Rect;
assert_eq!(DrawOp::Clear.to_line(), "clear");
let rule = DrawOp::Rect { rect: Rect::new(16, 60, 448, 1), kind: RectKind::Filled, black: true };
assert_eq!(rule.to_line(), "rect (16,60 448x1) filled");

See also: testing::render, testing::ops_log, testing::RectKind

How a rectangle was painted.

pub enum RectKind
VariantMeaningIn a snapshot
testing::RectKind::FilledSolid ink or background.filled, then background when it was not ink
testing::RectKind::StrokedAn outline, one pixel wide.stroked
testing::RectKind::DitherDithered, which reads as grey on a 1-bit panel.dither, then light for the light pattern
testing::RectKind::ScrimDarkened without erasing, so what was behind stays legible.scrim

See also: testing::DrawOp, testing::RectDraw

Everything drawn since the last reset, in order.

pub fn ops_log() -> Vec<DrawOp>

A copy, so a test may hold it across later frames. Every draw accessor reads this same log, so "was a list drawn" and "what did the frame look like" never disagree.

See also: testing::reset, testing::render

Every op as text, one per line, which is exactly a golden file's body.

pub fn render(ops: &[DrawOp]) -> String

Each op is its to_line, and every line ends in a newline. Useful in a failure message, or to assert on a few lines without committing a golden.

Rust
use xpui::{Point, Text, VStack, View, testing, vstack};
testing::install();
testing::reset();
let mut view: VStack<()> = vstack![8; Text::new("Wi-Fi"), Text::new("Bluetooth")];
view.measure(testing::screen());
view.render(Point::ORIGIN);
let text = testing::render(&testing::ops_log());
assert!(text.contains("\"Wi-Fi\""), "{text}");
assert!(text.contains("\"Bluetooth\""), "{text}");

See also: testing::assert_snapshot

Asserts that everything drawn since the last reset matches the golden.

pub fn assert_snapshot(name: &str)
ParameterMeaning
nameThe golden's file name without .txt, under tests/snapshots/ in the crate being tested.

The golden is render of the log, committed beside the test, so a change to layout shows up as a diff a reviewer reads. It asserts call order, the clip lifecycle and the state a widget was drawn in, none of which a picture shows.

SituationWhat happens
The golden matchesThe assertion passes.
The golden differsIt panics with a line diff.
UPDATE_SNAPSHOTS is set, and not 0It writes the golden and passes.
No golden existsIt writes one and panics, so the new file is read before a second run trusts it.

Example — a whole screen against its golden

no_run, because the first run writes a file:

Rust
use xpui::{App, NavigationScreen, Screen, Text, View, testing, vstack};
struct Settings;
impl Screen for Settings {
type Message = ();
fn body(&self) -> impl View<()> {
NavigationScreen::new(vstack![0; Text::new("Settings")])
}
fn update(&mut self, _message: ()) {}
}
testing::install();
let mut app = App::new(Settings);
testing::reset();
app.render();
testing::assert_snapshot("settings_screen");

See also: testing::render, testing::DrawOp::to_line

Each reads ops_log and keeps one kind of call, in order, so a test can assert on the kind it cares about without matching DrawOp itself.

AccessorAbstractReturns
testing::drawn_textEvery draw_text recorded since the last reset.Vec<TextDraw>
testing::drawn_rectsEvery rectangle recorded since the last reset, in draw order.Vec<RectDraw>
testing::drawn_headersThe title passed to each draw_header since the last reset.Vec<Option<String>>; None is a header with no title, which paints an empty band
testing::drawn_sub_headersEvery sub-header (a Section title) drawn since the last reset, with the rect it was given.Vec<(Rect, String)>
testing::drawn_listsEvery list drawn since the last reset, as (rows, selected).Vec<(usize, i32)>; selected is -1 for a list behind a dialog
testing::drawn_list_rowsThe full cell contents of every list drawn since the last reset.Vec<Vec<RowCells>>
testing::drawn_popupsEvery option dialog drawn since the last reset, as (title, options, highlighted).Vec<(String, usize, i32)>
testing::drawn_hintsWhich of the four button hints were asked for, per draw: true where the slot carries a label, false where it was left blank.Vec<[bool; 4]>
testing::drawn_slidersEvery slider drawn since the last reset, as (rect, value, max).Vec<(Rect, i32, i32)>
testing::drawn_progress_barsEvery progress bar drawn since the last reset, as (rect, current, total).Vec<(Rect, u32, u32)>
testing::drawn_indicatorsEvery scroll indicator asked for since the last reset, as (content, visible, offset).Vec<(i32, i32, i32)>
testing::clipsEvery clip set or lifted since the last reset, in order.Vec<Option<Rect>>; None is a lift, which a scroll view must leave behind

Example — asserting a screen's draw calls

Rust
use xpui::{App, List, ListRow, NavigationScreen, Screen, View, testing};
struct Settings;
impl Screen for Settings {
type Message = ();
fn body(&self) -> impl View<()> {
NavigationScreen::new(
List::new()
.push(ListRow::new("Wi-Fi").value("On"))
.push(ListRow::new("Sleep after").value("5 min")),
)
}
fn update(&mut self, _message: ()) {}
fn title(&self) -> Option<&'static str> {
Some("Settings")
}
}
testing::install();
let mut app = App::new(Settings);
testing::reset();
app.render();
assert_eq!(testing::drawn_headers(), [Some("Settings".to_string())]);
assert_eq!(testing::drawn_lists(), [(2, -1)], "two read-outs, nothing highlighted");
assert_eq!(
testing::drawn_list_rows()[0][1],
[Some("Sleep after".to_string()), None, Some("5 min".to_string())],
);

One recorded draw_text: position, text, font id and style.

pub type TextDraw = (i32, i32, String, i32, u8)

The position is the top-left corner of the text, not a baseline. The style is FontStyle as its u8.

One recorded rectangle: x, y, width, height, and how it was painted.

pub type RectDraw = (i32, i32, i32, i32, RectKind)

The title, subtitle and value of a list row, as the theme asked for them.

pub type RowCells = [Option<String>; 3]

None is a field the row omitted, which is how a host chooses a one- or two-line row.

See also: testing::drawn_list_rows, testing::DrawOp