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 goes through this pair.
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
| Command | Catches |
|---|---|
npm run typecheck | A 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:check | PAGE_DISPATCH.md is stale; also a page no handler can leave and an after naming nothing. |
npm run events:docs:check | EVENTS.md is stale; an emit outside Emit; one event with two payload shapes. |
npm run dispatch:eval | A dispatch or scheduler row changed. |
npm run live:check | A 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:check | CODEMAP.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:check | What each language is missing; a data-i18n naming no key. |
npm run build (its minify step, --verify bundle) | The 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 desktop browser is not the device's engine. The WebView the pages run in
is several Chromium releases behind, and it measures a flex container's
intrinsic width differently: a strip that fits on the desktop can come out with
its buttons clipped on the device. Give a fixed-size flex item an explicit
width, and check any layout change under a browser of that vintage rather
than the one on your desk.
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 frame corpus and the detection regression live outside this package; a fingerprint change is not finished until they have run, and they are 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 stats/,
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, hold the
floating bar's Log button, or let the script write one itself when it gives up.
report-folderBefore 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.