From 17c003c41405f38228435a43ec15fd7b5da9be41 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 28 May 2026 10:52:12 -0700 Subject: [PATCH 1/3] fix(reporter): print inline failure for non-retriable errors (#41026) --- packages/playwright/src/reporters/list.ts | 3 +- .../mock-binary-response-webkit-webview.png | Bin 0 -> 10009 bytes tests/playwright-test/reporter-list.spec.ts | 28 ++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 tests/page/page-request-fulfill.spec.ts-snapshots/mock-binary-response-webkit-webview.png diff --git a/packages/playwright/src/reporters/list.ts b/packages/playwright/src/reporters/list.ts index 1ded22453d657..58dff8deeca26 100644 --- a/packages/playwright/src/reporters/list.ts +++ b/packages/playwright/src/reporters/list.ts @@ -194,7 +194,8 @@ class ListReporter extends TerminalReporter { const wasPaused = this._paused.delete(result); if (!wasPaused) this._updateTestLine(test, result); - if (!wasPaused && this._printFailuresInline && !this.willRetry(test) && (test.outcome() === 'flaky' || test.outcome() === 'unexpected' || result.status === 'interrupted')) + const isFailure = result.status !== 'skipped' && result.status !== test.expectedStatus; + if (!wasPaused && this._printFailuresInline && isFailure) this._printFailure(test); } diff --git a/tests/page/page-request-fulfill.spec.ts-snapshots/mock-binary-response-webkit-webview.png b/tests/page/page-request-fulfill.spec.ts-snapshots/mock-binary-response-webkit-webview.png new file mode 100644 index 0000000000000000000000000000000000000000..62914006a65f4f05edea29f8381edba43b96e06d GIT binary patch literal 10009 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 { + // A missing snapshot fails the test but is not retried. The reporter must + // still print the failure inline, even though the retry budget is not exhausted. + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { + reporter: [['list', { printFailuresInline: true }]], + retries: 1, + workers: 1, + }; + `, + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('missing snapshot', async ({}) => { + expect('actual').toMatchSnapshot('foo.txt'); + }); + `, + }); + const text = result.output; + const failureHeader = '1) a.test.ts:3:15 › missing snapshot'; + const failureIndex = text.indexOf(failureHeader); + expect(failureIndex, 'failure should be printed inline').not.toBe(-1); + expect(text.indexOf(`A snapshot doesn't exist`, failureIndex)).toBeGreaterThan(failureIndex); + // It must not be retried, so there is exactly one failure block. + expect(text.indexOf(failureHeader, failureIndex + 1)).toBe(-1); + expect(result.exitCode).toBe(1); + }); + test('print stdio', async ({ runInlineTest }) => { const result = await runInlineTest({ 'a.test.ts': ` From f14741d73b3456c7ad7a40086568cfac4e34c94e Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 28 May 2026 16:29:06 -0700 Subject: [PATCH 2/3] feat(webkit): support connectOverCDP(transport) for direct WVPage attach (#41038) --- package-lock.json | 8 +- package.json | 2 +- packages/playwright-client/types/types.d.ts | 15 +-- .../playwright-core/src/client/browserType.ts | 8 +- .../src/server/webkit/DEPS.list | 1 + .../src/server/webkit/webkit.ts | 2 +- .../src/server/webkit/webview/wvBrowser.ts | 103 ++++++++++++++---- .../src/server/webkit/webview/wvPage.ts | 7 +- packages/playwright-core/types/types.d.ts | 15 +-- utils/generate_types/index.js | 2 +- utils/generate_types/overrides.d.ts | 5 +- 11 files changed, 118 insertions(+), 50 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3de49dcd74aac..d8c81d976a17a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64,7 +64,7 @@ "@types/react-dom": "^19.2.1", "@types/retry": "^0.12.5", "@types/source-map-support": "^0.5.4", - "@types/ws": "8.2.2", + "@types/ws": "8.18.1", "@types/xml2js": "^0.4.9", "@types/yazl": "^2.4.2", "@typescript-eslint/eslint-plugin": "^8.59.0", @@ -2779,9 +2779,9 @@ } }, "node_modules/@types/ws": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.2.2.tgz", - "integrity": "sha512-NOn5eIcgWLOo6qW8AcuLZ7G8PycXu0xTxxkS6Q18VWFxgPUSOwV0pBj2a/4viNZVu25i7RIB7GttdkAIUUXOOg==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index a1952db6050f4..91fa4051e624e 100644 --- a/package.json +++ b/package.json @@ -106,7 +106,7 @@ "@types/react-dom": "^19.2.1", "@types/retry": "^0.12.5", "@types/source-map-support": "^0.5.4", - "@types/ws": "8.2.2", + "@types/ws": "8.18.1", "@types/xml2js": "^0.4.9", "@types/yazl": "^2.4.2", "@typescript-eslint/eslint-plugin": "^8.59.0", diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index 185bb6dce242a..e39ebcb9cb855 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -15384,10 +15384,6 @@ export interface BrowserType { * @param options */ connectOverCDP(endpointURL: string, options?: ConnectOverCDPOptions): Promise; - /** - * Option `wsEndpoint` is deprecated. Instead use `endpointURL`. - * @deprecated - */ /** * This method attaches Playwright to an existing browser instance using the Chrome DevTools Protocol. * @@ -15413,7 +15409,11 @@ export interface BrowserType { * `ws://127.0.0.1:9222/devtools/browser/387adf4c-243f-4051-a181-46798f4a46f4`. * @param options */ - connectOverCDP(options: ConnectOverCDPOptions & { wsEndpoint?: string }): Promise; + connectOverCDP(transport: ConnectOverCDPTransport, options?: ConnectOverCDPOptions): Promise; + /** + * Option `wsEndpoint` is deprecated. Instead use `endpointURL`. + * @deprecated + */ /** * This method attaches Playwright to an existing browser instance using the Chrome DevTools Protocol. * @@ -15439,7 +15439,7 @@ export interface BrowserType { * `ws://127.0.0.1:9222/devtools/browser/387adf4c-243f-4051-a181-46798f4a46f4`. * @param options */ - connectOverCDP(transport: ConnectionTransport, options?: ConnectOverCDPOptions): Promise; + connectOverCDP(options: ConnectOverCDPOptions & { wsEndpoint?: string }): Promise; /** * This method attaches Playwright to an existing browser instance created via `BrowserType.launchServer` in Node.js. @@ -16221,7 +16221,8 @@ export interface BrowserType { name(): string; } -export interface ConnectionTransport { +export interface ConnectOverCDPTransport { + open?(): void; send(message: object): void; close(): void; onmessage?: (message: object) => void; diff --git a/packages/playwright-core/src/client/browserType.ts b/packages/playwright-core/src/client/browserType.ts index 036dd702a9b8d..c0af1bda9d946 100644 --- a/packages/playwright-core/src/client/browserType.ts +++ b/packages/playwright-core/src/client/browserType.ts @@ -138,10 +138,10 @@ export class BrowserType extends ChannelOwner imple async connectOverCDP(options: api.ConnectOverCDPOptions & { wsEndpoint?: string }): Promise; async connectOverCDP(endpointURL: string, options?: api.ConnectOverCDPOptions): Promise; - async connectOverCDP(transport: api.ConnectionTransport, options?: api.ConnectOverCDPOptions): Promise; - async connectOverCDP(overloaded: (api.ConnectOverCDPOptions & { wsEndpoint?: string }) | string | api.ConnectionTransport, options?: api.ConnectOverCDPOptions): Promise { + async connectOverCDP(transport: api.ConnectOverCDPTransport, options?: api.ConnectOverCDPOptions): Promise; + async connectOverCDP(overloaded: (api.ConnectOverCDPOptions & { wsEndpoint?: string }) | string | api.ConnectOverCDPTransport, options?: api.ConnectOverCDPOptions): Promise { let endpointURL: string | undefined; - let transport: api.ConnectionTransport | undefined; + let transport: api.ConnectOverCDPTransport | undefined; let params: api.ConnectOverCDPOptions; if (typeof overloaded === 'string') { endpointURL = overloaded; @@ -193,6 +193,6 @@ export class BrowserType extends ChannelOwner imple } } -function isConnectionTransport(value: any): value is api.ConnectionTransport { +function isConnectionTransport(value: any): value is api.ConnectOverCDPTransport { return !!value && typeof value === 'object' && typeof value.send === 'function' && typeof value.close === 'function'; } diff --git a/packages/playwright-core/src/server/webkit/DEPS.list b/packages/playwright-core/src/server/webkit/DEPS.list index 46643d0e0aa01..67e194822dae6 100644 --- a/packages/playwright-core/src/server/webkit/DEPS.list +++ b/packages/playwright-core/src/server/webkit/DEPS.list @@ -5,6 +5,7 @@ ../registry/ node_modules/jpeg-js node_modules/pngjs +node_modules/ws [webkit.ts] ./webview/wvBrowser.ts diff --git a/packages/playwright-core/src/server/webkit/webkit.ts b/packages/playwright-core/src/server/webkit/webkit.ts index 994bc25d2776f..da412abdc192b 100644 --- a/packages/playwright-core/src/server/webkit/webkit.ts +++ b/packages/playwright-core/src/server/webkit/webkit.ts @@ -42,7 +42,7 @@ export class WebKit extends BrowserType { } override async connectOverCDP(progress: Progress, params: channels.BrowserTypeConnectOverCDPParams): Promise { - return connectOverRDP(progress, this, params.endpointURL!, params); + return connectOverRDP(progress, this, params); } override amendEnvironment(env: NodeJS.ProcessEnv, userDataDir: string, isPersistent: boolean, options: types.LaunchOptions): NodeJS.ProcessEnv { diff --git a/packages/playwright-core/src/server/webkit/webview/wvBrowser.ts b/packages/playwright-core/src/server/webkit/webview/wvBrowser.ts index 3b646b1ae9ae9..3480dfa163c69 100644 --- a/packages/playwright-core/src/server/webkit/webview/wvBrowser.ts +++ b/packages/playwright-core/src/server/webkit/webview/wvBrowser.ts @@ -18,12 +18,14 @@ import os from 'os'; import path from 'path'; +import ws from 'ws'; import { debugLogger, RecentLogsCollector } from '@utils/debugLogger'; import { removeFolders } from '@utils/fileUtils'; +import { httpHappyEyeballsAgent, httpsHappyEyeballsAgent } from '@utils/happyEyeballs'; import { headersArrayToObject } from '@isomorphic/headers'; import { Browser } from '../../browser'; import { helper } from '../../helper'; -import { WebSocketTransport } from '../../transport'; +import { perMessageDeflate } from '../../transport'; import { getUserAgent } from '../../userAgent'; import { BrowserContext } from '../../browserContext'; import { DialogBridge } from './dialogBridge'; @@ -33,9 +35,11 @@ import { WVPage } from './wvPage'; import type { BrowserOptions, BrowserProcess } from '../../browser'; import type { SdkObject } from '../../instrumentation'; import type { InitScript, Page } from '../../page'; +import type { ProtocolRequest, ProtocolResponse } from '../../transport'; import type * as types from '../../types'; import type * as channels from '@protocol/channels'; import type { Progress } from '../../progress'; +import type { ConnectOverCDPTransport } from '../../../../types/types.d.ts'; type ProxyTab = { url: string; @@ -64,26 +68,82 @@ async function listTabs(proxyBase: string, headers: { [key: string]: string }): return data.filter(t => !!t.webSocketDebuggerUrl); } -export async function connectOverRDP(progress: Progress, parent: SdkObject, endpointURL: string, options: { slowMo?: number, headers?: types.HeadersArray, isLocal?: boolean, noDefaults?: boolean, artifactsDir?: string }): Promise { +// Local WebSocket-backed transport: defers opening the socket until `open()` +// is called, so listeners on `onmessage` are wired before the remote side +// starts emitting events. +class DeferredWebSocketTransport implements ConnectOverCDPTransport { + private readonly _url: string; + private readonly _headers: { [key: string]: string }; + private _ws: ws | undefined; + private _closed = false; + + onmessage?: (message: object) => void; + onclose?: (reason?: string) => void; + + constructor(url: string, headers: { [key: string]: string }) { + this._url = url; + this._headers = headers; + } + + open(): void { + if (this._closed) + return; + const url = this._url; + this._ws = new ws(url, [], { + maxPayload: 256 * 1024 * 1024, + headers: this._headers, + followRedirects: true, + agent: (/^(https|wss):\/\//.test(url)) ? httpsHappyEyeballsAgent : httpHappyEyeballsAgent, + perMessageDeflate, + allowSynchronousEvents: false, + }); + this._ws.addEventListener('message', event => { + const eventData = event.data as string; + let parsedJson: ProtocolResponse; + try { + parsedJson = JSON.parse(eventData); + this.onmessage?.(parsedJson); + } catch { + this._ws?.close(); + } + }); + this._ws.addEventListener('close', event => { + this.onclose?.(event.reason); + }); + this._ws.addEventListener('error', () => {}); + } + + send(message: object): void { + this._ws?.send(JSON.stringify(message as ProtocolRequest)); + } + + close(): void { + this._closed = true; + this._ws?.close(); + } +} + +export async function connectOverRDP(progress: Progress, parent: SdkObject, params: channels.BrowserTypeConnectOverCDPParams): Promise { let headersMap: { [key: string]: string; } | undefined; - if (options.headers) - headersMap = headersArrayToObject(options.headers, false); + if (params.headers) + headersMap = headersArrayToObject(params.headers, false); if (!headersMap) headersMap = { 'User-Agent': getUserAgent() }; else if (!Object.keys(headersMap).some(key => key.toLowerCase() === 'user-agent')) headersMap['User-Agent'] = getUserAgent(); - const proxyBase = deriveProxyBase(endpointURL); + const transport = params.transport as ConnectOverCDPTransport | undefined; + const proxyBase = transport ? '' : deriveProxyBase(params.endpointURL!); - const artifactsDir = options.artifactsDir ?? path.join(os.tmpdir(), 'playwright-artifacts-'); + const artifactsDir = params.artifactsDir ?? path.join(os.tmpdir(), 'playwright-artifacts-'); const doCleanup = async () => { await removeFolders([artifactsDir]); }; const browser = await progress.race((async () => { const dialogBridge = await DialogBridge.start(); - const created = new WVBrowser(parent, proxyBase, headersMap!, dialogBridge, { - slowMo: options.slowMo, + const created = new WVBrowser(parent, proxyBase, headersMap!, dialogBridge, transport, { + slowMo: params.slowMo, name: 'webkit', browserType: 'webkit', browserProcess: { close: async () => {}, kill: async () => {} } as BrowserProcess, @@ -104,7 +164,7 @@ export async function connectOverRDP(progress: Progress, parent: SdkObject, endp return created; })()); - if (!options.isLocal) + if (!params.isLocal) browser._isCollocatedWithServer = false; browser.on(Browser.Events.Disconnected, doCleanup); return browser; @@ -112,7 +172,7 @@ export async function connectOverRDP(progress: Progress, parent: SdkObject, endp type TabEntry = { pageId: string; - transport: WebSocketTransport; + transport: ConnectOverCDPTransport; connection: WVConnection; page: WVPage; }; @@ -122,24 +182,30 @@ export class WVBrowser extends Browser { readonly _proxyBase: string; readonly _headers: { [key: string]: string }; readonly _dialogBridge: DialogBridge; + readonly _directPageTransport: ConnectOverCDPTransport | undefined; readonly _tabs = new Map(); private _didCloseFired = false; // Backwards compat — old code still reads `_page` for the "primary" tab. _page!: WVPage; - constructor(parent: SdkObject, proxyBase: string, headers: { [key: string]: string }, dialogBridge: DialogBridge, options: BrowserOptions) { + constructor(parent: SdkObject, proxyBase: string, headers: { [key: string]: string }, dialogBridge: DialogBridge, directPageTransport: ConnectOverCDPTransport | undefined, options: BrowserOptions) { super(parent, options); this._proxyBase = proxyBase; this._headers = headers; this._dialogBridge = dialogBridge; + this._directPageTransport = directPageTransport; this._context = new WVBrowserContext(this); } async _initialize(): Promise { await this._context.initialize(); - await this._syncTabs(); - if (!this._tabs.size) - throw new Error(`No Mobile Safari tabs found at ${this._proxyBase}/json — open Safari first.`); + if (this._directPageTransport) { + await this._attachTab('rdp-transport', this._directPageTransport); + } else { + await this._syncTabs(); + if (!this._tabs.size) + throw new Error(`No Mobile Safari tabs found at ${this._proxyBase}/json — open Safari first.`); + } this._page = this._firstTab().page; } @@ -156,7 +222,7 @@ export class WVBrowser extends Browser { if (this._tabs.has(pageId)) continue; try { - await this._attachTab(pageId, tab); + await this._attachTab(pageId, new DeferredWebSocketTransport(tab.webSocketDebuggerUrl, this._headers)); } catch (e) { debugLogger.log('error', `webview: failed to attach to tab ${pageId}: ${(e as Error).message}`); } @@ -168,15 +234,14 @@ export class WVBrowser extends Browser { } } - private async _attachTab(pageId: string, tab: ProxyTab): Promise { - const transport = await WebSocketTransport.connect(undefined, tab.webSocketDebuggerUrl, { headers: this._headers, followRedirects: true }); + private async _attachTab(pageId: string, transport: ConnectOverCDPTransport): Promise { const connection = new WVConnection(transport, () => this._detachTab(pageId), this.options.protocolLogger, this.options.browserLogsCollector); - // TODO: handle this as RDP connection parameter. - connection.outerSession.sendMayFail('Target.setPauseOnStart', { pauseOnStart: true }); const dialogEndpoint = this._dialogBridge.endpointFor(pageId); const page = new WVPage(this._context, connection.outerSession, dialogEndpoint); this._dialogBridge.registerTab(pageId, req => page.onBridgeDialog(req)); this._tabs.set(pageId, { pageId, transport, connection, page }); + transport.open?.(); + connection.outerSession.sendMayFail('Target.setPauseOnStart', { pauseOnStart: true }); await page.waitForInitialized(); } diff --git a/packages/playwright-core/src/server/webkit/webview/wvPage.ts b/packages/playwright-core/src/server/webkit/webview/wvPage.ts index 3375aa90673f7..88d1ace33c3cb 100644 --- a/packages/playwright-core/src/server/webkit/webview/wvPage.ts +++ b/packages/playwright-core/src/server/webkit/webview/wvPage.ts @@ -19,6 +19,7 @@ import { PNG } from 'pngjs'; import jpegjs from 'jpeg-js'; import { assert } from '@isomorphic/assert'; import { headersArrayToObject } from '@isomorphic/headers'; +import { ManualPromise } from '@isomorphic/manualPromise'; import { splitErrorMessage } from '@isomorphic/stackTrace'; import { debugLogger } from '@utils/debugLogger'; import { eventsHelper } from '@utils/eventsHelper'; @@ -65,8 +66,7 @@ export class WVPage implements PageDelegate { private _firstNonInitialNavigationCommittedPromise: Promise; private _firstNonInitialNavigationCommittedFulfill = () => {}; _firstNonInitialNavigationCommittedReject = (e: Error) => {}; - private _initializedPromise: Promise; - private _initializedFulfill = () => {}; + private _initializedPromise = new ManualPromise(); private _lastConsoleMessage: { derivedType: string, text: string, handles: JSHandle[]; count: number, location: types.ConsoleMessageLocation; } | null = null; private readonly _requestIdToResponseReceivedPayloadEvent = new Map(); @@ -95,7 +95,6 @@ export class WVPage implements PageDelegate { }); // Avoid unhandled rejection on disconnect in the middle of initialization. this._firstNonInitialNavigationCommittedPromise.catch(() => {}); - this._initializedPromise = new Promise(f => { this._initializedFulfill = f; }); } waitForInitialized(): Promise { @@ -225,7 +224,7 @@ export class WVPage implements PageDelegate { if (targetInfo.isPaused) this._outerSession.sendMayFail('Target.resume', { targetId: targetInfo.targetId }); await this._page.reportAsNew(undefined, pageOrError instanceof Page ? undefined : pageOrError); - this._initializedFulfill(); + this._initializedPromise.resolve(); } else { assert(!this._provisionalPage); this._provisionalPage = new WVProvisionalPage(session, this); diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 185bb6dce242a..e39ebcb9cb855 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -15384,10 +15384,6 @@ export interface BrowserType { * @param options */ connectOverCDP(endpointURL: string, options?: ConnectOverCDPOptions): Promise; - /** - * Option `wsEndpoint` is deprecated. Instead use `endpointURL`. - * @deprecated - */ /** * This method attaches Playwright to an existing browser instance using the Chrome DevTools Protocol. * @@ -15413,7 +15409,11 @@ export interface BrowserType { * `ws://127.0.0.1:9222/devtools/browser/387adf4c-243f-4051-a181-46798f4a46f4`. * @param options */ - connectOverCDP(options: ConnectOverCDPOptions & { wsEndpoint?: string }): Promise; + connectOverCDP(transport: ConnectOverCDPTransport, options?: ConnectOverCDPOptions): Promise; + /** + * Option `wsEndpoint` is deprecated. Instead use `endpointURL`. + * @deprecated + */ /** * This method attaches Playwright to an existing browser instance using the Chrome DevTools Protocol. * @@ -15439,7 +15439,7 @@ export interface BrowserType { * `ws://127.0.0.1:9222/devtools/browser/387adf4c-243f-4051-a181-46798f4a46f4`. * @param options */ - connectOverCDP(transport: ConnectionTransport, options?: ConnectOverCDPOptions): Promise; + connectOverCDP(options: ConnectOverCDPOptions & { wsEndpoint?: string }): Promise; /** * This method attaches Playwright to an existing browser instance created via `BrowserType.launchServer` in Node.js. @@ -16221,7 +16221,8 @@ export interface BrowserType { name(): string; } -export interface ConnectionTransport { +export interface ConnectOverCDPTransport { + open?(): void; send(message: object): void; close(): void; onmessage?: (message: object) => void; diff --git a/utils/generate_types/index.js b/utils/generate_types/index.js index 09ca058974f55..dc811837eb808 100644 --- a/utils/generate_types/index.js +++ b/utils/generate_types/index.js @@ -522,7 +522,7 @@ class TypesGenerator { ...assertionClasses, ]), ignoreMissing: new Set([ - 'ConnectionTransport', + 'ConnectOverCDPTransport', ]), }); let types = await generator.generateTypes(path.join(__dirname, 'overrides.d.ts')); diff --git a/utils/generate_types/overrides.d.ts b/utils/generate_types/overrides.d.ts index ea7d6a03d5711..89ef7bd94ff9a 100644 --- a/utils/generate_types/overrides.d.ts +++ b/utils/generate_types/overrides.d.ts @@ -211,12 +211,12 @@ export interface Locator { export interface BrowserType { connectOverCDP(endpointURL: string, options?: ConnectOverCDPOptions): Promise; + connectOverCDP(transport: ConnectOverCDPTransport, options?: ConnectOverCDPOptions): Promise; /** * Option `wsEndpoint` is deprecated. Instead use `endpointURL`. * @deprecated */ connectOverCDP(options: ConnectOverCDPOptions & { wsEndpoint?: string }): Promise; - connectOverCDP(transport: ConnectionTransport, options?: ConnectOverCDPOptions): Promise; connect(wsEndpoint: string, options?: ConnectOptions): Promise; /** @@ -228,7 +228,8 @@ export interface BrowserType { connect(options: ConnectOptions & { wsEndpoint?: string }): Promise; } -export interface ConnectionTransport { +export interface ConnectOverCDPTransport { + open?(): void; send(message: object): void; close(): void; onmessage?: (message: object) => void; From 37a5aa6731a96ce9badd1c8c9c30dbc44d252929 Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Thu, 28 May 2026 17:32:25 -0700 Subject: [PATCH 3/3] fix(tools): pass time parameter to page.evaluate in waitForTimeout (#41037) Signed-off-by: Sebastien Tardif --- packages/playwright-core/src/tools/backend/tab.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playwright-core/src/tools/backend/tab.ts b/packages/playwright-core/src/tools/backend/tab.ts index 65e85c1ae1004..bb819501fe89a 100644 --- a/packages/playwright-core/src/tools/backend/tab.ts +++ b/packages/playwright-core/src/tools/backend/tab.ts @@ -483,7 +483,7 @@ export class Tab extends EventEmitter { return; } - await this.page.evaluate(() => new Promise(f => setTimeout(f, 1000))).catch(() => {}); + await this.page.evaluate(ms => new Promise(f => setTimeout(f, ms)), time).catch(() => {}); } }