From dcbe13111acfd29140103984e85376ba9ee36184 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Mon, 8 Jun 2026 08:50:27 +0200 Subject: [PATCH 1/4] feat(screencast): expose frame presentation timestamp on onFrame (#41162) --- docs/src/api/class-screencast.md | 5 +++-- packages/playwright-client/types/types.d.ts | 6 +++--- packages/playwright-core/src/client/screencast.ts | 8 ++++---- packages/playwright-core/src/protocol/validator.ts | 1 + .../src/server/dispatchers/pageDispatcher.ts | 2 +- packages/playwright-core/types/types.d.ts | 6 +++--- packages/protocol/spec/page.yml | 1 + packages/protocol/src/channels.d.ts | 1 + tests/library/screencast.spec.ts | 5 +++-- utils/generate_types/overrides.d.ts | 2 +- 10 files changed, 21 insertions(+), 16 deletions(-) diff --git a/docs/src/api/class-screencast.md b/docs/src/api/class-screencast.md index 12e3111ffe805..e304733239a52 100644 --- a/docs/src/api/class-screencast.md +++ b/docs/src/api/class-screencast.md @@ -22,8 +22,8 @@ await page.screencast.stop(); ```js // Capture frames await page.screencast.start({ - onFrame: ({ data, viewportWidth, viewportHeight }) => { - console.log(`frame size: ${data.length} (${viewportWidth}x${viewportHeight})`); + onFrame: ({ data, timestamp, viewportWidth, viewportHeight }) => { + console.log(`frame size: ${data.length} (${viewportWidth}x${viewportHeight}) at ${timestamp}`); }, size: { width: 800, height: 600 }, }); @@ -36,6 +36,7 @@ await page.screencast.stop(); - `onFrame` <[function]\([Object]\): [Promise]> * alias: ScreencastFrame - `data` <[Buffer]> JPEG-encoded frame data. + - `timestamp` <[float]> The timestamp of when the frame was presented by the browser, in milliseconds since the Unix epoch. - `viewportWidth` <[int]> Width of the page viewport at the time the frame was captured. - `viewportHeight` <[int]> Height of the page viewport at the time the frame was captured. diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index d9902ca08932a..c80d5655a3788 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -16706,8 +16706,8 @@ export interface Screencast { * ```js * // Capture frames * await page.screencast.start({ - * onFrame: ({ data, viewportWidth, viewportHeight }) => { - * console.log(`frame size: ${data.length} (${viewportWidth}x${viewportHeight})`); + * onFrame: ({ data, timestamp, viewportWidth, viewportHeight }) => { + * console.log(`frame size: ${data.length} (${viewportWidth}x${viewportHeight}) at ${timestamp}`); * }, * size: { width: 800, height: 600 }, * }); @@ -16718,7 +16718,7 @@ export interface Screencast { * @param options */ start(options?: { - onFrame?: (frame: { data: Buffer, viewportWidth: number, viewportHeight: number }) => Promise|any; + onFrame?: (frame: { data: Buffer, timestamp: number, viewportWidth: number, viewportHeight: number }) => Promise|any; path?: string; size?: { width: number; diff --git a/packages/playwright-core/src/client/screencast.ts b/packages/playwright-core/src/client/screencast.ts index c9cdf535bf90c..de6f463311586 100644 --- a/packages/playwright-core/src/client/screencast.ts +++ b/packages/playwright-core/src/client/screencast.ts @@ -24,17 +24,17 @@ export class Screencast implements api.Screencast { private _page: Page; private _started = false; private _savePath: string | undefined; - private _onFrame: ((frame: { data: Buffer, viewportWidth: number, viewportHeight: number }) => Promise) | null = null; + private _onFrame: ((frame: { data: Buffer, timestamp: number, viewportWidth: number, viewportHeight: number }) => Promise) | null = null; private _artifact: Artifact | undefined; constructor(page: Page) { this._page = page; - this._page._channel.on('screencastFrame', ({ data, viewportWidth, viewportHeight }) => { - void this._onFrame?.({ data, viewportWidth, viewportHeight }); + this._page._channel.on('screencastFrame', ({ data, timestamp, viewportWidth, viewportHeight }) => { + void this._onFrame?.({ data, timestamp, viewportWidth, viewportHeight }); }); } - async start(options: { onFrame?: (frame: { data: Buffer, viewportWidth: number, viewportHeight: number }) => Promise|any, path?: string, size?: { width: number, height: number }, quality?: number } = {}): Promise { + async start(options: { onFrame?: (frame: { data: Buffer, timestamp: number, viewportWidth: number, viewportHeight: number }) => Promise|any, path?: string, size?: { width: number, height: number }, quality?: number } = {}): Promise { if (this._started) throw new Error('Screencast is already started'); this._started = true; diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index bbdc20aebf172..75600eabe1d85 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -2299,6 +2299,7 @@ scheme.PageRouteEvent = tObject({ }); scheme.PageScreencastFrameEvent = tObject({ data: tBinary, + timestamp: tFloat, viewportWidth: tInt, viewportHeight: tInt, }); diff --git a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts index 45eee1bbba2b1..7fc4d2231e18c 100644 --- a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts @@ -398,7 +398,7 @@ export class PageDispatcher extends Dispatcher { - this._dispatchEvent('screencastFrame', { data: frame.buffer, viewportWidth: frame.viewportWidth, viewportHeight: frame.viewportHeight }); + this._dispatchEvent('screencastFrame', { data: frame.buffer, timestamp: frame.frameSwapWallTime, viewportWidth: frame.viewportWidth, viewportHeight: frame.viewportHeight }); }, dispose: () => {}, size: params.size, diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index d9902ca08932a..c80d5655a3788 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -16706,8 +16706,8 @@ export interface Screencast { * ```js * // Capture frames * await page.screencast.start({ - * onFrame: ({ data, viewportWidth, viewportHeight }) => { - * console.log(`frame size: ${data.length} (${viewportWidth}x${viewportHeight})`); + * onFrame: ({ data, timestamp, viewportWidth, viewportHeight }) => { + * console.log(`frame size: ${data.length} (${viewportWidth}x${viewportHeight}) at ${timestamp}`); * }, * size: { width: 800, height: 600 }, * }); @@ -16718,7 +16718,7 @@ export interface Screencast { * @param options */ start(options?: { - onFrame?: (frame: { data: Buffer, viewportWidth: number, viewportHeight: number }) => Promise|any; + onFrame?: (frame: { data: Buffer, timestamp: number, viewportWidth: number, viewportHeight: number }) => Promise|any; path?: string; size?: { width: number; diff --git a/packages/protocol/spec/page.yml b/packages/protocol/spec/page.yml index 73a3adc21810b..900dc11b5763e 100644 --- a/packages/protocol/spec/page.yml +++ b/packages/protocol/spec/page.yml @@ -718,6 +718,7 @@ Page: screencastFrame: parameters: data: binary + timestamp: float viewportWidth: int viewportHeight: int diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index 09fb8655f9285..c97f2e689d227 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -4140,6 +4140,7 @@ export type PageRouteEvent = { }; export type PageScreencastFrameEvent = { data: Binary, + timestamp: number, viewportWidth: number, viewportHeight: number, }; diff --git a/tests/library/screencast.spec.ts b/tests/library/screencast.spec.ts index 867766fb2d163..468880b61e650 100644 --- a/tests/library/screencast.spec.ts +++ b/tests/library/screencast.spec.ts @@ -57,9 +57,9 @@ test('onFrame receives viewport size', async ({ browser, server, trace }) => { const context = await browser.newContext({ viewport: { width: 1000, height: 400 } }); const page = await context.newPage(); - const frames: { viewportWidth: number, viewportHeight: number }[] = []; + const frames: { timestamp: number, viewportWidth: number, viewportHeight: number }[] = []; await page.screencast.start({ - onFrame: ({ viewportWidth, viewportHeight }) => frames.push({ viewportWidth, viewportHeight }), + onFrame: ({ timestamp, viewportWidth, viewportHeight }) => frames.push({ timestamp, viewportWidth, viewportHeight }), size: { width: 500, height: 400 }, }); await page.goto(server.EMPTY_PAGE); @@ -70,6 +70,7 @@ test('onFrame receives viewport size', async ({ browser, server, trace }) => { for (const frame of frames) { expect(frame.viewportWidth).toBe(1000); expect(frame.viewportHeight).toBe(400); + expect(typeof frame.timestamp).toBe('number'); } await context.close(); diff --git a/utils/generate_types/overrides.d.ts b/utils/generate_types/overrides.d.ts index 89ef7bd94ff9a..4bdde3cd94ddb 100644 --- a/utils/generate_types/overrides.d.ts +++ b/utils/generate_types/overrides.d.ts @@ -255,7 +255,7 @@ export interface WebSocketRoute { export interface Screencast { start(options?: { - onFrame?: (frame: { data: Buffer, viewportWidth: number, viewportHeight: number }) => Promise|any; + onFrame?: (frame: { data: Buffer, timestamp: number, viewportWidth: number, viewportHeight: number }) => Promise|any; path?: string; size?: { width: number; From 021497acb4829fb6ad8d03ed7a6103bcae0b2a36 Mon Sep 17 00:00:00 2001 From: Andrey Lushnikov Date: Mon, 8 Jun 2026 03:58:02 -0400 Subject: [PATCH 2/4] test: enabled regression test (#41165) --- tests/library/browsercontext-basic.spec.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/library/browsercontext-basic.spec.ts b/tests/library/browsercontext-basic.spec.ts index adc8fb6736cff..3d9aec28c8845 100644 --- a/tests/library/browsercontext-basic.spec.ts +++ b/tests/library/browsercontext-basic.spec.ts @@ -69,8 +69,7 @@ it('should be able to click across browser contexts', async function({ browser } await page2.close(); }); -it('should be able to hover across browser contexts in parallel', async function({ browser, browserName, isBidi }) { - it.fixme(browserName === 'firefox' && !isBidi); +it('should be able to hover across browser contexts in parallel', async function({ browser }) { it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/40562' }); const html = ` From 1843521d19a118365c0780ae64d9a35c98835201 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Mon, 8 Jun 2026 09:59:28 +0200 Subject: [PATCH 3/4] chore(python): support screencast size option (#41183) --- docs/src/api/class-screencast.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/src/api/class-screencast.md b/docs/src/api/class-screencast.md index e304733239a52..ba9817177514e 100644 --- a/docs/src/api/class-screencast.md +++ b/docs/src/api/class-screencast.md @@ -56,7 +56,6 @@ The quality of the image, between 0-100. ### option: Screencast.start.size * since: v1.59 -* langs: js - `size` ?<[Object]> * alias-csharp: ScreencastSize - `width` <[int]> Max frame width in pixels. From 910b224b5af9109c4591eb44a5195880c5ccbfd6 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Mon, 8 Jun 2026 11:30:07 +0100 Subject: [PATCH 4/4] fix(network): use internal methods in HarTracer (#41186) --- .../src/server/har/harTracer.ts | 12 ++++----- .../playwright-core/src/server/network.ts | 26 ++++++++++++------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/packages/playwright-core/src/server/har/harTracer.ts b/packages/playwright-core/src/server/har/harTracer.ts index 9490437fdaa82..4c57af8323713 100644 --- a/packages/playwright-core/src/server/har/harTracer.ts +++ b/packages/playwright-core/src/server/har/harTracer.ts @@ -321,7 +321,7 @@ export class HarTracer { // In WebKit security details and server ip are reported in Network.loadingFinished, so we populate // it here to not hang in case of long chunked responses, see https://github.com/microsoft/playwright/issues/21182. if (!this._options.omitServerIP) { - this._addBarrier(page || request.serviceWorker(), response.serverAddr(nullProgress).then(server => { + this._addBarrier(page || request.serviceWorker(), response.internalServerAddr().then(server => { if (server?.ipAddress) harEntry.serverIPAddress = server.ipAddress; if (server?.port) @@ -329,7 +329,7 @@ export class HarTracer { })); } if (!this._options.omitSecurityDetails) { - this._addBarrier(page || request.serviceWorker(), response.securityDetails(nullProgress).then(details => { + this._addBarrier(page || request.serviceWorker(), response.internalSecurityDetails().then(details => { if (details) harEntry._securityDetails = details; })); @@ -374,7 +374,7 @@ export class HarTracer { }); this._addBarrier(page || request.serviceWorker(), promise); - this._addBarrier(page || request.serviceWorker(), response.httpVersion(nullProgress).then(httpVersion => { + this._addBarrier(page || request.serviceWorker(), response.internalHttpVersion().then(httpVersion => { harEntry.request.httpVersion = httpVersion; harEntry.response.httpVersion = httpVersion; })); @@ -385,7 +385,7 @@ export class HarTracer { this._computeHarEntryTotalTime(harEntry); if (!this._options.omitSizes) { - this._addBarrier(page || request.serviceWorker(), response.sizes(nullProgress).then(sizes => { + this._addBarrier(page || request.serviceWorker(), response.internalSizes().then(sizes => { harEntry.response.bodySize = sizes.responseBodySize; harEntry.response.headersSize = sizes.responseHeadersSize; harEntry.response._transferSize = sizes.transferSize; @@ -595,13 +595,13 @@ export class HarTracer { } this._recordRequestOverrides(harEntry, request); - this._addBarrier(page || request.serviceWorker(), request.rawRequestHeaders(nullProgress).then(headers => { + this._addBarrier(page || request.serviceWorker(), request.internalRawRequestHeaders().then(headers => { this._recordRequestHeadersAndCookies(harEntry, headers); })); // Record available headers including redirect location in case the tracing is stopped before // response extra info is received (in Chromium). this._recordResponseHeaders(harEntry, response.headers()); - this._addBarrier(page || request.serviceWorker(), response.rawResponseHeaders(nullProgress).then(headers => { + this._addBarrier(page || request.serviceWorker(), response.internalRawResponseHeaders().then(headers => { this._recordResponseHeaders(harEntry, headers); })); } diff --git a/packages/playwright-core/src/server/network.ts b/packages/playwright-core/src/server/network.ts index e0ea2fee3e0f4..db00d5a7039a8 100644 --- a/packages/playwright-core/src/server/network.ts +++ b/packages/playwright-core/src/server/network.ts @@ -228,7 +228,7 @@ export class Request extends SdkObject { } async rawRequestHeaders(progress: Progress): Promise { - return await this.raceWithPageClosure(progress, this._rawRequestHeaders()); + return await this.raceWithPageClosure(progress, this.internalRawRequestHeaders()); } async response(progress: Progress): Promise { @@ -280,7 +280,7 @@ export class Request extends SdkObject { this._rawRequestHeadersPromise.resolve(headers || this._headers); } - private async _rawRequestHeaders(): Promise { + async internalRawRequestHeaders(): Promise { return this._overrides?.headers || this._rawRequestHeadersPromise; } @@ -336,7 +336,7 @@ export class Request extends SdkObject { } async _requestHeadersSize(): Promise { - return requestHeadersSize(await this._rawRequestHeaders(), this.url(), this.method()); + return requestHeadersSize(await this.internalRawRequestHeaders(), this.url(), this.method()); } } @@ -553,19 +553,19 @@ export class Response extends SdkObject { } async serverAddr(progress: Progress): Promise { - return (await this._request.raceWithPageClosure(progress, this._serverAddrPromise)) || null; + return await this._request.raceWithPageClosure(progress, this.internalServerAddr()); } async rawResponseHeaders(progress: Progress): Promise { - return await this._request.raceWithPageClosure(progress, this._rawResponseHeadersPromise); + return await this._request.raceWithPageClosure(progress, this.internalRawResponseHeaders()); } async httpVersion(progress: Progress): Promise { - return await this._request.raceWithPageClosure(progress, this._httpVersion()); + return await this._request.raceWithPageClosure(progress, this.internalHttpVersion()); } async sizes(progress: Progress): Promise { - return await this._request.raceWithPageClosure(progress, this._sizes()); + return await this._request.raceWithPageClosure(progress, this.internalSizes()); } _serverAddrFinished(addr?: RemoteAddr) { @@ -634,6 +634,14 @@ export class Response extends SdkObject { return await this._securityDetailsPromise || null; } + async internalServerAddr(): Promise { + return await this._serverAddrPromise || null; + } + + async internalRawResponseHeaders(): Promise { + return await this._rawResponseHeadersPromise; + } + internalBody(): Promise { if (!this._contentPromise) { this._contentPromise = this._finishedPromise.then(async () => { @@ -661,7 +669,7 @@ export class Response extends SdkObject { return this._request.frame(); } - private async _httpVersion(): Promise { + async internalHttpVersion(): Promise { const httpVersion = await this._httpVersionPromise || null; if (!httpVersion) return 'HTTP/1.1'; @@ -685,7 +693,7 @@ export class Response extends SdkObject { return responseHeadersSize(await this._rawResponseHeadersPromise, this.statusText()); } - private async _sizes(): Promise { + async internalSizes(): Promise { const requestHeadersSize = await this._request._requestHeadersSize(); const responseHeadersSize = await this.responseHeadersSize();