Test a Day app

Use day test to run tests inside your app on a selected toolkit. Tests can exercise a page or check logic that needs the app’s environment. Run them on each target your app supports. For walkthroughs written in YAML, see DayScript.

Declare a test

Mark a function with #[day::test] and return a Case. A GUI case supplies a page and an async body that drives it. Test names replace underscores with hyphens: button_press runs as button-press. Names must be unique across linked test providers.

Use element IDs to locate controls independently of their translated labels. The example below uses generated res::str accessors; add the example messages to your app’s catalog.

use day::prelude::*;
use day::{Case, Drive};

#[day::test]
fn button_press() -> Case {
    let presses = Signal::new(0i64);
    Case::new()
        .page(move || {
            column((
                button(res::str::press())
                    .action(move || presses.update(|n| *n += 1))
                    .id(id!(btn_press)),
                label(res::str::counter_value(presses)).id(id!(btn_count)),
            ))
        })
        .shot("default")
        .drive(|d: Drive| async move {
            d.tap(id!(btn_press).as_str()).await?;
            d.assert_text(
                id!(btn_count).as_str(),
                &res::str::counter_value(1_i64).format(),
            )
            .await
        })
}

Each awaited Drive operation lets the app process events. Use tap, input, and toggle to interact with controls, and assertions such as assert_text, assert_value, and assert_focused to check the result. Pass id!(...).as_str() where a drive operation accepts a string ID.

Keep each case focused on one behavior. Declare tests in a module included by the app’s test build. A Cargo feature such as conformance can exclude them from shipping builds; enable that feature when building the test app. A disabled module contributes no tests.

Host a test page

Wrap the app’s root with day::test_host:

pub fn root() -> impl Piece {
    day::test_host(|| app_root())
}

The host shows the app until a test starts, then displays each case’s page independently. It disposes the page before the next case and restores the app when the run ends. A GUI test fails if the app has no test host.

For logic that needs the app’s environment without a page, use Case::headless and .run. The body receives a Drive with check and check_eq assertions.

Run tests

Run these commands from the app project directory:

day test -t macos-appkit
day test -t macos-appkit 'button-*'
day test -t ios-uikit --ios-simulator <udid>
day test -t android-material --shots always
day test -t web-dom --list

day test builds and launches the app, then reports each selected test’s result. --skip-build reuses the last build. --list lists registered tests without running them. The command accepts the same device-selection flags as day launch; see the CLI reference.

The default case timeout is 30 seconds. Set --case-timeout <seconds> to change it, or use 0 when debugging without a limit. A case can set its limit with .timeout. A failed test fails the command. A timed-out case fails and the run continues.

For web tests, build through Day’s CLI so the test registry is included in the app. A bare Cargo build lacks the metadata needed to run those tests.

The VS Code extension also runs tests from the Test Explorer and provides Debug Test on desktop targets.

Inspect results

Results and captures are written under:

build/day/screenshots/<target>/<variant>/conformance.json
build/day/screenshots/<target>/<variant>/tests/<test>/<shot>.png
build/day/screenshots/<target>/<variant>/tests/<test>/failed.png

With a device profile, the path includes <device> between the target and variant. The JSON report records passes, failures, skips, durations, and failure messages. --shots on-failure is the default; use always to retain every capture or never to disable captures. Declare a capture with .shot on the case, or call Drive::shot during the test.

Interpret a result

A passing test verifies the assertions it ran. Scripted input checks Day’s event handling; it does not verify that a physical tap or a native gesture recognizer produces that event. Exercise native input separately when testing gestures, text composition, or platform menus.

Use assert_native to check properties read from the native widget. Fields a toolkit cannot read appear as native_unread in the report. Treat those as unverified native properties. Visibility assertions check hidden flags and window membership; a clipped or transparent view can still report visible.

A case can declare a required capability with .requires. Unsupported cases are skipped with a reason. Review skips alongside passes; a skip does not verify the feature. Snapshot-dependent cases cannot capture on Web DOM.

On targets that unwind panics, a panic in the test body or page construction fails the case and the run continues. Targets that abort on panic, including the web, terminate the app; day test reports the interrupted run.