waterui-testing 0.3.0

Headless testing helpers for WaterUI
# waterui-testing

Accessibility-first, headless UI testing for [WaterUI](https://github.com/lexoliu/waterui).
Tests run under plain `cargo test` (use `cargo nextest run` in this workspace) against the
Hydrolysis renderer — no simulator, no device, no custom runner.

## Quick start

```toml
[dev-dependencies]
waterui-testing = "0.3"
hydrolysis-m3 = "0.2"
```

```rust
use waterui_testing::{Role, SemanticApp, UiBuilder};

// Mounting form: the macro mounts a no-arg view function.
#[waterui::test(login_view, theme = hydrolysis_m3::install, viewport = (360, 320))]
fn login_flow(app: &mut SemanticApp) {
    app.query().role(Role::BUTTON).label("Login").tap();
    app.query().label("Welcome").assert_exists();
}

// Manual-mount form: the test owns Bindings the view closes over.
#[waterui::test(theme = hydrolysis_m3::install)]
fn stepper_updates(ui: UiBuilder) {
    let value = Binding::i32(2);
    let value_for_view = value.clone();
    let mut app = ui.mount(move || stepper("Limited", &value_for_view));
    app.query().label("Limited").increment();
    assert_eq!(value.get(), 3);
}
```

## Design

- **Theme and render mode are orthogonal.** `.theme(installer)` swaps the theme package
  (plain Hydrolysis test theme by default); `mount()` is the fast semantic runtime,
  `mount_offscreen()` the GPU-backed one. Any theme works in either mode.
- **Interactions are assertions.** `tap`, `set_text`, `increment`, `focus`, drags and key
  presses return `()` and panic when the runtime reports the accessibility action
  unhandled. Tests for disabled/clamped controls assert the panic (`catch_unwind`).
- **Quiescence, not sleeps.** After every interaction the session pumps until the runtime
  is genuinely idle — no queued input, no spawned tasks, no scheduled animations or
  patches. Waits (`wait_for_existence`, `wait_for`) pump hot while work is scheduled and
  touch wall-clock time only for work outside the runtime.
- **Virtual frame clock.** Every pump advances the animation clock exactly one frame, so
  transition sampling is deterministic under any host load. `OffscreenApp::pump_for`
  lands on an exact phase; snapshots capture it via
  `capture_snapshot(suite, case, stage)` (PNGs under `WATERUI_TEST_ARTIFACTS_DIR`).
- **Semantic queries.** `app.query().role(Role::SWITCH).label("Wi-Fi")` with
  `.assert_exists()` / `.single()` / `.all()`, XCTest-style expectations
  (`Expectation`, `WaitOptions`, inverted and ordered waits), and scoping through element
  handles (`Query::within`). Views tagged `.a11y_id("settings.wifi")` resolve via
  `.identifier("settings.wifi")` — the same identifier surfaces to XCUITest and Android
  automation in the native backends.
- **Gesture and keyboard control.** `DragOptions` paces drags (`frame_per_step` gives
  recognizers a real motion timeline); `press_named_key_with` / `press_character_key_with`
  hold explicit `Modifiers`.
- **Performance harness.** `ui().perf(view)` / `perf_with` measure steady-state offscreen
  frames (`PerfConfig`, `PerfReport`) with per-phase Hydrolysis timings and process
  resource samples.

Because the tree under test is the [Hydrolysis accessibility tree](../backends/hydrolysis),
every test doubles as an accessibility-correctness test: a component that cannot be
driven through this crate is a component assistive technology cannot drive either.