makepad/skills/makepad-qa-release/references/test-harness.md
Arena Agent fe21c07d84 skills: add Makepad game skills pack ported from threejs-game-skills
Nine agent skills for building Makepad/Rust games, ported from
majidmanzarpour/threejs-game-skills. Same director-routed workflow and
premium bar; runtime rewritten for this fork's game crates.

- makepad-game-director        entrypoint, routing, continuity, asset probe
- makepad-gameplay-systems     loop, movers vs rigid bodies, input, camera, netplay
- makepad-aaa-graphics-builder lighting, shaders, budget, visual scorecard
- makepad-game-ui-designer     HUD, menus, touch and XR UI
- makepad-debug-profiler       defect bisection and profiling
- makepad-qa-release           verification ladder, evidence, packaging
- makepad-3d-generator         CC0 model search, casts, procedural geometry
- makepad-image-generator      texgen textures, palettes, sky, icons
- makepad-audio-generator      sample bank, mixer, material impacts, 3D audio

Written against the real APIs in libs/game/*, libs/sim and apps/arcade:
the game.* verb table, GameRenderer adaptive quality, the packed 6-float
GameMeshVertex layout, script_mod! splash styling, makepad-test driving,
and the BUDGETS.md numbers. No paid generation API is required - the CC0
library plus seeded makepad-game-gen replaces them.

Includes install.sh (Codex/Claude), validate-skills.sh and a repo-aware
probe_assets.sh; both scripts verified against this checkout.
2026-09-05 21:18:58 +00:00

4.5 KiB

Test harness

Which kind of test

Claim Test
A rule holds (physics, scoring, generation, netcode) unit test in the Cx-free crate
Two systems compose correctly integration test with both crates as dev-deps
The app responds to input makepad-test driven test
It looks right driven test + screenshot() + your inspection
It is fast enough probe example in --release + frame_p90_ms()

Prefer the lowest rung that can express the claim. Driven tests are slow and flaky relative to unit tests; use them for what only they can prove.

Unit tests

#[test]
fn checkpoint_counts_once_per_crossing() {
    let mut kit = RaceKit::new(/* .. */);
    cross(&mut kit, 0);
    cross(&mut kit, 0);                       // same checkpoint again
    assert_eq!(kit.standings()[0].lap, 0, "re-crossing must not advance the lap");
}

#[test]
fn generator_is_deterministic() {
    let a = generate_level(1234);
    let b = generate_level(1234);
    assert_eq!(a, b, "same seed must give byte-identical output");
}

Determinism tests matter more here than in most codebases: netplay and (preset, seed, position) replication both depend on them, and a non-deterministic generator fails silently until a second player joins.

Integration tests

When the claim spans crates, add the other crate as a dev-dependency and test the composition. The house precedent: makepad-game-blocks dev-depends on gen and render because "a walker can enter a REAL generated room" and "the car's body does not float above the road" are claims about real geometry meeting real physics. Split across two crates they become two half-tests that both pass while the car hovers — which is exactly what happened.

#[test]
fn car_body_rests_on_the_road() {
    let road = makepad_game_gen::/* generate real road */;
    let car  = Car::new(CarConfig::default());
    let settled = settle(car, &road, 120);
    let lowest = lowest_vertex_y(&real_glb_body(), settled.transform());
    assert!((lowest - road_surface_y).abs() < 0.05, "car floats or sinks: {lowest}");
}

Driven tests

use makepad_test::{makepad_test, TestApp, KeyCode, Selector};

#[makepad_test]
fn pause_and_resume(app: TestApp) {
    app.wait_for_log_contains("game: ready");

    app.press_key(KeyCode::Escape);
    app.wait_for_log_contains("state: paused");
    let paused = app.screenshot();

    app.press_key(KeyCode::Escape);
    app.wait_for_log_contains("state: playing");

    println!("paused shot: {}", paused.display());
}

Available on TestApp: type_text, press_return, press_key, press_key_with_modifiers, touch_down, screenshot, widget_dump, widget_snapshot, wait_for_log_contains, locator(Selector), forward(Vec<StudioToApp>). Each has a try_ variant returning TestResult for when a failure is expected.

widget_dump() prints the tree — use it to discover ids rather than guessing selectors.

Synchronisation

Never sleep. Emit one log line per state transition and wait on it:

log!("game: ready");
log!("state: paused");
log!("score: {}", score);
log!("race: won");

This costs one line per transition and converts the entire game flow into something machine-checkable. It is the highest-value habit in this document.

Asset-dependent tests

They skip without the library, which is correct for CI and dangerous for reporting.

fn require_assets() -> Option<AssetIndex> {
    let idx = load_index().ok()?;
    if idx.is_empty() {
        eprintln!("SKIP: run ./apps/arcade/download_assets.sh for real-asset coverage");
        return None;
    }
    Some(idx)
}

Check test output for skips before reporting a pass. And assert loading, since a bad id degrades silently:

assert!(renderer.model_is_loaded("kenney/racing/vehicle-truck-yellow"));

Bot playtest

#[makepad_test]
fn five_minute_soak(app: TestApp) {
    app.wait_for_log_contains("game: ready");
    for _ in 0..300 {
        drive_one_second(&app);
        assert!(!app.widget_dump().contains("panic"));
    }
    app.wait_for_log_contains("soak: complete");
}

Catches what unit tests structurally cannot: softlocks, memory growth, frame-time drift, and incomplete reset() (win twice in a row).

Flakiness

A flaky test is worse than no test — it trains everyone to ignore failures. Causes here: sleeping instead of waiting on a log; asserting exact floats (use an epsilon); depending on iteration order of an unordered container; assuming assets exist; and racing the first frame. Fix the cause; never add a retry.