makepad/libs/makepad_test/GUIDE.md
Admin 12d223737a tools, arch, docs, workspace, examples, sqlite, wasm_bridge
Squash of 55 work commits (Sep 1–12):
  0fd356d  windows: the vendored bindings are generated from a checked-in filter
  79882b5  fabric: a photo or a live camera to a fitted sewing pattern
  1e93309  AGENTS.md: designs stay local; no OS screenshots; focus and hidden-window laws
  aa96dbd  cargo-makepad wasm: package the bin target's wasm and create dirs before minifying
  14d0723  cargo-makepad wasm: production packaging — strip, small profile LTO, optional binaryen, size report
  79e7526  sqlite: a page-store seam — the file backend as before, an in-memory backend, and open_memory / open_with
  60ee978  cargo-makepad: package artifacts carry a content hash so a re-upload is a new URL
  611c9eb  cargo-makepad: production packaging stays off fat LTO; script VM under LTO investigated
  ab8febc  cargo-makepad: fonts packaged from the app's font manifest
  f6bbee9  wasm bridge: shared memory asks for the 4 GiB wasm32 ceiling and steps down where the engine refuses
  fb1416d  cargo-makepad: the threaded wasm module is linked with the 4 GiB wasm32 memory ceiling
  fd5d70c  cargo-makepad: the app's own resources are packaged under its bin name, which is how self:// resolves
  f51ca07  workspace: the wasm interpreter's tests build at opt-level 1 — its own profile setting is ignored inside a workspace, and opt-level 0 overflowed the script eval stack
  942ff86  sqlite: the browser store has one owner — its locks never wait on a clock
  82d0cfa  web path: the trace helper keeps its doc, the journal nonce steps a counter where there is no clock or pid
  6f8c08f  web-server: POST /api/crash stores crash reports in a rotating log on both servers
  106b38c  wasm bridge: the imported memory honours the module's declared limits
  94b8726  dj-pack: tracks in, stems through the hub, a site store snapshot out
  d9f04fc  dj-pack: pack reads caches, never creates them; dry-run writes nothing
  e6a305c  ai-hub + dj-pack: a whole track fits a stems job; long tracks split into spans
  d1910b8  web-server: a store snapshot's extensionless routes are served with the types the exporter recorded
  c1f7b53  asset-client + dj-pack: a long description never rejects a snapshot; the packer writes one bounded line
  453197d  dj-pack: analyse produces the beat grid, overview and loop-splat caches the demo cache ships
  9b4a3ca  network: every completion raises the UI signal
  fad1c49  web server: audio and text files are served, and models/ is immutable like maps/
  569b4a7  dj-pack: every CC BY and CC BY-SA version and the public domain mark are redistributable licences
  dc9cce6  workspace: no std clock on the web in any crate the web apps link — the last start-up worker death is gone
  c7639f0  clippy: timed std waits (sleep, recv_timeout, wait_timeout, park_timeout) are disallowed — they read the std clock and panic on wasm workers
  d748753  wasm bridge: the page environment carries js_worker_wait so the module links — the pool landing added the import for workers only
  ec40fdb  vj + widgets: double-click a knob or fader to reset it to its default — the Slider handles tap_count 2 and emits its normal Slide action; the deck controls carry their neutral defaults (pitch 0, gain/EQ/stems 1, filter centre, crossfader centre)
  fe23e06  AGENTS.md: the execution policy — zero locking on the UI thread as one mechanism for native and wasm, no temporary threads (the pool), the standard operating flow (Codex codes, Fable designs and reviews, Grok tests), and the tweaker on Shift+F10
  e689aea  web_server + geodata + route: live radar, wind and weather on makepad.nl/api — one bounded poller per feed, hourly, disk-backed cache served from an Arc snapshot (a restart never re-polls early), 503 warming until the first result, health reports ok/warming/unavailable with timestamps; KNMI key from --knmi-key-file, the documented anonymous open-data key otherwise; libs/geodata fetches through the platform HTTP client instead of shelling out to curl; the client retries 503 after 30 s and disables a layer only on 404
  cd33943  web_server: radar and weather run on KNMI's documented anonymous key when no --knmi-key-file is given; without --live-cache the pollers keep an in-memory cache and say so once
  2f3e393  web_server: a directory path without its trailing slash (/score) redirects to /score/ instead of 404, query preserved
  9a31d21  flow-ui + widgets: a chosen model shows no node list, and a closed ComboBox shows the start of a long label
  22b2c78  docs: streamline agent runbook and extract reference guides
  6058284  terminal: add hostable session multiplexing via tools/screen
  8a9345b  tools: migrate Cargo.toml lookups to segment-path keys
  8749b91  counter: keep app state across Splash reloads
  3ffd485  tools: add agent launcher and AIHub node update and smoke scripts
  c33a9d3  screen: add bypass and resume menu options
  c40d234  AGENTS.md: current delegation hierarchy (Grok mechanical, Codex hard, Fable manages)
  4184455  Workspace: scope lives at apps/scope (clone of makepad/scope)
  27844c9  makepad arch usb builder
  dd967eb  Workspace: drop nine members that are not in the repository
  1961768  AGENTS.md: hierarchy 2026-09-11 — Fable builds, Codex reviews, Grok proves
  9cd9f94  AGENTS.md: rendering is verified on the real GPU backend, headless is for logic tests only
  5f53254  AGENTS.md: GPU proofs may run hidden; only the headless CPU backend is out
  e950b08  docs: app-remote — hidden GPU runs vs the simulated-GPU backend, measured grab cadence, remote hazards
  9c94a77  arch: the platform plan names the simulated-GPU backend gpusim
  3009257  micro_serde: serde_json-style JsonValue accessors, pretty printer, depth limit and strict parse
  134cd76  gitignore: alternate target directories, root scratch dirs and stray logs are never source
  60ba86e  arch: the render node refs name platform/src/os/gpusim/mod.rs
  1dc571a  tools/arch_usb: Wi-Fi, Intel GPU firmware, the AI hub service and game-hardware udev rules on the Arch image; the WM session script picks the saved compositor GPU
  cc3b05a  tools/arch_usb: a polkit rule lets the arch account start, stop and restart the WM service

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 13:40:33 +02:00

8.6 KiB

makepad_test Guide

This guide covers how to write, run, and debug UI tests with makepad_test.

Authoring Model

Tests live beside the package they exercise, usually under tests/.

examples/text_input/
├── Cargo.toml
├── src/main.rs
└── tests/ui.rs

#[makepad_test] is current-package oriented by default:

  • env!("CARGO_MANIFEST_DIR") provides the package directory
  • env!("CARGO_PKG_NAME") provides the package to run

That keeps the normal Rust workflow intact: add a dev-dependency, write tests/*.rs, and run cargo test --release -p <package>.

Macro Behavior

#[makepad_test] expands to a normal #[test] wrapper that:

  1. runs cargo build --release -p <package> from the package directory
  2. selects the standalone executable from Cargo's compiler-artifact messages
  3. launches that executable with --remote, hidden unless visible mode is requested
  4. reads the owned PID and endpoint and waits for the app's first window
  5. passes a TestApp into your test body
  6. captures failure artifacts on returned errors or panics
  7. requests /gq, falls back to /quit if needed, and confirms the owned child exits

Cargo's owning-workspace target is reused, including explicit CARGO_TARGET_DIR overrides. No build target is forced beneath each package. Tests keep their small artifact directories beneath the package directory.

Supported signatures:

#[makepad_test]
fn smoke(app: TestApp) {
    // ...
}

#[makepad_test]
fn smoke(app: TestApp) -> Result<(), TestError> {
    // ...
    Ok(())
}

Unsupported:

  • async tests
  • methods with self
  • generic test functions
  • macro arguments

Runtime Defaults

The runtime is synchronous and serial-first:

  • action timeout: 10s
  • poll interval: 50ms
  • artifacts: <manifest_dir>/target/makepad_test/<package>/<test>/

The runner serializes app sessions within each test executable. Use --test-threads=1 for predictable suite order. MAKEPAD_TEST_PARALLEL=1 opts out of the runner lock when a suite is designed for concurrent owned instances.

Visible Mode and Configuration

The default is a hidden native window (MAKEPAD_HIDE_WINDOWS=1). To watch a standalone test window without focusing it:

MAKEPAD_TEST_VISIBLE=1 cargo test --release -p makepad-example-text-input --test ui -- --test-threads=1

Visible mode uses the same owned-process transport and never connects to an existing app or Studio session. The harness removes MAKEPAD_FOCUS from the child's environment.

Pacing variables:

  • MAKEPAD_TEST_STARTUP_DELAY_MS=1000 waits after startup before the test starts
  • MAKEPAD_TEST_ACTION_DELAY_MS=750 waits after each interaction
  • MAKEPAD_TEST_KEEP_OPEN_MS=3000 pauses briefly before shutdown

For explicit configuration, construct TestConfig::new or TestConfig::current_package, adjust it, and pass it to run_with_config. bin_name selects a target in a package with several binaries; app_args adds arguments after --remote; visible controls visibility. env supplies app environment variables. Its CARGO_TARGET_DIR, when present, also applies to the build. Otherwise Cargo's inherited environment and configuration apply.

The former mount_name and listen_address fields and MAKEPAD_TEST_STUDIO/MAKEPAD_TEST_STUDIO_MOUNT settings no longer apply. There is no hub or mount to configure.

Selectors

Selectors are snapshot-based. They match structured widget state instead of only relying on geometry query strings.

Constructors:

  • Selector::all()
  • Selector::id("widget_id")
  • Selector::widget_type("TextInput")
  • Selector::raw("text:hello")

Builder filters:

  • .text_exact("...")
  • .text_contains("...")
  • .nth(index)
  • .window("panel_window")
  • .window_index(1)
  • .any_window()

Selectors default to the primary window. That keeps single-window tests terse while still allowing explicit multi-window targeting.

Locators

Locator methods require exactly one visible match for interaction. That strictness is intentional: it keeps tests from silently clicking the wrong widget.

Common actions:

app.locator(Selector::id("panel_input"))
    .wait_visible()
    .fill("hello")
    .wait_value("hello")
    .press_key(KeyCode::ReturnKey);

Available interaction helpers:

  • click
  • type_text
  • fill
  • clear
  • press_key
  • press_key_with_modifiers
  • scroll
  • drag_by

Available waits and assertions:

  • wait_visible
  • wait_hidden
  • wait_count
  • wait_text / assert_text
  • wait_value / assert_value
  • wait_checked / assert_checked
  • wait_enabled / assert_enabled

Inspection helpers:

  • snapshot()
  • count()
  • widget_snapshot()
  • widget_dump()
  • screenshot()
  • wait_for_log_contains(...)

Lower-level escape hatch:

app.forward(vec![/* pointer, scroll, key, or text StudioToApp events */]);

forward translates supported input events to HTTP input routes. Other legacy protocol variants return an explicit error. Native timestamps are assigned at injection; key repeat and IME metadata are not forwarded. It does not provide live reload, window resizing, swapchains, clipboard forwarding, or hub control. Prefer the regular TestApp methods for snapshots, grabs, and logs.

Structured Widget State

Each snapshot record exposes:

  • widget id
  • widget type
  • bounds
  • window id and window index
  • visible/enabled state
  • widget-specific state when available:
    • text
    • value
    • checked
    • selected

Window names, enabled flags, and selections come from the actual widget snapshot. Missing required fields fail decoding instead of fabricating state. Optional state is absent when the widget does not expose it; empty labels may omit text, while an empty input value remains an empty string.

Interactions require visible geometry. State reads prefer visible matches and can fall back to a uniquely matched clipped widget, such as a label below a scrolling page.

Failure Artifacts

Failed tests write to:

<manifest_dir>/target/makepad_test/<package>/<test>/

Builds retain stderr; launched sessions also retain app stdout/stderr and a shutdown.txt record with the owned PID and shutdown result. Test-body failures additionally capture:

  • failure.txt
  • logs.txt
  • widget-snapshot.json
  • widget-tree.txt or widget-tree-error.txt
  • failure-screenshot.png or failure-screenshot-error.txt

If a capture step fails, the runtime writes a *-error.txt file instead of silently dropping the artifact.

Running Tests

cargo test --release -p makepad-test
cargo test --release -p makepad-example-text-input --test ui -- --test-threads=1

Standalone Transport and Ownership

The runtime uses the app's documented HTTP remote surface: /s, /snap, /d, /g, /log, and input routes. Input requests wait for a resulting frame. Rectangles are window-local layout points; do not apply DPI conversion to clicks.

Screenshots are captured from the app's own drawable. Setting TestConfig::env["MAKEPAD_GPUSIM_DPI"] to a positive number scales screenshots to that pixel density for existing suites; it does not select a software renderer or change the native window's DPI.

Cleanup first uses /gq to save a final frame and quit. If capture is unavailable or the app remains alive, it sends /quit. A dropped response is not treated as proof that the process failed to exit. The runner waits for its owned Child and kills only that child after graceful shutdown times out. This cleanup also runs after test panics. It never stops, replaces, or drives user-owned instances.

Troubleshooting

If a test times out or fails to resolve a widget:

  1. inspect target/makepad_test/.../logs.txt
  2. inspect widget-snapshot.json for text/value/checked/selected state
  3. inspect widget-tree.txt for the raw compact tree
  4. verify the selector is scoped tightly enough

For startup failures, inspect build-stderr.txt, app-stdout.txt, and app-stderr.txt. shutdown.txt records whether the child exited normally or required the exact-PID fallback. For live diagnostics inside a test, app.pid(), app.remote_endpoint(), and app.grab_dir() identify only that owned instance.

A closed by user response is preserved as an error; the runner does not relaunch a dismissed app. Grabs have a longer request timeout because the backend must finish readback and PNG encoding.

Current Limitations

  • the macro targets its current package; explicit configuration can select another
  • synchronous API only
  • no visual diffing or trace viewer yet
  • some complex widgets still need more structured state over time

The native runtime and lifecycle fixtures are currently validated on macOS. Other platform backends may differ in capture support.