zgui

Testing

Testing an interface with no window — the component harness, the headless platform, driving input, flushing, goldens and the accessibility tree.

Every stage of zgui hands the next stage a plain value: a view builds a document, the document is styled and laid out into fragments, the fragments are painted into a display list, and the display list goes to a renderer. Each of those values can be read from a test, so an interface is testable with no screen, no graphics device and no font files. This page assumes the whole of Learn.

Three instruments

CrateWhat runsWhat it answers
zgui-testkit-viewone component: the view, its listeners, its timers. No document, no style engine, no layout, no window.What did the view build? What did it ask the runtime for? What did a press reach?
zgui-platform-headlessthe whole application: real document, cascade, layout, text, paint, frame loop.Where is it on the screen? What is a screen reader told? What did the display list hold?
zgui-testkit-scenea frame loop you drive yourself, plus the scene transcript, the golden files and the frame counters.What did one frame draw and how much work did it do?

None of the three is re-exported by zgui. Each is its own crate, and each goes in [dev-dependencies].

Cargo.toml
[dev-dependencies]
zgui-testkit-view = { path = "../zgui/crates/zgui-testkit-view" }
zgui-testkit-scene = { path = "../zgui/crates/zgui-testkit-scene" }
zgui-platform-headless = { path = "../zgui/crates/zgui-platform-headless" }

Take all of them from the same source and the same revision as zgui itself. A component test passes a DomHandle between crates, so two copies of zgui-view in one build are a type error, not a subtle bug.

Naming zgui-testkit-view also turns on the stub-backend feature of zgui-view for the test build. That is where the in-memory tree comes from, and it is off in an ordinary build, so an application links none of it.

Testing one component

Window is everything a component test needs, wired together: one tree, one host, one reactive scope, one root element, and one transcript that all of them write into.

use zgui::prelude::*;
use zgui_testkit_view::Window;

let window = Window::open();

Prop

Type

MethodDoes
Window::open()Opens one. Installs the reactive runtime on this thread.
Window::for_document(id)The same, for a test that needs two documents.
place(node, x, y, width, height)Declares where a node's box is. A scripted press is aimed with it.
dispatcher()A Dispatcher aimed into the root's subtree.
click(x, y)Clicks at a point. Returns Delivered.
frame()Runs the reactive flush, which is what a frame does at this layer.
advance(by)Moves the clock, fires everything due, then runs a frame.
now()The virtual clock's reading, as a Duration.
bounds_of(node)The box the test declared for a node.

Mounting a view

Building a view produces an anchor. Mounting it attaches its nodes under a parent. Hold the anchor: dropping it unmounts the view and takes its nodes, signals and timers with it.

/// Builds `view` and mounts it under the window's root.
fn mount(window: &Window, view: impl IntoView) -> (NodeId, Box<dyn Anchor>) {
    let mut built = window.scope.with(|| view.into_view().build(&mut window.cx.cx()));
    built.mount(&window.dom_handle, window.root, None);
    let node = built.first_node().expect("the view produced a node");
    (node, Box::new(built))
}

A whole test

The component under test, built from the element vocabulary:

#[component]
fn Counter() -> impl IntoView {
    let (count, set_count) = signal(0);
    view! {
        row(class = "counter") {
            label(class = "counter__value") {{move || count.get().to_string()}}
            control(
                class = "counter__step",
                a11y:label = "Increment",
                on:click = move |_| set_count.update(|n| *n += 1)
            ) {
                "+"
            }
        }
    }
}

The test mounts it, reads the document, sends a press, flushes, and reads the document again:

#[test]
fn pressing_the_step_control_raises_the_count() {
    let window = Window::open();
    let (root, _held) = mount(&window, view! { Counter() });
    window.frame();

    // The control is the second child of the row. Give it a box, or nothing can be aimed at it.
    let step = window.dom.tree().children(root)[1];
    window.place(step, 0.0, 0.0, 24.0, 24.0);
    assert_eq!(window.dom.tree().text_content(root), "0+");

    // Forget the mount, so what follows is about the interaction.
    window.transcript.clear();

    let delivered = window.click(10.0, 10.0);
    assert_eq!(delivered.target, Some(step));
    assert_eq!(window.dom.tree().text_content(root), "0+", "not flushed yet");

    window.frame();
    assert_eq!(window.dom.tree().text_content(root), "1+");
}

Reading the tree

window.dom.tree() is the tree itself, for the questions a transcript cannot answer.

QuestionRead
What is under this node?children(node)
What does this subtree say?text_content(node)
What classes does it carry?classes(node)
What is one attribute?attribute(node, zgui::view::AttrName::new("variant"))
What inline style does it carry?style_property(node, "color")
Which interaction states does it assert?ui_state(node)
What does it mean to a screen reader?semantics(node)
What is its value?property(node, key)
How large is the tree?node_count(), listener_count()

ui_state is what a sheet selects on with :hover, :disabled and the rest. semantics is what the accessibility tree is built from.

Flushing

A signal write never runs anything at the moment of the write. It marks its subscribers, and the frame runs them. window.frame() is that frame.

window.click(10.0, 10.0);   // the handler ran and wrote a signal
                            // the tree still shows the old value here
window.frame();             // now the effects have run and the tree has changed

frame() is a bounded loop of at most eight rounds: flush, take the values the host wrote back, load them into the tree, repeat until nothing more was written. That is what makes a field echoing a keystroke settle inside one call.

Call frame()Because
after mounting, before the first assertionthe mount's own effects have not run
after every interactionthe handler wrote a signal and nothing has read it
after writing a signal from the test itselfthe same
after advance()already included — advance moves the clock and then runs a frame

The rule for the headless platform is the same idea with a different name: settle there, frame here.

The transcript

The transcript is one ordered record of everything the view asked its backend to do. The tree, the host and the dispatcher all append to it, which is what makes a question about order answerable.

window.transcript.clear();
window.click(10.0, 10.0);
window.frame();

// One handler run, one text write. A binding that rewrote its text on every frame shows two.
assert_eq!(window.transcript.len(), 2);
assert!(window.transcript.to_string().contains("click target"));

Let a golden hold the node numbers rather than writing them by hand.

MethodGives
to_string()one line per operation, the golden format
ops()the operations as Op values, for a structural assertion
len(), is_empty()how much was recorded
clear()forget everything so far
assert_matches(path)compare against a golden file

Every clone of a Transcript appends to the same list, so passing one around costs nothing.

The line vocabulary

create #1 control
insert #2 into #1                    insert #2 into #1 before #3
detach #1
text #2 "hello"
attr #2 name="value"                 attr #2 name removed
classes #2 [a b]
class #2 danger on                   class #2 danger off
style #2 color="red"                 style #2 color removed
state #2 hover on
prop #2 value="x"
custom-state #2 name on
semantics #2 button                  semantics #2 cleared
listen #2 click                      listen #2 click capture
unlisten #2
observe #2 scroll-position
focus #2       scroll #2      trap #2      untrap
selection #2 0..3     select-all #2     value #2 "x"
handler #2 click target              handler #2 click capture
sheet name     unsheet name
command request-focus #2

A node is written as # followed by the number the backend minted. Show, For and dynamic children mint #marker nodes, and those appear too.

Two things are deliberately not recorded: reads, and writes that changed nothing. A controlled field echoing the user's own keystroke produces no value line, and that is the point — a component that writes the same class twice per change is visible in a transcript and invisible in the finished tree.

Driving input

A Dispatcher aims a real event at a place or at a node, resolves the path down and back up, runs the handlers, and honours what each one says.

MethodSends
click_at(point)a click at a point, after a hit test
pointer_at(point, kind)one pointer event of any EventKind at a point
key(node, key)one key at a node, plus what a window does about it
type_text(node, text)text as the keyboard produces it
send_to(node, kind, payload)one event straight at a node, with no hit test
with_modifiers(modifiers)the same dispatcher with modifiers held down
listener_at(node, kind, position)the identity of one registered listener
use zgui::vocab::{EventKind, Key, Modifiers, NamedKey};

// A pointer, at a place.
window.dispatcher().pointer_at(point, EventKind::PointerDown);

// A key, at a node.
window.dispatcher().key(field, Key::Named(NamedKey::Enter));

// A shortcut.
window.dispatcher().with_modifiers(Modifiers::SHIFT).key(grid, Key::Named(NamedKey::PageUp));

// An accessibility action, or anything else aimed at an element rather than a place.
window.dispatcher().send_to(button, EventKind::Click, payload);

key does the two things a window does. It delivers the key event; then, if every handler let the framework's own behaviour stand, it offers the key to the editing model, and failing that synthesises a click for Enter and Space. So a button that handles no keys at all is activated by the space bar in a test, exactly as it is on screen.

A wheel is an EventKind::Wheel with a wheel payload:

use zgui::geom::{CssPx, Point, Size};
use zgui::vocab::{
    Payload, PointerId, PointerKind, ScrollDelta, ScrollPhase, WheelEvent,
};

window.dispatcher().send_to(
    list,
    EventKind::Wheel,
    Payload::Wheel(WheelEvent {
        delta: ScrollDelta::Lines { x: 0.0, y: -3.0 },
        phase: ScrollPhase::Discrete,
        position: Point::new(CssPx(10.0), CssPx(10.0)),
        id: PointerId::MOUSE,
        kind: PointerKind::Mouse,
    }),
);

What a dispatch reports

pub struct Delivered {
    pub target: Option<NodeId>,
    pub path: Vec<NodeId>,          // root first
    pub ran: Vec<(NodeId, Phase)>,  // which handlers ran, in order
    pub default: DefaultAction,     // whether the framework's own behaviour survived
    pub commands: Vec<Command>,     // what the handlers asked for
}

reached_anything() is true when any handler ran. commands holds CapturePointer, ReleasePointer, RequestFocus and Synthesize. The first three are collected, not carried out — the runtime does the same, and asserting on them is how a test says "the handler asked for focus" without a focus system existing. Synthesize is the exception: the dispatcher carries it out, exactly as a window does, and the handlers those runs reach are appended to ran and their own commands to commands.

Hit testing in the harness

The harness answers "what is under this point" from the boxes the test declared with place, under three rules:

  • a node with no declared box is not hit; its descendants still are;
  • a descendant wins over its ancestor;
  • at equal depth, the node later in document order wins.

place and bounds_of are the test's own numbers. Asserting that bounds_of returns what you placed asserts your own arithmetic. Real geometry comes from the headless platform, below.

The clock

The host owns a virtual clock. Nothing waits.

let window = Window::open();
let shown = window.scope.with(|| RwSignal::new(false));

let _pending = window.scope.with(|| {
    set_timeout(Duration::from_millis(700), move || shown.set(true))
});

window.advance(Duration::from_millis(699));
assert!(!shown.get_untracked());

window.advance(Duration::from_millis(1));
assert!(shown.get_untracked());
assert_eq!(window.now(), Duration::from_millis(700));

A seven-hundred-millisecond tooltip delay costs the test a microsecond. Dropping the handle set_timeout returned cancels the callback, so bind it.

What the host was asked for

ScriptedHost answers the questions a view asks the runtime, from what the test declared, and records the commands it was given.

DeclareMethod
where a node's box isset_border_box(node, rect) — or Window::place
the scale factorset_scale(scale)
what a scroll container's offset isset_scroll_position(node, position)
what is focusable under a rootset_focusables(root, nodes)
that one node contains anotherset_contains(ancestor, descendant)
the document orderset_tree_order(order)
how many animations are running on a nodeset_running_animations(node, count)
AskMethod
what scrolls were requestedscroll_commands()
how many focus traps are livelive_focus_traps(), topmost_focus_trap()
which sheets are installedstylesheet(name), stylesheet_names(), stylesheet_installs()
how many timers are pendinglive_timers()
whether scrolling is frozenscrolling_frozen()

Snapshot testing

The repository does not use insta. Snapshots are golden files, and the mechanism is zgui_testkit_scene::dump::golden, which a transcript reaches through assert_matches.

use std::path::{Path, PathBuf};

fn golden(name: &str) -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/goldens").join(name)
}

#[test]
fn counter_steps() {
    let window = Window::open();
    let (root, _held) = mount(&window, view! { Counter() });
    window.frame();
    window.place(window.dom.tree().children(root)[1], 0.0, 0.0, 24.0, 24.0);

    // Two presses, so the golden shows the text going 0, 1, 2. A binding that rewrote its text on
    // every frame rather than on every change looks identical after one.
    window.click(10.0, 10.0);
    window.frame();
    window.click(10.0, 10.0);
    window.frame();

    window.transcript.assert_matches(golden("counter_steps.txt"));
}

Three rules, all enforced by a panic:

SituationWhat happens
The golden is missing.The test fails and tells you to re-run with ZGUI_BLESS=1. A file is never created quietly.
ZGUI_BLESS=1 and the golden differs or is missing.The file is written and the test still fails, naming what changed.
No ZGUI_BLESS, and the rendering matches.The test passes.

Blessing that also reported success would check nothing, and an author who blessed by reflex would have no moment at which to read the diff. The run after the blessing is the one that passes.

ZGUI_BLESS counts as set for any non-empty value other than "0".

The whole application, with no window

zgui-platform-headless is a platform backend with no windowing system behind it: a clock a test moves, scripted input, and a surface that is a buffer. Every stage of the real loop runs — events dispatched, reactive work flushed, document restyled, laid out, painted into a display list and handed to a renderer. Only the renderer differs.

Build the application through zgui::runtime::App rather than through app(), because that is the route that hands back a Runtime you can keep and ask questions of.

use std::sync::Arc;

use zgui::platform::{Surface, SurfaceEvent};
use zgui::prelude::*;
use zgui::geom::{DevicePx, Size};
use zgui::render::{RenderTarget, Renderer};
use zgui::runtime::{App, AppError, Runtime};
use zgui_platform_headless::Harness;
use zgui_testkit_scene::CaptureRenderer;

/// A renderer that records the display list instead of drawing it.
fn capture(_surface: &Arc<dyn Surface>, target: RenderTarget) -> Result<Box<dyn Renderer>, AppError> {
    let mut renderer = CaptureRenderer::new();
    renderer.configure(target);
    Ok(Box::new(renderer))
}

/// The application, open and driveable.
fn open() -> Harness<Runtime> {
    let runtime = App::new()
        .with_title("test")
        .with_size(400.0, 300.0)
        .with_stylesheet(SHEET)
        .with_renderer(Box::new(capture))
        .into_handler(|cx: &mut BuildCx<'_>| -> Box<dyn Anchor> {
            Box::new(view! { Counter() }.into_view().build(cx))
        })
        .expect("the reactive runtime installs");

    let mut harness = Harness::new(runtime);
    harness.deliver_to_first(SurfaceEvent::Resized(Size::new(DevicePx(400.0), DevicePx(300.0))));
    harness.settle(64);
    harness
}

The argument order differs between the two builders. zgui::runtime::App::run(view, driver) takes the view first; the umbrella App::run_on(driver, view) takes the driver first.

App::new() installs no text engine and no glyph rasteriser, so text is not shaped. A test that asserts on boxes, classes or the accessibility tree needs neither. A test that asserts on text geometry or on glyphs supplies both, and the deterministic pair is the right choice — it needs no font file on disk, so the test gives the same answer on a machine with a thousand faces installed and on one with none.

.with_text_engine(Box::new(|| {
    Box::new(zgui_layout::Paragraphs::new(zgui_testkit_scene::MonoShaper::new()))
}))
.with_glyph_raster(Box::new(|| Arc::new(zgui_testkit_scene::MonoRaster::new())))

MonoShaper is a fixed 8 by 16 face, and the default metrics source the cascade resolves ex and ch against agrees with it by construction. zgui-layout is a separate crate; add it to [dev-dependencies] for this.

Driving the loop

MethodDoes
deliver(surface, event)one platform event to one surface
deliver_all(surface, events)a burst: several events, one park, one chance to draw
deliver_to_first(event)the same, to the first surface the application created
pump()one turn of the loop. Returns how many frames ran
settle(turns)pump until nothing is pending. Panics if work remains after turns
advance(by)move the clock, taking the deadline edge if it is crossed
run_for(total, step)advance in steps and pump each time
suspend()remove every surface without closing the requested windows
resume()recreate surfaces and rebuild their window views
shut_down()deliver application shutdown
app(), app_mut()the Runtime, and through it windows()
platform()the Headless platform, and through it the offscreen surfaces
assert_park_invariant()resumes are at most frames plus one

A window system delivers bursts, not a stream. deliver_all is what models that; delivering the same events one at a time tests a sequence nothing produces.

For a multi-window test, take the surface IDs from app().windows() or the offscreen surfaces from platform().offscreens(), then pass the intended ID to deliver. OffscreenSurface retains the SurfaceAttributes that created it, so a test can assert the requested title, size, application identifier, decorations, and other initial window state. Use suspend followed by resume to prove that application-scope state survives and each window view is rebuilt.

Input at the surface

use zgui::geom::{Css, CssPx, Point};
use zgui::vocab::{
    KeyEvent, KeyState, Modifiers, NamedKey, PhysicalKey, PointerAction, PointerEvent,
    PointerId, PointerKind, ScrollDelta, ScrollPhase, Timestamp, WheelEvent,
};

/// One pointer event at a place.
fn pointer(action: PointerAction, at: Point<CssPx, Css>) -> SurfaceEvent {
    SurfaceEvent::Pointer {
        action,
        event: PointerEvent::mouse(at),
        modifiers: Modifiers::NONE,
        timestamp: Timestamp::ORIGIN,
    }
}

/// One wheel notch at a place.
fn wheel(at: Point<CssPx, Css>, lines: f32) -> SurfaceEvent {
    SurfaceEvent::Wheel {
        event: WheelEvent {
            delta: ScrollDelta::Lines { x: 0.0, y: lines },
            phase: ScrollPhase::Discrete,
            position: at,
            id: PointerId::MOUSE,
            kind: PointerKind::Mouse,
        },
        modifiers: Modifiers::NONE,
        timestamp: Timestamp::ORIGIN,
    }
}

/// One key going down.
fn key(event: KeyEvent) -> SurfaceEvent {
    SurfaceEvent::Key {
        state: KeyState::Pressed,
        event,
        modifiers: Modifiers::NONE,
        timestamp: Timestamp::ORIGIN,
    }
}

A click is three events in order: PointerAction::Moved, Pressed, Released, each settled. KeyEvent::named(NamedKey::Enter, PhysicalKey::Unidentified(0)) and KeyEvent::character("q") build the two kinds of key.

let mut harness = open();
harness.deliver_to_first(pointer(PointerAction::Moved, at));
harness.deliver_to_first(pointer(PointerAction::Pressed, at));
harness.deliver_to_first(pointer(PointerAction::Released, at));
harness.settle(32);

Headless is fast enough to be a unit test: a 1 851-box document reaches its first painted frame in 103.31 ms headless, against a 250 ms budget (cold.first_frame, docs/performance.md).

Asserting on layout geometry

Geometry comes out of the fragment tree, which is where layout wrote it. Read it; never compute it from the sheet, because a constant asserts the test's own arithmetic rather than the layout.

/// The centre of every 34 by 34 box in the window, left to right.
fn swatch_centres(window: &zgui::runtime::Window) -> Vec<Point<CssPx, Css>> {
    let layout = window.layout().borrow();
    let mut found = Vec::new();
    for key in layout.keys() {
        for fragment in layout.fragments_of_box(key) {
            let Some(fragment) = layout.fragment(*fragment) else { continue };
            let border = fragment.border_box;
            if (border.size.width.0 - 34.0).abs() < 0.5 && (border.size.height.0 - 34.0).abs() < 0.5 {
                found.push(Point::new(
                    CssPx(border.origin.x.0 + border.size.width.0 / 2.0),
                    CssPx(border.origin.y.0 + border.size.height.0 / 2.0),
                ));
            }
        }
    }
    found.sort_by(|a, b| a.x.0.total_cmp(&b.x.0));
    found
}

window.chain_at(point) answers the other direction: the path of elements under a point, the document's root first, in absolute device pixels. It is the path an event would travel, so a difference three levels above the target is visible in it.

Asserting on the accessibility tree

Two levels, and they answer different questions.

In a component test, semantics(node) is what the element says it is. That is the input to an accessibility tree, and it is where a missing role or a missing label shows up.

let step = window.dom.tree().children(root)[1];
let semantics = window.dom.tree().semantics(step).expect("the element says what it is");
assert_eq!(semantics.role, Role::Button);

In a headless application test, the update the surface was handed is the real thing: assembled by the frame, after paint, out of the whole document.

fn published(harness: &Harness<Runtime>) -> zgui_a11y::TreeUpdate {
    harness
        .platform()
        .offscreens()
        .first()
        .expect("a surface was created")
        .last_a11y_update()
        .expect("the frame published an accessibility update")
}

#[test]
fn the_step_control_is_announced() {
    let harness = open();
    let update = published(&harness);
    assert!(zgui_a11y::dump(&update).contains("Increment"));
}

zgui_a11y::dump renders an update as stable text, which makes it a golden. a11y_log() gives every update the surface has ever been handed, in order; an update is a difference, so a claim about the whole tree has to apply the sequence. zgui-a11y is a separate crate, added the same way.

An update carries only what changed. Asserting that a number ticking over produced one node is a real assertion: it fails when a parent is re-published for no reason.

Asserting on the display list

The display list is a value, and zgui-testkit-scene renders it as stable, diffable text. Nothing about the text depends on a driver version or on which fonts are installed.

use zgui::bits::DamageSet;

#[test]
fn the_counter_draws_what_it_should() {
    let harness = open();
    let window = &harness.app().windows()[0];
    let text = zgui_testkit_scene::transcript::of(window.scene(), &DamageSet::default());

    zgui_testkit_scene::dump::golden::assert_matches(&golden("counter_scene.txt"), text.as_str());
}

The shape:

scene viewport=256x256
damage full
passes 1 clip_layers=0 culled=0
  pass 0 region=rect(32, 32, 64, 48) clip=[…] instanced=true composite_order=5 items=1
primitives 12 batches=12
  shadow order=1 bounds=rect(-5, -3, 266, 122) blur=4 color=premul_srgb[0, 0, 0, 0.25]
  quad order=2 bounds=rect(8, 8, 240, 96) fill=linear from=(0, 0) to=(256, 0) in oklab
  mono_sprite order=3 bounds=rect(24, 24, 8, 16) tile=mono:0#0 texels=rect(0, 0, 8, 16)

Two properties hold, and neither is optional: the same scene writes the same bytes, and every field that can regress is written. A field at its default value is omitted, so a diff shows what moved rather than a wall of zeroes.

transcript::of panics unless the scene is finished. A window's scene is finished after a frame.

ItemIs
transcript::of(scene, damage)the display list as text
Transcript::as_str(), lines(), line_count()reading it
CaptureRenderera renderer that records the scene and draws nothing
MonoShaper, MonoRaster, FixedMetricstext with no font file on disk
TreeDump, to_text(tree)the seam any tree dumper is written against
golden::assert_matches(path, text)compare against a file, and the blessing rules

A frame loop of your own

Harness and Fixture drive a frame loop directly, for a test about what one frame drew and what it cost. The frame body is a Pipeline, and a closure is one.

use zgui_testkit_scene::{Fixture, FrameCx, Harness};

let mut harness = Harness::new(Fixture::new(|cx: &mut FrameCx<'_>| {
    // build the display list here
    cx.damage_rect(ink);
    cx.record_subject("#card", ink);
}));

harness.frame();
assert_eq!(harness.ink_of("#card").size.width, 64);
assert!(harness.damage_rects().iter().any(|rect| rect.contains_rect(harness.ink_of("#card"))));

ink_of panics for a name that was never recorded, deliberately: an empty rectangle would make every containment assertion hold. transcript() panics before the first frame.

The harness also holds the frame counters, exclusively, for its whole life. Two harnesses on one thread panic rather than deadlock. A counter assertion needs a Control — a second run in which the same counter did move — so an assertion cannot pass because the mechanism it names never ran. This is framework machinery; an application rarely needs it.

What the conformance suite proves

zgui-conformance is the instrument the framework's CSS support is measured with. It is not published, so an application cannot depend on it. What matters to an application author is what it publishes.

  • Support is a fraction, not a boolean. docs/parity.md is generated by the suite. It reports 250 distinct longhands behind the property names the engine generates, of which 128 are implemented and 122 are parsed and cascaded but not yet implemented.
  • A property counts as implemented only when a probe shows it changes something. A module has to declare that it reads the value, and setting it on a fixture has to visibly change the fragment tree or the hit-test answer. A declaration with no observable consequence fails the build unless it is listed with a reason.
  • Reference tests are compared as fragment trees, not pixels. No graphics device, no anti-aliasing tolerance, and a failure that names the box and the edge that moved.
  • The pass rate may never fall, and it is two numbers. A run with fewer tests fails however well the survivors did, because deleting or refusing a failing test is the easiest way to raise a pass rate. Nine converted reference tests are committed today, across block flow, sizing, flexbox, float and grid.

Read docs/parity.md before you rely on a property. If it is listed as parsed and not implemented, your sheet will parse, the declaration will cascade, and nothing will act on it.

What is worth testing

Worth a testNot worth a test
What the view builds for a given state: classes, attributes, text, semantics.That a view compiles. A view that compiles and does nothing compiles equally well.
What a press, a key or a wheel reaches, and what changed after the flush.Exact pixels. The display list is the artifact; a screenshot is not.
The order of operations, through the transcript.The cascade, layout and text shaping themselves. The framework's own suites and the parity suite own those.
What a listener asked the runtime for: focus, scroll, a focus trap.Geometry the test itself declared with place.
Timing, on the virtual clock: a delay, a debounce, an interval.Real elapsed time. Nothing in a test should sleep.
Geometry the interface computed, read out of the fragment tree.Node numbers written by hand. Let a golden hold them.
What a screen reader is told, and that every relation resolves.Frame counts in a component test. There is no frame loop at that layer.
That unmounting removes what mounting added.Framework behaviour a component does not implement, such as Enter activating a button.

Two habits are worth adopting early. Call transcript.clear() after mounting, so an assertion is about the interaction and not about the mount. Hold the anchor a mount returns, because dropping it unmounts the view and every later assertion then passes against nothing.

Next

On this page