From 6018b71d0b380745e464cc506cfb4965f36cefcf Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 16 Jun 2026 16:47:32 -0700 Subject: [PATCH 1/2] fix(webview): intercept subframe document requests before frame is known Stock WebKit RDP has no Page.frameAttached, so a subframe's document request arrives before Page.frameNavigated creates its frame. The request was dropped, which stalled intercepted subframe loads (the route handler never ran). Let such a request through frameless: requestStarted (and the inflight helpers / requestFailed) now tolerate a null frame and skip the per-frame bookkeeping, so the route still fulfills/continues it. The frame is created normally once Page.frameNavigated arrives. --- packages/playwright-core/src/server/frames.ts | 16 ++++++++-------- .../webkit/webview/wvInterceptableRequest.ts | 5 +++-- .../src/server/webkit/webview/wvPage.ts | 12 ++++-------- 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index 31fdaca340b9e..7afcb5a7f5b39 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -321,9 +321,9 @@ export class FrameManager { } requestStarted(request: network.Request, route?: network.RouteDelegate) { - const frame = request.frame()!; + const frame = request.frame(); this._inflightRequestStarted(request); - if (request._documentId) + if (frame && request._documentId) frame._setPendingDocument({ documentId: request._documentId, request }); if (request._isFavicon) { // Abort favicon requests to avoid network access in case of interception. @@ -350,9 +350,9 @@ export class FrameManager { } requestFailed(request: network.Request, canceled: boolean) { - const frame = request.frame()!; + const frame = request.frame(); this._inflightRequestFinished(request); - if (frame.pendingDocument() && frame.pendingDocument()!.request === request) { + if (frame && frame.pendingDocument() && frame.pendingDocument()!.request === request) { let errorText = request.failure()!.errorText; if (canceled) errorText += '; maybe frame was detached?'; @@ -377,8 +377,8 @@ export class FrameManager { } private _inflightRequestFinished(request: network.Request) { - const frame = request.frame()!; - if (request._isFavicon) + const frame = request.frame(); + if (request._isFavicon || !frame) return; if (!frame._inflightRequests.has(request)) return; @@ -388,8 +388,8 @@ export class FrameManager { } private _inflightRequestStarted(request: network.Request) { - const frame = request.frame()!; - if (request._isFavicon) + const frame = request.frame(); + if (request._isFavicon || !frame) return; frame._inflightRequests.add(request); if (frame._inflightRequests.size === 1) diff --git a/packages/playwright-core/src/server/webkit/webview/wvInterceptableRequest.ts b/packages/playwright-core/src/server/webkit/webview/wvInterceptableRequest.ts index b88806940b4eb..7ecdf9e6cfa8c 100644 --- a/packages/playwright-core/src/server/webkit/webview/wvInterceptableRequest.ts +++ b/packages/playwright-core/src/server/webkit/webview/wvInterceptableRequest.ts @@ -22,6 +22,7 @@ import * as network from '../../network'; import type * as frames from '../../frames'; import type * as types from '../../types'; import type { Protocol } from './protocol'; +import type { WVBrowserContext } from './wvBrowser'; import type { WVSession } from './wvConnection'; @@ -49,7 +50,7 @@ export class WVInterceptableRequest { _timestamp: number; _wallTime: number; - constructor(session: WVSession, frame: frames.Frame, event: Protocol.Network.requestWillBeSentPayload, redirectedFrom: WVInterceptableRequest | null, documentId: string | undefined) { + constructor(session: WVSession, browserContext: WVBrowserContext, frame: frames.Frame | null, event: Protocol.Network.requestWillBeSentPayload, redirectedFrom: WVInterceptableRequest | null, documentId: string | undefined) { this._session = session; this._requestId = event.requestId; const resourceType = event.type ? toResourceType(event.type) : (redirectedFrom ? redirectedFrom.request.resourceType() : 'other'); @@ -61,7 +62,7 @@ export class WVInterceptableRequest { // the Playwright-patched WebKit build which base64-encodes it. postDataBuffer = Buffer.from(event.request.postData, 'utf8'); } - this.request = new network.Request(frame._page.browserContext, frame, null, redirectedFrom?.request || null, documentId, event.request.url, + this.request = new network.Request(browserContext, frame, null, redirectedFrom?.request || null, documentId, event.request.url, resourceType, event.request.method, postDataBuffer, headersObjectToArray(event.request.headers), this._wallTime); } diff --git a/packages/playwright-core/src/server/webkit/webview/wvPage.ts b/packages/playwright-core/src/server/webkit/webview/wvPage.ts index ec1b60c402cd4..54ac344451b19 100644 --- a/packages/playwright-core/src/server/webkit/webview/wvPage.ts +++ b/packages/playwright-core/src/server/webkit/webview/wvPage.ts @@ -880,16 +880,12 @@ export class WVPage implements PageDelegate { redirectedFrom = request; } } - const frame = redirectedFrom ? redirectedFrom.request.frame() : this._page.frameManager.frame(event.frameId); - // sometimes we get stray network events for detached frames - // TODO(einbinder) why? - if (!frame) - return; - - // TODO(einbinder) this will fail if we are an XHR document request + // The frame may be null for a subframe document request that arrives before + // Page.frameNavigated creates its frame. + const frame = redirectedFrom ? redirectedFrom.request.frame() : (this._page.frameManager.frame(event.frameId) ?? null); const isNavigationRequest = event.type === 'Document'; const documentId = isNavigationRequest ? event.loaderId : undefined; - const request = new WVInterceptableRequest(session, frame, event, redirectedFrom, documentId); + const request = new WVInterceptableRequest(session, this._browserContext, frame, event, redirectedFrom, documentId); let route; if (intercepted) { route = new WVRouteImpl(session, event.requestId); From f39890a37b0f443c69e88aa52672cac2e36e1269 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 16 Jun 2026 18:41:50 -0700 Subject: [PATCH 2/2] test(webview): unskip frame tests enabled by subframe interception Intercepting subframe document requests lets the selectors-frame / locator-frame route-based tests load their iframes, so unskip the 26 that now pass. Still skipped: the smoke/nested/$ variants that fulfill the main navigation (flaky iOS 26 main-nav interception), the COEP/COOP/CORP isolated iframe (out-of-process), and locator.contentFrame / frameLocator.owner. --- .../expectations/webkit-webview-page.txt | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/tests/webview/expectations/webkit-webview-page.txt b/tests/webview/expectations/webkit-webview-page.txt index 3af3a2e64d488..bd3c95dcdf560 100644 --- a/tests/webview/expectations/webkit-webview-page.txt +++ b/tests/webview/expectations/webkit-webview-page.txt @@ -218,7 +218,6 @@ page/expect-to-have-text.spec.ts › toHaveText with array › fail [fail] page/frame-evaluate.spec.ts › should dispose context on cross-origin navigation [fail] page/frame-evaluate.spec.ts › should dispose context on navigation [fail] page/interception.spec.ts › should intercept network activity from worker 2 [fail] -page/locator-frame.spec.ts › should click in lazy iframe [fail] page/locator-misc-1.spec.ts › should clear input [fail] page/locator-misc-2.spec.ts › should scroll zero-sized element into view [fail] page/locator-query.spec.ts › should allow some, but not all nested frameLocators [fail] @@ -563,9 +562,6 @@ page/to-match-aria-snapshot.spec.ts › should not match what is not matched [fa # tests). Kept skipped until the provisional-target pause gap is addressed. # ============================================================================ page/frame-evaluate.spec.ts › should work in iframes that interrupted initial javascript url navigation [fail] -page/locator-frame.spec.ts › click should survive iframe navigation [fail] -page/locator-frame.spec.ts › locator.frameLocator should work for iframe [fail] -page/locator-frame.spec.ts › should click in lazy iframe [fail] page/locator-frame.spec.ts › should work for iframe @smoke [fail] page/locator-frame.spec.ts › should work for nested iframe [fail] page/locator-frame.spec.ts › should work with COEP/COOP/CORP isolated iframe [fail] @@ -653,13 +649,10 @@ page/page-wait-for-load-state.spec.ts › should wait for load state of about:bl page/page-wait-for-load-state.spec.ts › should wait for load state of empty url popup [fail] page/page-wait-for-load-state.spec.ts › should wait for load state of popup with network url [fail] page/page-wait-for-load-state.spec.ts › should wait for load state of popup with network url and noopener [fail] -page/selectors-frame.spec.ts › click should survive iframe navigation [fail] -page/selectors-frame.spec.ts › should click in lazy iframe [fail] page/selectors-frame.spec.ts › should work for iframe (handle) [fail] page/selectors-frame.spec.ts › should work for iframe @smoke [fail] page/selectors-frame.spec.ts › should work for nested iframe [fail] page/selectors-frame.spec.ts › should work for nested iframe (handle) [fail] -page/selectors-frame.spec.ts › waitForSelector should survive iframe navigation (handle) [fail] page/wheel.spec.ts › should dispatch wheel events after popup was opened @smoke [fail] page/workers.spec.ts › should attribute network activity for worker inside iframe to the iframe [fail] @@ -706,16 +699,9 @@ page/frame-hierarchy.spec.ts › should send "framenavigated" when navigating on page/interception.spec.ts › should intercept network activity from worker [fail] page/interception.spec.ts › should intercept worker requests when enabled after worker creation [fail] page/interception.spec.ts › should work with navigation @smoke [fail] -page/locator-frame.spec.ts › click should survive frame reattach [fail] page/locator-frame.spec.ts › frameLocator.owner should work [fail] -page/locator-frame.spec.ts › getBy coverage [fail] page/locator-frame.spec.ts › locator.contentFrame should work [fail] -page/locator-frame.spec.ts › locator.frameLocator should not throw on first/last/nth [fail] -page/locator-frame.spec.ts › locator.frameLocator should throw on ambiguity [fail] -page/locator-frame.spec.ts › should wait for frame 2 [fail] -page/locator-frame.spec.ts › should wait for frame to go [fail] page/locator-frame.spec.ts › should work for $ and $$ [fail] -page/locator-frame.spec.ts › waitFor should survive frame reattach [fail] page/locator-misc-2.spec.ts › Locator.locator() and FrameLocator.locator() should accept locator [fail] page/network-post-data.spec.ts › should get post data for file/blob [fail] page/network-post-data.spec.ts › should get post data for navigator.sendBeacon api calls [fail] @@ -791,20 +777,7 @@ page/page-wait-for-url.spec.ts › should work with history.pushState() [fail] page/page-wait-for-url.spec.ts › should work with history.replaceState() [fail] page/page-wait-for-url.spec.ts › should work with url match for same document navigations [fail] page/retarget.spec.ts › should check the box outside shadow dom label [fail] -page/selectors-frame.spec.ts › click should survive frame reattach [fail] -page/selectors-frame.spec.ts › click should survive navigation [fail] -page/selectors-frame.spec.ts › should capture after the enter-frame [fail] -page/selectors-frame.spec.ts › should not allow capturing before enter-frame [fail] -page/selectors-frame.spec.ts › should not allow dangling enter-frame [fail] -page/selectors-frame.spec.ts › should not allow leading enter-frame [fail] page/selectors-frame.spec.ts › should work for $ and $$ [fail] -page/selectors-frame.spec.ts › should work for $ and $$ (handle) [fail] -page/selectors-frame.spec.ts › should work for $$eval [fail] -page/selectors-frame.spec.ts › should work for $$eval (handle) [fail] -page/selectors-frame.spec.ts › should work for $eval [fail] -page/selectors-frame.spec.ts › should work for $eval (handle) [fail] -page/selectors-frame.spec.ts › waitFor should survive frame reattach [fail] -page/selectors-frame.spec.ts › waitForSelector should survive frame reattach (handle) [fail] # ============================================================================ # worker-remaining (2 tests) # Web Workers attach over the WebView backend now (Worker.enable is sent during