Testing
Testing
The Testing page reads back a session you recorded at the table and turns it
into a repeatable test scenario. Play a ball, record it, open the recording
here, and take away a .cade.test file that asserts the game scored exactly what
it scored the day you played it.
It is a read-only surface. Nothing on it edits the table, which is why the usual editing shortcuts do nothing here — see what it doesn’t bind.
Reach it from Testing in the top bar. It shares the shared editor shell with the other editors.
Recording a session
Recording starts at the table, not here. The Record button in the engine panel on the table page asks first:
Record a session? Recording loads the full engine and starts a new game, resetting the current score.
Confirming reloads the page with the full engine and arms two recorders at once. The button then reads Stop recording; pressing it stops both and downloads the result immediately:
| File | What it holds |
|---|---|
showcade-session-<timestamp>.json | The session — every event, in order, with its score |
showcade-session-<timestamp>.rec.jsonl | The paired physics trace, which is what the display is derived from |
They share one timestamp, and that shared name is the pairing. If anything was dropped while recording, you are asked whether to export the incomplete recording or discard it before either file is written.
Back here, Open takes both files at once — pick them together in the file dialog. Files are routed by their content, so it doesn’t matter which order you select them in. A session on its own opens fine, but Playback stays gated until a trace is paired with it. Pairing happens through Open only.
The first thing you see is a demo. With nothing opened, the page loads a bundled Sample session — two players, three ball turns, eight events. The Details rail labels it as the bundled sample, so you can always tell it from a recording of your own.
The four views
Switch with the pill tabs, or with the nested leaves under Testing in the nav panel. A breadcrumb drills through three scopes — Session ▸ Ball ▸ Cascade — and changing view never leaves the scope you are in.
| View | What it is for |
|---|---|
| ▶ Playback | Replaying the display the session produced |
| ▤ Timeline | The whole session at a glance, one lane per ball |
| ⋔ Cascade | One ball’s causal chains, as a graph |
| ☑ Expectations | The generated test scenario, and running it |
Playback is where you land.
Playback
Playback replays the dot-matrix display, derived from the recording’s switch stream — not the playfield. There is no ball to watch again.
Deriving those pixels needs the table the session was played on, and the
recording doesn’t carry one: the Details rail says so plainly. So paste or drop
the table’s .cade source into the box above the panel and press Compute.
Nothing verifies that you supplied the right table.
Once it has computed, the transport along the bottom gives you ▶ play, II
pause, a scrub slider over the whole trace, a Loop checkbox (off to start),
and a t=1.23s / 45.67s readout.
An Align to event cursor checkbox beside the panel ties the display’s step cursor to the event cursor the other views use. It is available only when the trace and the open session agree on which session they came from — alignment is refused rather than guessed — and it works one way: checking it makes the display follow the event cursor, never the reverse.
Timeline
One lane per ball turn, in order, each headed with a chip like p1 · Ball 2.
A ball that scored nothing still gets its lane. Along each lane the events are
small boxes showing the event’s short name and, when it scored, +points, with
causal parent-to-child links drawn between them.
The horizontal axis is event index, not wall clock — pinball data is bursty enough that a real time axis would spend most of its pixels on nothing. Dead air appears as a fixed blank gap rather than scaled empty space.
Click an event to select it and move the event cursor. Clicking one that belongs to a different ball is taken as a deliberate scope change and drills the breadcrumb to that ball. The view fits the whole session when it is built, and stops re-fitting once you have panned or zoomed yourself.
Every event paints one of three states against the cursor, and the Details rail names them in words: Already fired, Firing now, Not yet fired.
Cascade
This is the same cascade the runtime means: a triggering switch or event at the root, and everything it caused hanging beneath it. The Details rail calls the root a Cascade trigger and the rest Cascade effects, and shows the cascade’s id, trigger, event count, points, and start.
The graph is laid out once and time is a cursor over it — past events dim, the current one is bold, later ones dimmer. Nothing moves when you step, which is what makes the shape readable. Double-click a node to drill into that one cascade; double-click empty canvas, or press Escape, to come back out.
The Cascade view is scoped to one ball. Only the Timeline plays a whole session.
The event transport
Timeline and Cascade share a floating transport:
| Control | What it does |
|---|---|
| Ball and cascade crumbs | Where you are — p1 · Ball 2 ▸ device.bumper.hit |
| ▶ / ❚❚ | Play and pause the event cursor |
| ◀ / ⇥ | Previous and next event |
| Readout | Event 3 of 8, or Before first event |
| Jump box + Go | Jump to an event by number (1-based; blank or 0 means before the first) |
− 1.0x + | Half and double speed, from 0.1× to 10× |
| Auto-pause box | Comma-separated event names — playback stops the moment one fires |
| Follow playhead | Keep the cursor’s event in view |
Events are numbered from 1 everywhere a person reads them, and “before the first event” is said in words rather than called event 0. Previous, Next, and Go pause playback first; the arrow keys do not.
Expectations
Opening a recording generates the whole scenario for you — there is no
per-event “add an assertion” gesture. The view shows the generated .cade.test
text with Copy and Download buttons above it, then the same table-source
box Playback uses and a Run button.
The generated file is a scenario block: a name and description, an
initial_state with one player block each, then one event per recorded
event. Each event’s id comes from the event’s own name — repeats get a numeric
suffix — its time is the gap since the previous event, and an event that scored
carries a points assertion:
event "device_bumper_hit_3" {
type = "device.bumper.hit"
time = "+240ms"
expected {
points = 500
}
}Points are the only kind of assertion generated. Ball boundaries and running scores appear as comments between the events.
Run replays the scenario against the table source you supplied and reports a
verdict — PASS — 12/14 events passed in green, or the same line in red — over a
table of Event | Type | Result | Message rows. Editing the table source
clears the previous verdict rather than leaving a stale one on screen.
Download writes <recording-id>.cade.test. That is a real cade scenario: the
structure, the ids, and the relative times deliberately match what cade’s own
session-to-scenario conversion produces.
A table-hash mismatch is not proof of the wrong table. The shipped table was split into several
.cadefiles, and the same content composed from several documents hashes differently from one document. A scenario recorded before that split reports a mismatch nobody caused.
The left drawer
Three sections, all closed until you open them:
- Session — the event tree, with a Find an event… box.
- Analysis — the most easily missed thing on the page: a session summary (config, duration, players, total score, balls played), a per-ball table, Top scoring devices, Timing gaps, and Anomalies graded by severity.
- Expectations — a Copy/Download mirror that works from any view.
On a phone-width screen every floating panel, transport included, hides behind a single pull-tab.
Keyboard
| Key | Action |
|---|---|
| ← / → | Previous / next event |
| Home / End | Jump to before the first event / to the last |
| / | Focus the Find an event… box |
| Escape | Drill out one scope level |
| Enter | Commit the jump box, or the auto-pause names |
Double-clicking a cascade node drills in; double-clicking empty canvas drills out.
Everything that edits is deliberately unbound here — no delete, rename, copy, paste, duplicate, undo, redo, audition, or palette digits. There is nothing on this page to change, and a shortcut that silently did nothing would be worse than one that isn’t there. The palette says the same thing in words:
A recording is a record of what happened — it can’t be added to.
The toolbar’s Copy link captures the open recording, the scope, the view, and the selected event, and Back and Forward move between them. A trace you attached in this tab is not carried by that link.
What it needs
There is no sign-in gate and no sample fallback: every operation here is an engine
call, so the page waits for the engine and then reports honestly if it isn’t
there. The status line reads Testing · no recording loaded, then the recording’s
name, plus whether a physics trace is paired with it.