From ec680e239889ffbd3ac7d23a319ff684e0cf06e3 Mon Sep 17 00:00:00 2001 From: Devin Rousso Date: Mon, 13 Jul 2026 18:21:21 -0600 Subject: [PATCH 1/2] fix(webview): support input for elements inside cross-origin ` + + `); + await page.waitForSelector('iframe'); + await page.frames()[1].waitForSelector('#childBox'); + const log = () => page.evaluate(() => window['__log']); + const parentBox = (await page.locator('#parentBox').boundingBox())!; + const iframeBox = (await page.locator('iframe').boundingBox())!; + const parentCenter = { x: parentBox.x + parentBox.width / 2, y: parentBox.y + parentBox.height / 2 }; + const childCenter = { x: iframeBox.x + 90, y: iframeBox.y + 90 }; + + await page.mouse.move(parentCenter.x, parentCenter.y); + await expect.poll(log).toEqual(['parent:enter']); + + // Crossing into the iframe leaves the parent element and enters the child. + await page.mouse.move(childCenter.x, childCenter.y); + await expect.poll(log).toEqual(['parent:enter', 'parent:leave', 'child:enter']); + + // Crossing back out leaves the child element and re-enters the parent. + await page.mouse.move(parentCenter.x, parentCenter.y); + await expect.poll(log).toEqual(['parent:enter', 'parent:leave', 'child:enter', 'child:leave', 'parent:enter']); +}); diff --git a/tests/webview/expectations/webkit-webview-page.txt b/tests/webview/expectations/webkit-webview-page.txt index 8383ec40ecfee..19cd7d7ef5cd7 100644 --- a/tests/webview/expectations/webkit-webview-page.txt +++ b/tests/webview/expectations/webkit-webview-page.txt @@ -70,6 +70,21 @@ page/page-screenshot.spec.ts › page screenshot › should work for webgl [fail page/page-screenshot.spec.ts › page screenshot › should work while navigating [fail] page/page-screenshot.spec.ts › page screenshot › should work with Array deleted [fail] +# ============================================================================ +# mouse-offset-border-box (3 tests) +# The iOS 18 simulator WebView reports MouseEvent.offsetX/offsetY relative to +# the border box, while Playwright — like Chromium, Firefox and recent desktop +# WebKit — measures from the padding box (the long-standing WebKit "border box" +# behavior, https://bugs.webkit.org/show_bug.cgi?id=135399). The click lands at +# the correct coordinate; only the reported offset is larger by the element's +# border width, so the offsetX/offsetY assertions fail. Platform reporting +# difference, not an input-translation bug — drop once the simulator ships a +# WebKit that measures from the padding box. +# ============================================================================ +page/page-click.spec.ts › should click a button in scrolling container with offset [fail] +page/page-click.spec.ts › should click the button with em border with offset [fail] +page/page-click.spec.ts › should click the button with px border with offset [fail] + # ============================================================================ # frame-element-and-ownership (remaining) # elementHandle.contentFrame / elementHandle.ownerFrame / frame.frameElement @@ -89,11 +104,8 @@ page/frame-evaluate.spec.ts › should not allow cross-frame element handles whe page/frame-frame-element.spec.ts › should work inside closed shadow root [fail] page/frame-frame-element.spec.ts › should work inside declarative shadow root [fail] page/frame-goto.spec.ts › should return matching responses [fail] -page/page-click.spec.ts › should click button inside frameset [fail] -page/page-click.spec.ts › should click the button inside an iframe [fail] page/page-click.spec.ts › should not hang when frame is detached [fail] page/page-drag.spec.ts › Drag and drop › should work inside iframe [fail] -page/page-keyboard.spec.ts › should type emoji into an iframe [fail] page/page-wait-for-selector-1.spec.ts › page.waitForSelector is shortcut for main frame [fail] # ============================================================================ @@ -181,13 +193,10 @@ page/page-request-fulfill.spec.ts › should fulfill with gzip and readback [fai # shielding, so DOM mutations between checks can detach the handle. Needs the # same retry-with-fresh-handle pattern that wkPage uses. # ============================================================================ -page/elementhandle-click.spec.ts › should double click the button [fail] -page/elementhandle-click.spec.ts › should work with Node removed [fail] page/elementhandle-misc.spec.ts › should fill input [fail] page/elementhandle-misc.spec.ts › should hover [fail] page/elementhandle-misc.spec.ts › should hover when Node is removed [fail] page/elementhandle-scroll-into-view.spec.ts › should work @smoke [fail] -page/elementhandle-select-text.spec.ts › should wait for visible [fail] page/locator-misc-2.spec.ts › should scroll into view [fail] page/page-keyboard.spec.ts › should handle selectAll [fail] page/page-screenshot.spec.ts › page screenshot animations › should not capture infinite css animation [fail] @@ -231,23 +240,12 @@ page/page-autowaiting-basic.spec.ts › should work with waitForLoadState(load) page/page-basic.spec.ts › async stacks should work [fail] page/page-basic.spec.ts › has navigator.webdriver set to true [fail] page/page-basic.spec.ts › page.url should include hashes [fail] -page/page-click-react.spec.ts › should not retarget when element is recycled on hover [fail] page/page-click-scroll.spec.ts › should scroll into view span element [fail] -page/page-click-timeout-2.spec.ts › should timeout waiting for display:none to be gone [fail] -page/page-click-timeout-2.spec.ts › should timeout waiting for visibility:hidden to be gone [fail] -page/page-click-timeout-3.spec.ts › should report wrong hit target subtree [fail] -page/page-click-timeout-3.spec.ts › should timeout waiting for hit target [fail] -page/page-click-timeout-4.spec.ts › should timeout waiting for stable position [fail] page/page-click.spec.ts › ensure events are dispatched in the individual tasks [fail] -page/page-click.spec.ts › should click a button in scrolling container with offset [fail] -page/page-click.spec.ts › should click on checkbox input and toggle [fail] -page/page-click.spec.ts › should click the button with em border with offset [fail] -page/page-click.spec.ts › should click the button with px border with offset [fail] page/page-click.spec.ts › should fire contextmenu event on right click in correct order [fail] page/page-click.spec.ts › should set PointerEvent.pressure on pointerdown [fail] page/page-click.spec.ts › should set PointerEvent.pressure on pointermove [fail] page/page-dispatchevent.spec.ts › should dispatch absolute device orientation event [fail] -page/page-drag.spec.ts › Drag and drop › should be able to drag the mouse in a frame [fail] page/page-drag.spec.ts › Drag and drop › should cancel on escape [fail] page/page-drag.spec.ts › Drag and drop › should work @smoke [fail] page/page-drag.spec.ts › Drag and drop › should work if the drag event is captured but not canceled [fail] @@ -356,7 +354,6 @@ page/elementhandle-scroll-into-view.spec.ts › should wait for display:none to page/frame-evaluate.spec.ts › should work in iframes that interrupted initial javascript url navigation [fail] page/frame-goto.spec.ts › should reject when frame detaches [fail] page/page-aria-snapshot-ai.spec.ts › should snapshot a locator inside an iframe [fail] -page/page-click-react.spec.ts › should not retarget when element changes on hover [fail] page/page-click.spec.ts › should not wait with noAutoWaiting 2 [fail] page/page-emulate-media.spec.ts › should report hover and fine pointer for desktop [fail] page/page-evaluate.spec.ts › should await promise from popup [fail] @@ -427,7 +424,6 @@ page/page-request-fallback.spec.ts › should work [fail] page/page-route.spec.ts › should not support ? in glob pattern [fail] page/page-screenshot.spec.ts › page screenshot › should capture canvas changes [fail] page/page-screenshot.spec.ts › page screenshot › should work with odd clip size on Retina displays [fail] -page/page-select-option.spec.ts › should select only first option [fail] page/page-set-input-files.spec.ts › should upload large file [fail] page/page-set-input-files.spec.ts › should upload large file with relative path [fail] page/page-wait-for-load-state.spec.ts › should wait for load state of newPage [fail] @@ -506,7 +502,6 @@ page/page-autowaiting-no-hang.spec.ts › calling window.stop sync [fail] # Pre-existing (fails on main too): a same-document navigation via the Navigation # API does not update the frame URL on the stock WebView backend. page/page-autowaiting-no-hang.spec.ts › clicking a link intercepted by the Navigation API same-document [fail] -page/page-click.spec.ts › should wait for becoming hit target [fail] page/page-drag.spec.ts › Drag and drop › should respect the drop effect [fail] page/page-drag.spec.ts › Drag and drop › should work if a frame is stalled [fail] page/page-evaluate.spec.ts › should return -Infinity [fail] @@ -540,7 +535,6 @@ page/to-match-aria-snapshot.spec.ts › should not match what is not matched [fa page/frame-evaluate.spec.ts › should work in iframes that interrupted initial javascript url navigation [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] page/page-add-init-script.spec.ts › init script should run only once in iframe [fail] page/page-aria-snapshot-ai.spec.ts › return empty snapshot when iframe is not loaded [fail] page/page-aria-snapshot-ai.spec.ts › should limit depth across iframe boundary [fail] @@ -558,9 +552,6 @@ page/page-click.spec.ts › should click a button that is overlaid by a permissi page/page-click.spec.ts › should click in a nested transformed iframe [fail] page/page-click.spec.ts › should click in a transformed iframe [fail] page/page-click.spec.ts › should click in a transformed iframe with force [fail] -page/page-click.spec.ts › should click in an iframe with border [fail] -page/page-click.spec.ts › should click in an iframe with border 2 [fail] -page/page-click.spec.ts › should click the button inside an iframe [fail] page/page-click.spec.ts › should click the button with fixed position inside an iframe [fail] page/page-click.spec.ts › should issue clicks in parallel in page and popup [fail] page/page-drag.spec.ts › Drag and drop › iframe › should drag into an iframe [fail] @@ -672,7 +663,6 @@ page/network-post-data.spec.ts › should get post data for navigator.sendBeacon page/page-add-init-script.spec.ts › should work after a cross origin navigation [fail] page/page-autowaiting-basic.spec.ts › should await cross-process navigation when clicking anchor [fail] page/page-autowaiting-basic.spec.ts › should await form-get on click [fail] -page/page-basic.spec.ts › frame.press should work [fail] page/page-basic.spec.ts › page.close should work with window.close [fail] page/page-basic.spec.ts › should provide access to the opener page [fail] page/page-click-scroll.spec.ts › should scroll into view display:contents [fail] From dcc8d792a7b235fe4e5b12c526d61582a489ca79 Mon Sep 17 00:00:00 2001 From: Devin Rousso Date: Mon, 13 Jul 2026 23:24:55 -0600 Subject: [PATCH 2/2] fix(firefox): network instrumentation misses `WebSocket` handshakes in `Worker` (#41759) this is being fixed upstream temporarily working around it for now as only HAR uses the request/response info (e.g. headers, status, etc.) fixes --- .../src/server/chromium/crNetworkManager.ts | 11 ++++++++-- .../src/server/firefox/ffPage.ts | 20 ++++++++++++------- packages/playwright-core/src/server/frames.ts | 15 +++++++------- .../src/server/webkit/webview/wvPage.ts | 11 ++++++++-- .../src/server/webkit/wkPage.ts | 11 ++++++++-- tests/library/web-socket.spec.ts | 18 +++++++++++++++++ 6 files changed, 66 insertions(+), 20 deletions(-) diff --git a/packages/playwright-core/src/server/chromium/crNetworkManager.ts b/packages/playwright-core/src/server/chromium/crNetworkManager.ts index 4f57947001e00..1c720b5821004 100644 --- a/packages/playwright-core/src/server/chromium/crNetworkManager.ts +++ b/packages/playwright-core/src/server/chromium/crNetworkManager.ts @@ -78,7 +78,11 @@ export class CRNetworkManager { sessionInfo.eventListeners.push(...[ eventsHelper.addEventListener(session, 'Network.webSocketCreated', e => this._page!.frameManager.onWebSocketCreated(e.requestId, e.url)), eventsHelper.addEventListener(session, 'Network.webSocketWillSendHandshakeRequest', event => this._onWebSocketWillSendHandshakeRequest(event)), - eventsHelper.addEventListener(session, 'Network.webSocketHandshakeResponseReceived', e => this._page!.frameManager.onWebSocketResponse(e.requestId, e.response.status, e.response.statusText, headersObjectToArray(e.response.headers, '\n'))), + eventsHelper.addEventListener(session, 'Network.webSocketHandshakeResponseReceived', e => this._page!.frameManager.onWebSocketResponse(e.requestId, { + status: e.response.status, + statusText: e.response.statusText, + headers: headersObjectToArray(e.response.headers, '\n'), + })), eventsHelper.addEventListener(session, 'Network.webSocketFrameSent', e => e.response.payloadData && this._page!.frameManager.onWebSocketFrameSent(e.requestId, e.response.opcode, e.response.payloadData, this._timestampToWallTimeMsForWebSocket(e.requestId, e.timestamp))), eventsHelper.addEventListener(session, 'Network.webSocketFrameReceived', e => e.response.payloadData && this._page!.frameManager.webSocketFrameReceived(e.requestId, e.response.opcode, e.response.payloadData, this._timestampToWallTimeMsForWebSocket(e.requestId, e.timestamp))), eventsHelper.addEventListener(session, 'Network.webSocketClosed', event => this._onWebSocketClosed(event)), @@ -527,7 +531,10 @@ export class CRNetworkManager { _onWebSocketWillSendHandshakeRequest(event: Protocol.Network.webSocketWillSendHandshakeRequestPayload) { const wallTimeMs = event.wallTime * 1000; this._timestampBaselineForWebSocket.set(event.requestId, wallTimeMs - event.timestamp * 1000); - this._page!.frameManager.onWebSocketRequest(event.requestId, headersObjectToArray(event.request.headers, '\n'), wallTimeMs); + this._page!.frameManager.onWebSocketRequest(event.requestId, { + headers: headersObjectToArray(event.request.headers, '\n'), + wallTimeMs, + }); } _onWebSocketClosed(event: Protocol.Network.webSocketClosedPayload) { diff --git a/packages/playwright-core/src/server/firefox/ffPage.ts b/packages/playwright-core/src/server/firefox/ffPage.ts index ab5944d2c45c1..4eecb45561e4e 100644 --- a/packages/playwright-core/src/server/firefox/ffPage.ts +++ b/packages/playwright-core/src/server/firefox/ffPage.ts @@ -150,25 +150,31 @@ export class FFPage implements PageDelegate { url.protocol = url.protocol === 'https' ? 'wss' : 'ws'; this._page.frameManager.onWebSocketCreated(requestId, url.toString()); - this._page.frameManager.onWebSocketRequest(requestId, request.headers); - this._page.frameManager.onWebSocketResponse(requestId, response.status, response.statusText, response.headers); + this._page.frameManager.onWebSocketRequest(requestId, request); + this._page.frameManager.onWebSocketResponse(requestId, response); this._page.frameManager.webSocketClosed(requestId); return; } } _onWebSocketOpened(event: Protocol.Page.webSocketOpenedPayload) { + const socketId = webSocketId(event.frameId, event.wsid); const request = this._webSocketRequests.get(event.requestId); - assert(request); - const response = this._webSocketResponses.get(event.requestId); - assert(response); + // A `WebSocket` opened inside a worker is reported here, but its upgrade request is + // never seen by the network stack, so there is no handshake metadata to attach. + // TODO: Remove this workaround and make `requestData` required in `FrameManager.onWebSocketRequest` + // once Playwright's bundled Firefox includes https://phabricator.services.mozilla.com/D310690. + if (!request || !response) { + this._page.frameManager.onWebSocketRequest(socketId); + return; + } this._webSocketRequests.delete(event.requestId); this._webSocketResponses.delete(event.requestId); - this._page.frameManager.onWebSocketRequest(webSocketId(event.frameId, event.wsid), request.headers); - this._page.frameManager.onWebSocketResponse(webSocketId(event.frameId, event.wsid), response.status, response.statusText, response.headers); + this._page.frameManager.onWebSocketRequest(socketId, request); + this._page.frameManager.onWebSocketResponse(socketId, response); } _onWebSocketClosed(event: Protocol.Page.webSocketClosedPayload) { diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index 44de2e8249066..363f7a4edb801 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -433,29 +433,30 @@ export class FrameManager { this._webSockets.set(requestId, ws); } - onWebSocketRequest(requestId: string, headers: types.HeadersArray, wallTimeMs?: number) { + onWebSocketRequest(requestId: string, requestData?: { headers: types.HeadersArray, wallTimeMs?: number }) { const ws = this._webSockets.get(requestId); if (!ws) return; - ws.setWallTimeMs(wallTimeMs); + ws.setWallTimeMs(requestData?.wallTimeMs); if (ws.markAsNotified()) { this._page.emit(Page.Events.WebSocket, ws); this._page.browserContext.emit(BrowserContext.Events.WebSocket, ws, this._page); } - ws.requestSent(headers); + if (requestData) + ws.requestSent(requestData.headers); } - onWebSocketResponse(requestId: string, status: number, statusText: string, headers: types.HeadersArray) { + onWebSocketResponse(requestId: string, responseData: { status: number, statusText: string, headers: types.HeadersArray }) { const ws = this._webSockets.get(requestId); if (!ws) return; - ws.responseReceived(status, statusText, headers); - if (status >= 400) - ws.error(`${statusText}: ${status}`); + ws.responseReceived(responseData.status, responseData.statusText, responseData.headers); + if (responseData.status >= 400) + ws.error(`${responseData.statusText}: ${responseData.status}`); } onWebSocketFrameSent(requestId: string, opcode: number, data: string, wallTimeMs: number) { diff --git a/packages/playwright-core/src/server/webkit/webview/wvPage.ts b/packages/playwright-core/src/server/webkit/webview/wvPage.ts index e76d5597a562d..905c83294a39d 100644 --- a/packages/playwright-core/src/server/webkit/webview/wvPage.ts +++ b/packages/playwright-core/src/server/webkit/webview/wvPage.ts @@ -387,7 +387,11 @@ export class WVPage implements PageDelegate { eventsHelper.addEventListener(session, 'Network.loadingFailed', e => this._onLoadingFailed(session, e)), eventsHelper.addEventListener(session, 'Network.webSocketCreated', e => this._page.frameManager.onWebSocketCreated(e.requestId, e.url)), eventsHelper.addEventListener(session, 'Network.webSocketWillSendHandshakeRequest', event => this._onWebSocketWillSendHandshakeRequest(event)), - eventsHelper.addEventListener(session, 'Network.webSocketHandshakeResponseReceived', e => this._page.frameManager.onWebSocketResponse(e.requestId, e.response.status, e.response.statusText, headersObjectToArray(e.response.headers, ','))), + eventsHelper.addEventListener(session, 'Network.webSocketHandshakeResponseReceived', e => this._page.frameManager.onWebSocketResponse(e.requestId, { + status: e.response.status, + statusText: e.response.statusText, + headers: headersObjectToArray(e.response.headers, ','), + })), eventsHelper.addEventListener(session, 'Network.webSocketFrameSent', e => e.response.payloadData && this._page.frameManager.onWebSocketFrameSent(e.requestId, e.response.opcode, e.response.payloadData, this._timestampToWallTimeMsForWebSocket(e.requestId, e.timestamp))), eventsHelper.addEventListener(session, 'Network.webSocketFrameReceived', e => e.response.payloadData && this._page.frameManager.webSocketFrameReceived(e.requestId, e.response.opcode, e.response.payloadData, this._timestampToWallTimeMsForWebSocket(e.requestId, e.timestamp))), eventsHelper.addEventListener(session, 'Network.webSocketClosed', event => this._onWebSocketClosed(event)), @@ -1173,7 +1177,10 @@ export class WVPage implements PageDelegate { _onWebSocketWillSendHandshakeRequest(event: Protocol.Network.webSocketWillSendHandshakeRequestPayload) { const wallTimeMs = event.walltime * 1000; this._timestampBaselineForWebSocket.set(event.requestId, wallTimeMs - event.timestamp * 1000); - this._page.frameManager.onWebSocketRequest(event.requestId, headersObjectToArray(event.request.headers), wallTimeMs); + this._page.frameManager.onWebSocketRequest(event.requestId, { + headers: headersObjectToArray(event.request.headers), + wallTimeMs, + }); } _onWebSocketClosed(event: Protocol.Network.webSocketClosedPayload) { diff --git a/packages/playwright-core/src/server/webkit/wkPage.ts b/packages/playwright-core/src/server/webkit/wkPage.ts index ff7136c3b9c8d..f9c9ed6b907a4 100644 --- a/packages/playwright-core/src/server/webkit/wkPage.ts +++ b/packages/playwright-core/src/server/webkit/wkPage.ts @@ -398,7 +398,11 @@ export class WKPage implements PageDelegate { eventsHelper.addEventListener(this._session, 'Network.loadingFailed', e => this._onLoadingFailed(this._session, e)), eventsHelper.addEventListener(this._session, 'Network.webSocketCreated', e => this._page.frameManager.onWebSocketCreated(e.requestId, e.url)), eventsHelper.addEventListener(this._session, 'Network.webSocketWillSendHandshakeRequest', event => this._onWebSocketWillSendHandshakeRequest(event)), - eventsHelper.addEventListener(this._session, 'Network.webSocketHandshakeResponseReceived', e => this._page.frameManager.onWebSocketResponse(e.requestId, e.response.status, e.response.statusText, headersObjectToArray(e.response.headers, ',', wkSetCookieSeparator))), + eventsHelper.addEventListener(this._session, 'Network.webSocketHandshakeResponseReceived', e => this._page.frameManager.onWebSocketResponse(e.requestId, { + status: e.response.status, + statusText: e.response.statusText, + headers: headersObjectToArray(e.response.headers, ',', wkSetCookieSeparator), + })), eventsHelper.addEventListener(this._session, 'Network.webSocketFrameSent', e => e.response.payloadData && this._page.frameManager.onWebSocketFrameSent(e.requestId, e.response.opcode, e.response.payloadData, this._timestampToWallTimeMsForWebSocket(e.requestId, e.timestamp))), eventsHelper.addEventListener(this._session, 'Network.webSocketFrameReceived', e => e.response.payloadData && this._page.frameManager.webSocketFrameReceived(e.requestId, e.response.opcode, e.response.payloadData, this._timestampToWallTimeMsForWebSocket(e.requestId, e.timestamp))), eventsHelper.addEventListener(this._session, 'Network.webSocketClosed', event => this._onWebSocketClosed(event)), @@ -1218,7 +1222,10 @@ export class WKPage implements PageDelegate { _onWebSocketWillSendHandshakeRequest(event: Protocol.Network.webSocketWillSendHandshakeRequestPayload) { const wallTimeMs = event.walltime * 1000; this._timestampBaselineForWebSocket.set(event.requestId, wallTimeMs - event.timestamp * 1000); - this._page.frameManager.onWebSocketRequest(event.requestId, headersObjectToArray(event.request.headers), wallTimeMs); + this._page.frameManager.onWebSocketRequest(event.requestId, { + headers: headersObjectToArray(event.request.headers), + wallTimeMs, + }); } _onWebSocketClosed(event: Protocol.Network.webSocketClosedPayload) { diff --git a/tests/library/web-socket.spec.ts b/tests/library/web-socket.spec.ts index cacd0865e3ac2..7288c4fae4a37 100644 --- a/tests/library/web-socket.spec.ts +++ b/tests/library/web-socket.spec.ts @@ -197,6 +197,24 @@ it('should reject waitForEvent on page close', async ({ page, server }) => { expect((await error).message).toContain(kTargetClosedErrorMessage); }); +it('should not tear down the page when a WebSocket is opened inside a worker', { + annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/41742' }, +}, async ({ page, server }) => { + server.sendOnWebSocketConnection('incoming'); + await page.goto(server.EMPTY_PAGE); + const received = await page.evaluate(host => { + const code = ` + const ws = new WebSocket(${JSON.stringify('ws://' + host + '/ws')}); + ws.addEventListener('message', event => self.postMessage(event.data)); + `; + const worker = new Worker(URL.createObjectURL(new Blob([code], { type: 'text/javascript' }))); + return new Promise(resolve => worker.addEventListener('message', event => resolve(event.data))); + }, server.HOST); + expect(received).toBe('incoming'); + // Opening a `WebSocket` inside a worker must not tear down the page session. + expect(await page.evaluate(() => 1 + 1)).toBe(2); +}); + it('should turn off when offline', async ({ page }) => { it.fixme();