fix(features): browser waits cover a click's navigation and first paint
Fixes two timing gaps in the shared browser action runner (runBrowserActions, packages/features/src/browser-agent-cdp.ts), found driving a real Mac Browser tab on 2026-10-21. The runner serves both the cloud sandbox browser and the Mac's tabs, so both fixes live there.
1. wait after a navigating action waits for that navigation
Before: type, type, click Login, wait, screenshot captured the login page still filled in. The wait (no ref) answered at once, because the old document still read readyState === "complete".
Now: when an action that may navigate (click, key Enter, navigate, back/forward) is followed directly by a wait on the same tab, the runner subscribes to that tab's events and sends Page.enable before the action runs. The wait then:
- gives a navigation 500 ms (
NAVIGATION_GRACE) to start, meaningPage.frameStartedLoadingorPage.frameNavigated; - if one starts, waits until every frame it started fires
Page.frameStoppedLoading, or untilPage.loadEventFired; - then runs its usual check (page loaded, or the ref drawn).
All of this runs inside the wait's own existing timeout. If nothing starts within the grace, the wait answers as it did before. A wait after any other action, or with no navigating action before it, changes nothing and sends no new command. If a browser refuses Page.enable, the wait falls back to its usual check.
2. Screenshot right after open no longer fails on the first frame
The Mac refusal comes from mac-browser-tabs.ts, where tab.capture returns nothing for a view that hasn't drawn yet (NOTHING_DRAWN_WORDS). It reaches the runner as a refused Page.captureScreenshot. The runner now retries a refused capture every 250 ms, up to 8 more times (about 2 s), before failing with the same words as before. Because the retry is in the runner, Chromium's own refusal of an unpainted page gets the same treatment, so both browsers behave the same. mac-browser-tabs.ts itself is unchanged, which keeps this PR clear of the concurrent edit to macBrowserTabsOver there.
Tests
All hermetic, using TestClock:
browser-actions.test.ts(fake CDP peer):- A click that starts a navigation, followed by
waitandsnapshot, now snapshots the new page. Fails without the fix. - A tab that refuses capture three times is screenshotted, and one that never paints fails after 2 s. Fails without the fix.
- A
waitafter a click that navigates nowhere still answers after the grace, and awaitaftertypeanswers without the clock moving. This is a guard against slowing every wait.
- A click that starts a navigation, followed by
mac-browser-tabs.test.ts(fake tab): a capture that draws nothing twice still yields the screenshot and image. Fails without the fix.
Checks run locally: typecheck for features, host, and service; lint and discarded-effect via pnpm check:affected; the host and service browser suites (595 tests). check:affected's combined typecheck ran out of memory in the sandbox, so each package was typechecked on its own and passed.
Made with Luke