# App Debugging Reference - Driving the Page with `page eval`

The long tail of [app-debugging](app-debugging.md) - load that skill first for the core loop (`page inspect` → console/resources → `logs fn` → `fn call` → `page screenshot`). This reference is for when reading the page isn't enough and you have to **drive** it: verify state across a reload, feed the app a real file or a real camera frame, or advance a simulation's clock.

Everything here is a flag or a pattern on `gipity page eval <url> <expr>` - see [app-debugging](app-debugging.md) for the base command, its flag table, and the expression rules (it's an *expression*, not a statement block; return the value, don't post-process the output).

## `--step` - many checks, one page load

**Each separate `gipity page` command is a fresh page load.** Every `page eval` / `page inspect` invocation opens the URL anew, so anything you set up in one command (a `window.onerror` trap, a global you assigned) is gone by the next - you can't "install a probe, then read it" across two commands.

**`--step` runs more expressions against the SAME loaded page** - each one after the last, sharing everything the page already paid for (a vision model, a booted game, a connected socket) and everything the previous step left behind. So a several-part check ("it detects my throw" → "it scores the round" → "the tally survives the next round") is one page load, not one per assertion. `--json` returns `{ result, stepResults: [...] }`:

```bash
gipity page eval <url> "document.querySelector('#play').click(), 'started'" \
  --step "(async()=>{ await new Promise(r=>setTimeout(r,1000)); return document.querySelector('#score').textContent; })()"
```

## `--reload` - does the state survive a refresh?

**`--reload` gives you a two-phase check across a reload**: the expressions run, the page reloads in place (localStorage/sessionStorage/cookies preserved), then the `--reload` expression runs against the post-reload DOM. That is the direct test for "remember it if I close the tab and come back" - seed state, reload, assert the restored UI - no same-origin-iframe harness needed:

```bash
# Phase 1 saves state through the app's own UI; phase 2 proves it survives a reload:
gipity page eval "<url>" \
  "(()=>{ document.querySelector('#label').value='Trip'; document.querySelector('#save').click(); return localStorage.getItem('countdown'); })()" \
  --reload "({ restored: !!localStorage.getItem('countdown'), heading: document.querySelector('#display')?.textContent })"
```

`--reload-file <path>` reads the reload expression from a file.

## Feeding the page real bytes and long scripts

**Verifying against a real file (MP3, image, PDF, a multi-KB data blob)? Use `--fixture`, don't inline the bytes.** The eval body is capped at 64 KB (it's delivered as one shell argument to the sandbox), and the eval runs *in the page* - so bulk/binary data can't be pasted into the expression. `--fixture <path>` hosts a local file as a public asset, hands your eval a `fetch`-able `fixtureUrl`, runs the eval, and **deletes the hosted copy afterwards** - no manual upload/deploy/cleanup dance. The page fetches it in-page and drives the app's own code over the real bytes:

```bash
# Real binary in, parsed result out. fixtureUrl is injected into the eval scope:
gipity page eval <url> --fixture ./sample.mp3 \
  "(async()=>{ const buf = await fetch(fixtureUrl).then(r=>r.arrayBuffer()); const tags = await window.MyApp.parseId3(buf); return { title: tags.title, hasCover: !!tags.cover }; })()"
```

Pass `--fixture` more than once for several files; each is exposed under `fixtures['<basename>']` (and the first as `fixtureUrl`). Any size, binary-safe - the bytes travel over HTTP, never through the eval body.

**Large or multi-line eval body? Put it in a file with `--file <path>`, don't inline it.** A long or multi-line expression pasted as a shell argument is where quoting breaks - a stray quote/paren/newline and the shell hands `page eval` a mangled (or empty) `<expr>`, so the CLI sees a missing argument and prints the usage help instead of running anything. `--file ./probe.js` reads the body from disk verbatim (no shell quoting in play) and runs it exactly as if it were the inline `<expr>`:

```bash
gipity page eval <url> --file ./probe.js
```

**App-relative `import()` works.** A dynamic `import('./packages/i18n/index.js')` inside the eval resolves against the **page URL**, so you can load and drive the app's own ES modules without hand-building the deployed `/account/project/` path. Only literal `./` and `../` specifiers are rewritten; absolute paths and full URLs pass through unchanged.

**The eval runs against the live page with a ~20s in-page execution budget.** A single expression that awaits several slow things (network, animation timers, polling a selector in a loop) can blow that budget; you'll get a timeout message telling you to split it. Prefer one focused expression per fact, or use `--wait-for <selector>` to gate on readiness instead of `await`-ing inside the body.

## Catching an error the console didn't show you

(Only the `gipity page` CLI needs this - the web agent's `browser` tool is a sticky session that persists across calls and auto-captures errors.) To catch a **boot-time or runtime error**, the page itself has to record it, then you read it in a *single* eval. Add a tiny logger to the app's own boot (it ships with the app, so it's present on every load):

```js
// in the app's entry, before anything that might throw:
window.__errs = [];
addEventListener('error', (e) => window.__errs.push(e.error?.stack || e.message));
addEventListener('unhandledrejection', (e) => window.__errs.push('REJECTION: ' + (e.reason?.stack || e.reason)));
```

```bash
# then one eval reads what threw during this load:
gipity page eval "<url>" "JSON.stringify(window.__errs)" --wait 4000
```

For errors that only surface after async work or a state change, give them time with `--wait`, or gate on `--wait-for "<selector that appears once settled>"`. (`page inspect`'s **Console** already captures uncaught errors for the load it snapshots - reach for the in-app logger when you need errors from a specific later moment, or a value the console doesn't show.)

A message-less `error: (...)` line in **Console** is a cross-origin "Script error" from a **third-party** `<script>` (e.g. a CDN) loaded without `crossorigin="anonymous"` - not your own logic (the Gipity SDK sets it, so its errors show real messages; the `window.__errs` trap can't see cross-origin ones). Add `crossorigin="anonymous"` to that `<script>` (the CDN must also send `Access-Control-Allow-Origin`) to read the real message.

## Camera apps: point the headless browser at a picture

The camera is **not** one of the things the headless browser can't do (see "What the headless browser can't test" in [app-debugging](app-debugging.md)) - `--camera <image>` gives `page eval` / `screenshot` / `inspect` a synthetic webcam that plays your image on a loop (and a synthetic mic, so `getUserMedia` never prompts or fails). The app's real `getUserMedia` → inference → `onResult` → UI pipeline runs against it, so you read the app's **own** output instead of a hand-fed fake:

```bash
# The picture IS the camera. Read whatever the app renders from it.
gipity page eval <url> --camera ./fist.jpg --wait 6000 \
  "(()=>{ document.querySelector('#start').click(); return new Promise(r => setTimeout(() => r(document.querySelector('#throw').textContent), 4000)); })()"

gipity page screenshot <url> --camera ./fist.jpg --wait 6000   # see the overlay + label it drew
```

Any still image works (PNG/JPEG/WebP/GIF, local path or URL). The model downloads and compiles on the first frame, so the script gets a roomier in-page budget on `--camera` runs (raise it with `--timeout <ms>` if a big model needs longer). That load is the expensive part - **check everything about the app in ONE camera run, using `--step`** (above) rather than a fresh `--camera` eval per assertion; the model stays warm across steps. Don't rebuild the app to expose test hooks on `window`: a debug surface compiled into the app the user opens is not verification, and it isn't needed.

## Time-dependent logic: step the app's own clock

**Never wait on real time - step the simulation.** The headless browser has no GPU, so a WebGL scene software-renders at ~2-3 fps. Game engines cap their per-frame delta for stability (the 3D templates cap at 1/30s), which means **simulated time advances only as fast as frames paint** - roughly a *twelfth* of wall time. Sleep 3 real seconds expecting a 3-second charge and the player creeps about one unit - the assertion fails as a **false negative**, not an app bug. It bites physics, animation, canvas, and `2d-game` timers alike.

When the page paints too slowly to trust a wall-clock wait, `page eval` says so:

```
⚠ Slow render: page painted at 2.6 fps. Waiting on real time (setTimeout) advances animation/physics time far slower than it looks …
```

The fix is to advance the simulation yourself, without rendering - deterministic and ~350× faster. The 3D templates export `advance(seconds)` for exactly this; it runs the same physics/player/game/feature chain a real frame runs, just without the paint:

```bash
# Does a dropped block actually fall and settle?
gipity page eval <url> "const core = await import('./js/core.js');
  await core.whenReady();                       // real boot, no guessed sleep
  const b = core.primitives.createPart({ position:{x:0,y:20,z:0} });
  core.advance(4);                              // 4s of WORLD time, ~150ms real, deterministic
  return JSON.stringify({ dropped: +(20 - b._body.translation().y).toFixed(1), asleep: b._body.isSleeping() });"
# → {"dropped":20.4,"asleep":true}
```

Because `advance()` runs a fixed step, the same `seconds` gives a bit-identical result every run, on any machine - assert exact values, not tolerances. Advance in whole seconds until the bodies you care about report `isSleeping()`, rather than picking a duration and hoping.

For an app without such a hook, expose one: keep the step function pure (`function tick(dt)`), call it from your `requestAnimationFrame` loop, and put it on `window` so an eval can drive it directly. Don't `setTimeout` and hope.

## Related skills

- [app-debugging](app-debugging.md) - the core loop this reference extends
- [app-testing](app-testing.md) - testing functions server-side (`ctx.fn.call` / `callAs`, the isolated test DB)
- [web-vision-mediapipe](web-vision-mediapipe.md) / [web-vision-detect](web-vision-detect.md) - the camera apps `--camera` verifies
- [3d-engine](3d-engine.md) - the `advance(seconds)` hook the 3D templates export

