diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index 1ee49a01dc88f..8ed1d7c98d7bb 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -674,6 +674,11 @@ export class Page extends ChannelOwner implements api.Page if (isTargetClosedError(e) && !options.runBeforeUnload) return; throw e; + } finally { + // If the page actually closed, the 'close' event (and with it, screencast's + // auto-save) is dispatched by the server before the close response is sent, + // so this is guaranteed to observe an in-flight save, if any, at this point. + await this.screencast._waitForPendingAutoSave(); } } diff --git a/packages/playwright-core/src/client/screencast.ts b/packages/playwright-core/src/client/screencast.ts index 6b838a80ead92..acf0a25b014cf 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'; @@ -26,12 +27,31 @@ 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; 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(). page.close() awaits this via + // _waitForPendingAutoSave() before it resolves. + 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; + 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 { @@ -54,9 +74,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 };