Skip to main content

Test without a device

Almost everything about what the script decides can be checked on the PC in seconds, because the built bundle is a classic script with everything at global scope, and tools/runtime/ can load it into a Node vm with the host's natives shimmed underneath. What runs under test is the production code, not a copy of it.

The host shim

tools/runtime/load.js evaluates build/index.js in a vm; host.js shims the ~15 host primitives under it — captures off a PNG, colour reads, template matching, taps that record rather than tap — faithful to the app's own semantics. Every offline tool, here and in the development toolkit, goes through this pair.

app.gap.Tsum/tools/runtime/load.js
loading...

Driving anything is a few lines: createRuntime() gives you the context and the host, createTsum(ctx, host, frame, meta) a Tsum positioned for one captured frame (a PNG plus an optional geometry sidecar). From there ctx.gPages.sweep(...), ts.isFeverTime(), ts.scanBoardQuick() or a watcher you are writing run against the real frame with the real code.

The shim rebuilds the bundle when a source file is newer than it, so a test run needs no separate build step.

The dispatch and scheduler traces

npm run dispatch:eval answers whether a change to one page changed what happens on another. For every Page entry × navigate goal × kind of look, it sets the router's state, builds the event and runs the real queue over a Tsum whose leaves are replaced: a tap records ["tap", x, y], a sleep records and moves a fake clock, a settle records and moves nothing. The scheduler half builds a run off each preset in tools/dispatchEval/presets/ with the real buildRun and drives tick() under the fake clock. Both traces are pinned in tools/dispatchEval/golden/.

npm run dispatch:eval # compare; exit 1 on any changed row
npm run dispatch:eval -- -v # every changed row, in full
npm run dispatch:eval -- --only StartPage # rows whose id contains this
npm run dispatch:eval -- --dispatch # or --scheduler, for one half
npm run dispatch:update # rewrite the goldens, then read the diff

Invariants are checked on every row whatever the golden says: no image handle left open, no dispatch re-entered from a subscription, no navigate handler run without a goal, nothing but notify after something acted, no tap on a transient page with its window still to run, and the forecast's pick agreeing with the subscription that acted.

The diff dispatch:update produces is the review. A row that moved for a page the change was not about is exactly the failure the tool exists for.

The checks the build runs

CommandCatches
npm run typecheckA method not declared in interface Tsum, a UiText with no English, a TaskName without a body or a label, a misspelt enum member.
npm run pages:docs:checkPAGE_DISPATCH.md is stale; also a page no handler can leave and an after naming nothing.
npm run events:docs:checkEVENTS.md is stale; an emit outside Emit; one event with two payload shapes.
npm run dispatch:evalA dispatch or scheduler row changed.
npm run live:checkA setting a preset carries with no LiveSettings answer, an answer with no case, a value that does not survive the round trip, a held key that writes the world anyway. Drives the built bundle rather than reading it.
npm run map:checkCODEMAP.md names a path that does not exist, a source file or tool nobody listed, a name family that escaped its file, a script not in the command table.
npm run i18n:checkWhat each language is missing; a data-i18n naming no key.
npm run build -- --verify bundleThe shipped dist/index.js still has every name the bridge reaches by name, evaluated under the shim.

npm run build runs the doc and check steps as optional — findings are printed and never block — except live:check, which is required.

The pages in a desktop browser

build/index.html (the settings page before inlining) opens in any browser from file://; without the host bridge the clipboard falls back to the share box and the live-settings poll finds nothing, which is what the feature-detection is for. The Quick Bar has a staged preview the development toolkit can produce, with a stand-in engine behind it.

The desktop browser is not the device's engine. The emulator's WebView is Chromium 110, and it measures a flex container's intrinsic width from what its items contain: a bare flex-basis counts for nothing, and a percentage max-width inside a content-sized box resolves to nothing. A strip that fits on the desktop can come out with its buttons clipped on the device. Give a fixed-size flex item a width, and when a layout change matters, check it under that engine — a Win64 snapshot of 110 run headless with --screenshot --window-size=360,62 draws the strip the way the device does.

What needs a device

Anything about pixels the shim has not seen: a new fingerprint against frames the corpus does not hold, a colour threshold, an animation's real duration, a drag the game refuses when drawn too fast. The development toolkit holds the corpus and the detection regression (pages:eval, pages:calibrate, pages:audit); a fingerprint change is not finished until that has run, and it is the only place a probe threshold should be raised.

On the device, the tools you have are the log (Debug logs on), the annotated screenshots and page-history frames Debug game saves under tsum_record/, Collect unknown screens for a screen nothing fingerprinted, and the issue report — the screen, the screens before it, the settings and the last few hundred records, debug ones included. Press Report on the Debug tab or the Quick Bar, or let the script write one itself when it gives up.

Before believing a behaviour report, confirm what is on the device: which build (ScriptVersion in the log's run.start), which settings (the round.start payload), and which screen (the page-history frames). A "doesn't work" is nearly always a different build, a different setting, or a screen the script has never seen.