fix: wait for main world in cross-origin iframes instead of 1s timeout#2342
fix: wait for main world in cross-origin iframes instead of 1s timeout#2342trippyogi wants to merge 2 commits into
Conversation
Co-authored-by: Cursor <cursoragent@cursor.com>
🦋 Changeset detectedLatest commit: a56940f The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
This PR is from an external contributor and must be approved by a stagehand team member with write access before CI can run. |
There was a problem hiding this comment.
All reported issues were addressed across 7 files
Confidence score: 5/5
- Safe to merge after the addressed issues were fixed.
Architecture diagram
sequenceDiagram
participant Client as "Test / User"
participant FrameLocator as FrameLocator
participant Page as Page
participant CDPSession as "Child CDPSession"
participant ExecutionContextRegistry as ExecutionContextRegistry
Note over Client,ExecutionContextRegistry: NEW: Event-driven wait for cross-origin iframe main world
Client->>FrameLocator: deepLocator().click()
FrameLocator->>FrameLocator: ensureChildFrameReady(frameId)
loop Retry on OOPIF session change
FrameLocator->>Page: getSessionForFrame(frameId)
Page-->>FrameLocator: session (current owner)
FrameLocator->>CDPSession: Runtime.enable (best-effort)
FrameLocator->>CDPSession: Page.setLifecycleEventsEnabled (best-effort)
FrameLocator->>ExecutionContextRegistry: waitForMainWorld(session, frameId, remaining)
alt Main world already cached
ExecutionContextRegistry-->>FrameLocator: executionContextId (immediate)
else Event received before timeout
par Runtime.executionContextCreated fires
CDPSession-->>ExecutionContextRegistry: event {auxData.frameId, isDefault, context.id}
and Page.lifecycleEvent fires
CDPSession-->>ExecutionContextRegistry: event {frameId, name}
end
ExecutionContextRegistry-->>FrameLocator: executionContextId
else Timeout expires (DEFAULT_MAIN_WORLD_TIMEOUT_MS = 15s via shared constant)
ExecutionContextRegistry->>ExecutionContextRegistry: final cache check
alt Found
ExecutionContextRegistry-->>FrameLocator: executionContextId
else Still missing
ExecutionContextRegistry-->>FrameLocator: reject (main world not ready)
FrameLocator->>FrameLocator: catch() -> wait 50ms, retry loop
end
end
end
FrameLocator-->>Client: resolved
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
Co-authored-by: Cursor <cursoragent@cursor.com>
Fixes #2324.
Why
Cross-origin iframe interactions race OOPIF adoption. The
<iframe>element appears in the CDP frame tree quickly, but the child document'sRuntime.executionContextCreatedonly fires after it loads.waitForMainWorldhad a hardcoded 1000ms budget (selectorResolver.ts,frame.ts), andensureChildFrameReadywas best-effort with ~1200ms, so slow-loading third-party iframes fail in about 1s withmain world not ready for frame <id>, while an immediate retry succeeds in ~35ms because the execution context is cached by then. The reported ~1s failure matches the 1000ms budget exactly.What Changed
The wait is now event-driven rather than timeout-driven:
executionContextRegistry.ts: waitForMainWorldwaits onRuntime.executionContextCreatedwith a final cache check before rejecting; sharedDEFAULT_MAIN_WORLD_TIMEOUT_MS = 15_000.frameLocator.ts:ensureChildFrameReadynow blocks until the main world is ready on the correct session, with a retry when OOPIF adoption switches sessions mid-wait.selectorResolver.ts/frame.ts: use the shared constant instead of hardcoded 1000ms.Test Plan
execution-context-registry.test.ts, 4 new tests covering the wait/reject/cache paths.iframe-main-world-loading.spec.tsdelays an iframe's load by 3s and verifiesdeepLocator().click()waits and succeeds instead of throwing.flowlogger-eventstore.test.jsare pre-existing on main atbe0a2f6348716d3b5cce7247b4f85f85047a88aa.frame-get-location-and-click,iframe-ctx-addInitScript-race,locator-count-iframe,iframe-main-world-loading). Remaining iframe specs in the subset require external eval-site hosts that failed withERR_SSL_PROTOCOL_ERRORin this environment — left to CI.Design note for reviewers
The 15s ceiling means a genuinely unreachable frame now fails slower than the old 1s. Happy to wire this into an existing configurable timeout option instead if that's preferred; the constant is centralized so it's a one-line change either way.
Summary by cubic
Switch iframe main-world waiting from a fixed 1s timeout to an event-driven approach so cross-origin iframes don’t fail during slow OOPIF adoption. Fixes #2324 by waiting until the main world exists with a 15s safety net, and capping per-attempt waits to switch sessions quickly.
waitForMainWorldevent-driven viaRuntime.executionContextCreatedwith a final cache check; addedDEFAULT_MAIN_WORLD_TIMEOUT_MS = 15_000.ensureChildFrameReadyto poll session ownership during OOPIF adoption and cap per-attempt waits at 200ms.frame.tsandselectorResolver.tswith the shared constant.Written for commit a56940f. Summary will update on new commits.