After more investigation i've found a much better fix for #391.
I'm merging this either way, because it blocks 0.13.0 release and we
already have this fix tested and approved.
By the next release, I want to revert this patch in favour of fixing
core of the issue in simserver.
Regression introduced by
https://github.com/software-mansion/radon/pull/101 adding the streaming
feature to the argent simulator-server build, which made
`/api/screenshot` serve the last streamed frame async instead of
capturing on demand.
<details>
## Summary
Fixes#391 — `screenshot` failing with `no image to export` when more
than one simulator is booted, even with an explicit `--udid`. Regression
in 0.12.x.
## Root cause
The simulator-server captures screenshots from its **live frame
stream**, so a capture only succeeds once the first frame has arrived.
Immediately after the server starts streaming, `POST /api/screenshot`
replies `HTTP 200 { error: "no image to export" }` until that first
frame lands (~0.5–1s). The tool-server requested the capture right after
the server became ready, **losing that race**.
- With a single, foreground simulator the first frame arrives fast
enough that the race is usually won — hiding the bug.
- With more than one simulator booted, the target is often backgrounded
and its first frame is delayed, so the race is lost **reliably** → `no
image to export`.
This became reachable when streaming was enabled for the argent
simulator-server build in 0.12.x (radon
[#101](https://github.com/software-mansion/radon/pull/101)); the 0.11.0
non-streaming build captured frames on demand, so it was immune. Device
selection itself is correct — each device's stream captures the right
device (verified: Pro Max → 1320×2868, iPhone 15 → 1179×2556).
## Fix
`httpScreenshot` now polls past that specific transient (every 250ms, up
to 6s) instead of failing immediately, while still surfacing every other
error (non-2xx, Android `wrong data size`, missing url/path) right away.
## Verification
End-to-end against the **shipped** simulator-server binary with
**three** booted simulators, forcing a fresh sim-server spawn before
each capture:
| | result |
|---|---|
| **unfixed** (current `dist`) | `no image to export` ~2/3 of
fresh-spawn attempts |
| **fixed** | 7/7 success (~800ms), returns a correct PNG (1320×2868) |
- `npm test` (tool-server): **1199 passed**, including new retry /
give-up cases for `httpScreenshot`
- `npx tsc --build`, `prettier --check`, `typecheck:tests`: clean
</details>
## Problem
`screenshot-diff` with `baselinePath` + `captureCurrent: true` — the
tool's own
documented *"common visual-regression flow"* — fails reproducibly on
Android with:
> Screenshot failed: server response missing url or path. The
simulator-server may be misconfigured. Try restarting it.
That message is misleading (the server is healthy), and the flow is 100%
broken on Android.
## Root cause (verified against a live emulator)
`captureLiveInput` requests a **full-resolution** capture (`scale:
1.0`). The Android
simulator-server can't stream some framebuffers at full res and returns
**HTTP 200**
with an `error` field. Confirmed directly against the running
simulator-server:
| request | response |
|---|---|
| `{"scale":0.5}` | `{"path":…,"url":…}` ✅ |
| `{}` (full res) | `{"error":"wrong data size, expected 7853760 got
17627328"}` ❌ |
| `{"scale":1.0}` | `{"error":"wrong data size, expected 7853760 got
17627328"}` ❌ |
(`7853760 = 1080×2424×3`; the emulator returns ~2.24× that.)
Two compounding bugs:
1. **`httpScreenshot`** only reads `error` on a non-2xx status. On the
`200 + {error}` path it falls through to the generic *"missing url or
path … restart it"*, hiding the real cause.
2. **`captureLiveInput`** hard-codes `scale: 1.0` with no fallback, so
the whole diff fails on Android.
## Fix (TS-only)
- **`httpScreenshot`**: when `url`/`path` are missing, surface
`resBody.error` if present (so *"wrong data size …"* reaches the caller)
before the generic hint.
- **`captureLiveInput`**: attempt full-res, and on any failure **retry
at the server's default scale**, which captures reliably. #285's
same-aspect normalization already makes a scaled capture diff-compatible
with a baseline saved at any scale, and full-res is preserved wherever
it works (iOS). If the fallback also fails, its error propagates.
## Tests
`screenshot-diff-tool.test.ts` — full-res→default-scale fallback
(asserts the `1.0` call then a default-scale retry), and error
propagation when both fail.
`simulator-client.test.ts` (new) — `httpScreenshot` surfaces the server
`error` on a `200+{error}` response, still uses the generic hint when
there's genuinely no info, and returns `url`/`path` on success.
All 21 screenshot-diff/simulator-client tests pass; `tsc` clean on the
changed files.
## Scope note
The underlying inability to capture a full-resolution Android frame is a
**simulator-server (native)** issue — the *"wrong data size"*
framebuffer mismatch. This PR makes the tool **degrade gracefully**
(works at the default scale) and **diagnose clearly** instead of failing
outright; a proper native fix for full-res Android capture is a separate
follow-up.