terainia/test/README.md
Maximus Gorog f1e007dd63 Test harness: declarative Playwright scenarios + wasm state bindings
Mirrors the cucucaracha (lacucarachanews) toolkit pattern adapted for
the voxel game:

bridges.rs adds TestCommand + Telemetry plumbing:
  - thread_local TEST_COMMANDS queue + TELEMETRY snapshot.
  - drain_test_commands() called by App::tick at frame start.
  - publish_telemetry(t) called at frame end.
  - wasm_api exports: set_scene_time, teleport, look_at, plus
    getters get_scene_time / get_position / get_camera_angles.

app.rs:
  - drain_test_commands() applies SetSceneTime / Teleport / LookAt
    before physics integrates. Teleport zeroes velocity and syncs the
    camera to feet+EYE_HEIGHT.
  - publish_telemetry() at end of tick exposes scene state to JS.

test/:
  launch.py     Open Chromium with persistent profile + CDP:9222.
                Navigates to https://voxel.mxvs.art by default;
                --url for local dev.
  peek.py       Attach via CDP, screenshot canvas, dump telemetry
                (scene_time, position, camera angles, hp). Read-only.
  run.py        Execute a YAML scenario:
                  wait_for, wait, eval, key, mouse_move, mouse,
                  screenshot, assert
                Key allowlist prevents stray scenarios from sending
                arbitrary input.
  requirements.txt   playwright + PyYAML.
  README.md          Setup, grammar, available bindings, why this
                     exists.
  scenarios/
    lighting-times-of-day.yaml   Screenshots at noon / afternoon /
                                  sunset / civil twilight / midnight
                                  / sunrise. Verifies the Round A
                                  sunset fixes by visual diff.
    god-rays-look-at-sun.yaml    Pointed at the sun at four altitudes
                                  to inspect the Round B shafts.
    voxel-construction-darkness.yaml  Visual baseline for the sky_vis
                                  bake from Round D.
  .gitignore                     Excludes the browser profile +
                                  screenshots directory.

Visual regression workflow:
  1. python3 launch.py
  2. (separate terminal) python3 run.py scenarios/lighting-times-of-day.yaml
  3. Compare screenshots/lighting-times-of-day_*.png against baseline.

Tests still 63 passing. Native + wasm release clean.
2026-05-24 10:51:17 -06:00

3.8 KiB

Test harness — declarative visual + behavioral scenarios

Mirrors the cucucaracha (lacucarachanews) toolkit pattern: launch.py opens a Chromium with persistent profile + CDP on port 9222; small attach-only tools drive the same session via Playwright.

What's different: the game exposes a small set of wasm bindings so scenarios can declaratively set scene state and read back telemetry, not just click DOM elements. See src/bridges.rs wasm_api for the exports.

Setup (once)

python3 -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt
playwright install chromium

Open the game in a controllable browser

python3 launch.py                              # opens https://voxel.mxvs.art
python3 launch.py --url http://localhost:8080  # local dev server

The browser stays open; profile lives at ./.browser_profile/. CDP listens on localhost:9222 so the other tools can attach.

One-shot inspection / screenshot

python3 peek.py            # screenshot + dump game telemetry
python3 peek.py --json     # machine-readable

Writes a PNG to screenshots/<ts>_peek.png.

Run a scenario

python3 run.py scenarios/lighting-times-of-day.yaml

Scenarios are YAML lists of steps. Each step is one of:

step meaning
wait_for: <js_expr> block until js_expr evaluates truthy
wait: <ms> sleep that many ms
eval: <js> run JS in the page (state setters, etc.)
key: <key> [hold: ms] press a key (optionally hold)
mouse_move: [dx, dy] relative mouse motion
mouse: <down|up|click> mouse button events
screenshot: <name>.png save canvas screenshot to screenshots/
assert: <js_expr> fail scenario if js_expr is falsy

Available game-state JS bindings

All exported by the wasm module (see src/bridges.rs::wasm_api). After wait_for: "window.voxel_game !== undefined", call as window.voxel_game.<fn>(...).

Setter Effect
set_scene_time(t: f32) jump shader time to t seconds
set_time_scale(s: f32) freeze (0) / fast-forward time
teleport(x, y, z) move player feet to (x,y,z)
look_at(yaw, pitch) set camera angles (radians)
set_paused(b) pause input + physics
set_fov(deg) / set_mouse_sens(s) / set_render_distance(blocks) settings
respawn() one-shot respawn request
Getter Returns
get_scene_time() f32
get_position() [x, y, z]
get_camera_angles() [yaw, pitch]
get_hp() / is_alive() from death/respawn state

Why this exists

When something looks wrong (e.g. "tops of blocks don't react to sunset"), the dev loop without this is: deploy, open browser, fiddle the time slider, compare to baseline by memory. With this, the loop is: write a scenario that screenshots the same view at noon / sunset / midnight, run it, diff the screenshots against a baseline. Bugs become visible in one command.

For non-visual behaviors (e.g. "is the joystick releasing correctly?") the same harness sends keyboard events and reads back position telemetry to assert "after release, player stops moving within N ms".

Sanity guarantees

  • Persistent profile means cookies, settings, and game state survive across launch.py runs. The first launch is "fresh"; subsequent ones resume where you left off.
  • launch.py never touches the page beyond the initial navigation. Scenarios drive the session; the launcher just hosts the browser.
  • All tools attach via CDP — closing them doesn't close the browser.