From b6154905f57dcb6812ae208a42754c0a6f250d5f Mon Sep 17 00:00:00 2001 From: sarang-001 Date: Sat, 11 Jul 2026 21:14:11 +0530 Subject: [PATCH 1/2] fix(screencast): auto-save video when page closes without stop When using page.screencast.start({ path }), if the page closes before stop() is called, the video file was never saved. The server already stops the recording gracefully, but the client never called artifact.saveAs(). Listen for Events.Page.Close in the client Screencast constructor and auto-save the artifact when the page closes while a recording is active. Fixes #41608 --- .../playwright-core/src/client/screencast.ts | 18 ++++++++++++++++-- tests/library/screencast.spec.ts | 18 ++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/packages/playwright-core/src/client/screencast.ts b/packages/playwright-core/src/client/screencast.ts index 6b838a80ead92..8aefd9c5c8b46 100644 --- a/packages/playwright-core/src/client/screencast.ts +++ b/packages/playwright-core/src/client/screencast.ts @@ -16,6 +16,7 @@ import { Artifact } from './artifact'; import { DisposableStub } from './disposable'; +import { Events } from './events'; import type * as api from '../../types/types'; import type { Page } from './page'; @@ -32,6 +33,19 @@ export class Screencast implements api.Screencast { this._page._channel.on('screencastFrame', ({ data, timestamp, viewportWidth, viewportHeight }) => { void this._onFrame?.({ data, timestamp, viewportWidth, viewportHeight }); }); + this._page.once(Events.Page.Close, () => { + // Auto-save the video when the page closes, so recordings are not lost + // if the user forgets to call stop(). + if (this._started && this._savePath && this._artifact) { + const artifact = this._artifact; + const savePath = this._savePath; + this._started = false; + this._onFrame = null; + this._artifact = undefined; + this._savePath = undefined; + artifact.saveAs(savePath).catch(() => {}); + } + }); } 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 { @@ -54,9 +68,9 @@ export class Screencast implements api.Screencast { } async stop(): Promise { + this._started = false; + this._onFrame = null; await this._page._wrapApiCall(async () => { - this._started = false; - this._onFrame = null; await this._page._channel.screencastStop({}, undefined); if (this._savePath) await this._artifact?.saveAs(this._savePath); diff --git a/tests/library/screencast.spec.ts b/tests/library/screencast.spec.ts index a67b0aaaf250a..fdfd054189b20 100644 --- a/tests/library/screencast.spec.ts +++ b/tests/library/screencast.spec.ts @@ -204,6 +204,24 @@ test('start should finish when page is closed', async ({ browser }, testInfo) => await context.close(); }); +test('should auto-save video when page closes without stop', async ({ browser, server }, testInfo) => { + test.slow(); + const size = { width: 800, height: 800 }; + const context = await browser.newContext({ viewport: size }); + const page = await context.newPage(); + const videoPath = testInfo.outputPath('auto-save-video.webm'); + await page.screencast.start({ path: videoPath, size }); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => document.body.style.backgroundColor = 'red'); + await ensureSomeFrames(page); + // Close the page without calling stop() + await page.close(); + // The video file should be saved automatically + expect(fs.existsSync(videoPath)).toBeTruthy(); + expectFrames(videoPath, size, isAlmostRed); + await context.close(); +}); + test('empty video', async ({ browser }, testInfo) => { test.slow(); const size = { width: 800, height: 800 }; From a5c1fb4d60cc8d40c75d4e4474e031eef2374ecc Mon Sep 17 00:00:00 2001 From: sarang-001 Date: Sun, 12 Jul 2026 22:48:08 +0530 Subject: [PATCH 2/2] fix(screencast): await pending auto-save in page.close() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to review feedback on PR #41744. The auto-save triggered from the once(Events.Page.Close, ...) listener was fire-and-forget — artifact.saveAs(savePath).catch(() => {}) with nothing awaiting it. That meant page.close() could resolve before the video was actually on disk, and a failed save produced no signal at all. Screencast now tracks the in-flight save as _pendingAutoSave, and Page.close() awaits it in a finally block before resolving. Co-authored-by: Git-Raini <216259955+Git-Raini@users.noreply.github.com> --- packages/playwright-core/src/client/page.ts | 2 ++ packages/playwright-core/src/client/screencast.ts | 10 ++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index 1ee49a01dc88f..ee65df9cafc3b 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -674,6 +674,8 @@ export class Page extends ChannelOwner implements api.Page if (isTargetClosedError(e) && !options.runBeforeUnload) return; throw e; + } finally { + await this.screencast._waitForPendingAutoSave(); } } diff --git a/packages/playwright-core/src/client/screencast.ts b/packages/playwright-core/src/client/screencast.ts index 8aefd9c5c8b46..acf0a25b014cf 100644 --- a/packages/playwright-core/src/client/screencast.ts +++ b/packages/playwright-core/src/client/screencast.ts @@ -27,6 +27,7 @@ export class Screencast implements api.Screencast { private _savePath: string | undefined; private _onFrame: ((frame: { data: Buffer, timestamp: number, viewportWidth: number, viewportHeight: number }) => Promise) | null = null; private _artifact: Artifact | undefined; + private _pendingAutoSave: Promise | undefined; constructor(page: Page) { this._page = page; @@ -35,7 +36,8 @@ export class Screencast implements api.Screencast { }); this._page.once(Events.Page.Close, () => { // Auto-save the video when the page closes, so recordings are not lost - // if the user forgets to call stop(). + // if the user forgets to call stop(). page.close() awaits this via + // _waitForPendingAutoSave() before it resolves. if (this._started && this._savePath && this._artifact) { const artifact = this._artifact; const savePath = this._savePath; @@ -43,11 +45,15 @@ export class Screencast implements api.Screencast { this._onFrame = null; this._artifact = undefined; this._savePath = undefined; - artifact.saveAs(savePath).catch(() => {}); + this._pendingAutoSave = artifact.saveAs(savePath).catch(() => {}); } }); } + async _waitForPendingAutoSave(): Promise { + await this._pendingAutoSave; + } + 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');