`assertElementTextAfterScrolling` waited 1000ms per attempt. The wait's whole
budget goes to its first capture, so that budget has to cover one snapshot of
the current surface; on a loaded simulator it does not, and every attempt fails
with `wait_capture_stalled` instead of reporting the element as off-screen.
This broke the iOS Smoke Tests lane on main. #1484 moved the smoke automation
scenario onto this helper for `automation-press` and `automation-longpress`;
before that it was only reached from the full lane, so the tight budget went
unnoticed. Every sibling wait in the same scenario already budgets 2500ms or
more (`label="Settings"` 10000, `alert wait` 5000, `wait text` 2500).
Two changes:
- Raise the per-attempt budget to 2500ms, matching the nearest sibling.
- Stop spending a scroll attempt on a stalled capture. A stall means the
snapshot never came back, so the surface was never read — it is not evidence
the element is off-screen, and scrolling on it moves the surface for an
unrelated reason. Two stall retries absorb a slow runner; a genuine absence
still consumes attempts and still fails.
The final assertion now carries the last wait's JSON, so a future failure says
whether it stalled or genuinely never found the element.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8
Fixes the iOS Smoke Tests lane, red on
mainfor the last three runs.The failure
ios.ymlonmain:209ac83df❌,1fc916918❌,6067b9a48❌,32b9db2d7⚪ cancelled,0e51007b0⚪ cancelled,e70fac7b4✅ last green.Why it started failing
wait_capture_stalledis not "element not found".createWaitPollingonly classifies a timeout ascapture-stalledwhen the capture that timed out was the first one — the one handed the wait's entire budget (src/commands/interaction/runtime/wait-polling.ts:40-56). So the message is precisely: one snapshot of the current surface did not complete within 1000 ms.assertElementTextAfterScrollinghas budgeted 1000 ms per attempt since #1408, but until recently it was only reached fromlive-full-scenarios.ts. #1484 rewrote the smoke automation scenario to routeautomation-pressandautomation-longpressthrough it, replacing a fixedscroll down --pixels 120plusis visiblechecks. That put the tight budget on the smoke lane, where a single capture of that surface exceeds it on a loaded runner.The budget is also an outlier among its own neighbours — every other wait in that scenario allows far more:
label="Settings"alert waitwait text "Automation lab"Because the stall repeats on every attempt, the loop scrolls three times on no evidence and then fails — the element's actual visibility is never established either way.
The change
Two edits to
assertElementTextAfterScrolling, both intest/integration/ios-simulator-e2e/live-assertions.ts:The terminal assertion now includes the last wait's JSON, so the next failure distinguishes "stalled" from "genuinely never appeared" without needing the raw job log.
This is deliberately confined to the test harness. I found no product-side regression on the capture path between the last green commit and now — the only non-test changes there in that window are a dead-export deletion in
src/snapshot/snapshot-processing.tsand Maestro-specific adapters that plainwaitdoes not use.Verification
pnpm lint✅ ·pnpm typecheck✅ ·pnpm exec oxfmt --check✅ on the changed file ·smoke-ios-simulator-coverage.test.ts12/12 ✅ (the device-free contract guarding these scenarios).I could not execute the live simulator lane — no macOS, no Xcode, and this fix is by nature only exercised against a real simulator. CI on this PR is the verification that matters; the iOS lane running green here is the pass criterion.
One caveat worth stating plainly: raising a timeout makes a lane that stalls occasionally pass, but if a capture of this surface routinely takes over ~2.5 s that is a product-side problem this PR would only be masking. The
wait_capture_stalleddiagnostic added to the final assertion exists so that case is visible rather than silent.Generated by Claude Code