Testing with dayscript

dayscript is Day’s automation language: a YAML file of steps that drives and asserts a running app. One script taps buttons, types text, navigates, asserts what’s on screen, and captures screenshots, identically on macOS, iOS, Android, Linux, Windows, and OpenHarmony, because it addresses your UI by the stable ids you gave your Pieces, not by pixels or platform selectors.

It resembles Maestro, with one structural difference: the engine is compiled into your app and executes steps as real Day events, which makes the same script portable across all targets and makes waits deterministic instead of sleep-based.

A script

name: walkthrough
flow:
  - wait_for:      { id: home-title }
  - screenshot:    home
  - navigate:      { route: controls }
  - assert_route:  { route: controls }
  - input:         { id: name-field, text: "Ada" }
  - tap:           { id: increment-button }
  - assert_value:  { id: counter-label, value: "1 click" }
  - tap:           { id: btn-alert }
  - assert_presented:
  - respond:       { button: 0 }
  - a11y_audit:
  - screenshot:    controls

Run it against any target:

day launch -p macos-appkit --script dayscript/walkthrough.yaml
day launch -p android-mdc --script dayscript/walkthrough.yaml --locale fr

day launch builds, starts the app with the scripting engine invited, executes the steps, and exits nonzero if any assertion fails (exit code 5). Screenshots land under build/day/screenshots/<target>/<locale>/. Several --script flags run in sequence, and --locale makes the run a localization test at the same time; assertions can reference Fluent keys instead of literal strings, so the same script passes in every language.

The step vocabulary

GroupSteps
Waitingwait_for (an id appears), pause
Actingtap, input, set_value, toggle, select, focus
Navigationnavigate, nav_back, assert_route
Assertingassert_visible, assert_text, assert_value, assert_focused, assert_no_placeholders
Dialogsassert_presented, respond
Evidencescreenshot, a11y_audit

Every locating step waits (bounded, five seconds by default) rather than failing instantly, which removes the sleep-tuning that makes UI tests flaky. Acting steps synthesize Day events on the main thread between flushes, so they are deterministic and behave identically on every toolkit. Target elements by ids you know to be interactive, and scroll explicitly when a step needs an element brought into view.

How it works

The engine lives in day-script, compiled into your app. It activates only when invited: the launcher passes a localhost port and a one-time token through the environment; without them the engine never binds a socket, in debug or release. Steps arrive as JSON over that socket and execute on the main thread between reactive flushes:

day launch --script …          your app process
┌───────────────┐   localhost  ┌────────────────────────────────┐
│ script runner │ ───────────► │ day-script engine              │
│ (in the CLI)  │  step + token│  id → node (day-core index)    │
└───────────────┘ ◄─────────── │  synthesize Day event / assert │
        reply: ok / error / png└────────────────────────────────┘

A tap runs the same action path a user’s tap would; input goes through the controlled-text machinery; screenshot asks the toolkit for a native window snapshot. Because steps interleave with the reactive turn, “wait until idle” has a real definition (no pending reactive work, no dirty layout) rather than a timeout heuristic.

What it’s for beyond tests

The same scripts serve several jobs:

  • CI walkthroughs: every push builds the showcase on all targets and runs the walkthrough; the gallery is those screenshots. A content-validation step catches blank captures.
  • Iteration: with no hot reload, --script goto-settings.yaml after each relaunch puts you back on the screen you’re editing. Cheap and surprisingly effective.
  • Accessibility audits: the a11y_audit step diffs the native accessibility tree against your declarations (details).
  • Agent verification: AI coding agents use dayscript to check their own work: write a change, run a script, read the assertions (for agents).

Recording

You don’t have to write a script from scratch. day::record captures the taps, edits, selections, and navigation an app receives and turns them back into a dayscript — the reverse of playback, riding the one point every backend funnels its events through, so it needs no per-toolkit code.

Record headlessly from the CLI:

day launch -p macos-appkit --record recording.yaml

Drive the app by hand; recording.yaml is rewritten as you go and holds everything up to the last action even if the app is killed. Because it’s an ordinary dayscript, you replay it on any target:

day launch -p android-mdc --script recording.yaml

Or record and replay inside the app. day::record::start_into(buffer) streams the script into a Signal<String> you can bind a text_area to; day::play_script(&yaml) replays one in-process through the same engine --script drives. The showcase’s Scripting page is a working example — Record, move around, Stop, edit, Play. exclude_prefix keeps a UI’s own record and stop controls out of its recording.

The recorder is honest about the same blind spots the engine has. It captures actions on elements you gave ids, not positional taps, slider drags, or native OS chrome, so a recording is a starting point you edit — not a pixel-exact replay.

Limits

dayscript can only see what Day owns. It cannot type through the native IME, verify the software keyboard, drive OS permission prompts or file dialogs, or assert native animations. Those blind spots exist; the project’s practice is scripted coverage for everything Day-side plus a short manual smoke per platform for the native seams. Unit-level testing below the UI has a separate tool: the mock toolkit runs your Pieces headlessly in cargo test.