terainia/test/peek.py
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

114 lines
3.3 KiB
Python

"""
One-shot inspection: attach to the running launch.py browser via CDP,
screenshot the canvas, and dump current game telemetry (scene time,
player position, camera angles, hp).
Read-only. Cannot send inputs, change state, or close the browser.
Usage:
python3 peek.py
python3 peek.py --json
python3 peek.py --name baseline # custom screenshot filename
"""
from __future__ import annotations
import argparse
import json
import sys
import time
from pathlib import Path
from playwright.sync_api import sync_playwright
HERE = Path(__file__).resolve().parent
SCREENSHOT_DIR = HERE / "screenshots"
CDP_URL = "http://localhost:9222"
# JS evaluated in the page to gather game state. Pure reads only.
TELEMETRY_JS = r"""
() => {
const g = window.voxel_game;
if (!g) return { ready: false };
return {
ready: true,
scene_time: g.get_scene_time?.() ?? null,
position: g.get_position?.() ?? null,
camera_angles: g.get_camera_angles?.() ?? null,
hp: g.get_hp?.() ?? null,
alive: g.is_alive?.() ?? null,
canvas_size: (() => {
const c = document.getElementById('game-canvas');
return c ? { w: c.width, h: c.height } : null;
})(),
};
}
"""
def attach_page():
p = sync_playwright().start()
try:
browser = p.chromium.connect_over_cdp(CDP_URL)
except Exception as e: # noqa: BLE001
p.stop()
sys.exit(f"could not attach to launch.py via {CDP_URL}: {e}")
pages = [pg for c in browser.contexts for pg in c.pages]
if not pages:
browser.close()
p.stop()
sys.exit("no open pages")
return p, browser, pages[-1]
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--name", default="peek", help="screenshot name prefix")
ap.add_argument("--json", action="store_true", help="JSON output only")
args = ap.parse_args()
SCREENSHOT_DIR.mkdir(exist_ok=True)
p, browser, page = attach_page()
try:
try:
telemetry = page.evaluate(TELEMETRY_JS)
except Exception as e: # noqa: BLE001
telemetry = {"ready": False, "error": str(e)}
shot = SCREENSHOT_DIR / f"{int(time.time())}_{args.name}.png"
try:
page.screenshot(path=str(shot), full_page=False)
except Exception as e: # noqa: BLE001
shot = None
print(f"[!] screenshot failed: {e}", file=sys.stderr)
result = {
"url": page.url,
"screenshot": str(shot) if shot else None,
**telemetry,
}
if args.json:
print(json.dumps(result, indent=2))
else:
print(f"URL: {result['url']}")
print(f"SHOT: {result.get('screenshot')}")
print(f"READY: {result.get('ready')}")
if result.get("ready"):
print(f"TIME: {result.get('scene_time'):.2f}s")
pos = result.get("position") or []
print(f"POS: ({', '.join(f'{v:.2f}' for v in pos)})")
ang = result.get("camera_angles") or []
print(f"YAW/PITCH: ({', '.join(f'{v:.3f}' for v in ang)})")
print(f"HP: {result.get('hp')}")
print(f"ALIVE: {result.get('alive')}")
finally:
browser.close()
p.stop()
return 0
if __name__ == "__main__":
sys.exit(main())