In this guide
E2E scripts, deterministic replay, profiler reports, and debug overlays.
Testing & debugging
Project E2E tests run through the real runtime. Deterministic replay, profiler reports, and debug overlays make regressions visible before they ship.
E2E tests
Tests live at scripts/tests/e2e.lua and return a list of named tests. Each test is a function that runs like a coroutine, so you can yield on waits and scene loads:
local Test = require("demi.test")
local tests = {}
tests[#tests + 1] = {
name = "menu_starts_game",
func = function()
Test.expect_scene("scene://game/menu", 5) -- wait up to 5s
Test.touch("start") -- tap the HUD node
Test.expect_scene("scene://game/main", 5)
Test.expect(Test.node_center("hud/health") ~= nil, "health widget exists")
end,
}
return { tests = tests }
The file must return { tests = tests }. The harness runs each entry in order and prints a summary.
Test API
| Call | Behavior | Returns |
|---|---|---|
Test.touch(node_id) | Taps an interactive HUD node by stable ID — survives resolution, DPI, and safe-area changes | — |
Test.tap(x, y) | Taps a canvas point | — |
Test.swipe(from_node_id, to_node_id, duration?) | Swipe between two HUD node centers (default 0.4 s) | — |
Test.swipe_xy(x1, y1, x2, y2, duration?) | Swipe between two canvas-space positions | — |
Test.wait(seconds) | Waits (yields) for the given time | — |
Test.expect_scene(scene_id, timeout?) | Waits until the scene is active, up to the timeout | — |
Test.expect(condition, message) | Asserts a condition; fails the test with message when false | — |
Test.node_center(node_id) | Center point of a HUD node | Vec2 numeric array ([1], [2]) or nil |
The harness prints one line per test and a final summary:
[test] PASS menu_starts_game.
[test] FAIL menu_starts_game: scene never activated.
[test] SUMMARY passed=3 failed=1.
Prefer stable HUD node IDs and scene IDs over screen coordinates so tests survive resolution and orientation changes. Keep tests deterministic and bounded by explicit timeouts.
Run tests
# Desktop E2E through the real runtime
demi test linux
# Device E2E (attached physical Android device via adb)
demi test android --serial <device>
# E2E during a normal launch
demi run --e2e-tests
# Headless one-frame smoke
DEMI_HEADLESS=1 demi test
Android qualification records lua_tests in build/android/qualification/qualification.json.
Deterministic input replay
demi run \
--input-replay fixtures/smoke.replay.json
Recorded action frames re-apply through the authored context filter. The project fixed timestep must match the fixture. Use replays as CI smoke tests and for bug reproduction.
Profiler
demi run linux --profiler
demi run linux \
--profile-report report.csv --profile-frames frames.csv
--profile-report writes aggregate scope totals; --profile-frames writes per-frame rows. Profile a repeatable scene and keep the CSV with the change that motivated it.
local Profile = require("demi.profile")
Profile.scope("ai_update", function()
updateAI()
end)
-- The scope appears in profiler reports as "ai_update".
Debug overlays
demi run \
--debug-overlays colliders,contacts,grid,entity_ids,draw_order,ui_bounds,profiler
Or set the matching project debug flags. Overlay geometry matches the authored simulation shapes.
Saves
local Save = require("demi.save")
Save.write("slot1", { level = 3, gold = 120 })
local data = Save.read("slot1") -- table, or nil on failure
if data then print(data.level, data.gold) end
Save.set_number("slot1", "gold", 150)
print(Save.get_number("slot1", "gold", 0)) -- value or the default
Save.register_migration(2, 3, function(state, from_version, to_version)
state.level = state.level or 1
return state
end)
print(Save.metadata("slot1")) -- metadata table (autosave, sequence, reason)
print(Save.last_error()) -- string or nil
print(Save.exists("slot1")) -- boolean
Save.delete("slot1")
- Slots survive install, upgrade, and rollback. Migrations run when the stored version is older than the current one.
write_state/read_statehandle structured state blobs alongside slot values.- Inspect a save on disk:
demi save inspect saves/slot1.save.json.
Validation in CI
demi validate examples/minimal_3d
demi validate examples/minimal_2d_android --platform android
demi script check games/my_game/scripts/player.lua
Exit code 1 means validation or test failure — fail the pipeline on it.