From 2a5d83a623e67a79c3b60c5785580d92dc44e10f Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Mon, 19 May 2025 20:39:41 +0000 Subject: [PATCH] chore: introduce `Page.viewportSizeChanged` event (#35994) --- packages/playwright-core/src/client/page.ts | 7 ++++--- packages/playwright-core/src/protocol/validator.ts | 6 ++++++ .../playwright-core/src/server/bidi/bidiPage.ts | 6 +++--- .../playwright-core/src/server/chromium/crPage.ts | 4 ++-- .../src/server/dispatchers/pageDispatcher.ts | 3 ++- .../playwright-core/src/server/firefox/ffPage.ts | 2 +- packages/playwright-core/src/server/page.ts | 14 ++++++++------ .../playwright-core/src/server/screenshotter.ts | 11 +++++------ .../playwright-core/src/server/webkit/wkBrowser.ts | 2 +- .../playwright-core/src/server/webkit/wkPage.ts | 12 ++++++------ packages/protocol/src/channels.d.ts | 8 ++++++++ packages/protocol/src/protocol.yml | 8 ++++++++ 12 files changed, 54 insertions(+), 29 deletions(-) diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index bdc36d3a6a..1cf7c1b4ca 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -84,7 +84,7 @@ export class Page extends ChannelOwner implements api.Page _workers = new Set(); private _closed = false; readonly _closedOrCrashedScope = new LongStandingScope(); - private _viewportSize: Size | null; + private _viewportSize: Size | undefined; _routes: RouteHandler[] = []; _webSocketRoutes: WebSocketRouteHandler[] = []; @@ -130,7 +130,7 @@ export class Page extends ChannelOwner implements api.Page this._mainFrame = Frame.from(initializer.mainFrame); this._mainFrame._page = this; this._frames.add(this._mainFrame); - this._viewportSize = initializer.viewportSize || null; + this._viewportSize = initializer.viewportSize; this._closed = initializer.isClosed; this._opener = Page.fromNullable(initializer.opener); @@ -151,6 +151,7 @@ export class Page extends ChannelOwner implements api.Page const artifactObject = Artifact.from(artifact); this._forceVideo()._artifactReady(artifactObject); }); + this._channel.on('viewportSizeChanged', ({ viewportSize }) => this._viewportSize = viewportSize); this._channel.on('webSocket', ({ webSocket }) => this.emit(Events.Page.WebSocket, WebSocket.from(webSocket))); this._channel.on('worker', ({ worker }) => this._onWorker(Worker.from(worker))); @@ -506,7 +507,7 @@ export class Page extends ChannelOwner implements api.Page } viewportSize(): Size | null { - return this._viewportSize; + return this._viewportSize || null; } async evaluate(pageFunction: structs.PageFunction, arg?: Arg): Promise { diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index aa8082bf54..bdbae046c6 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -1122,6 +1122,12 @@ scheme.PageDownloadEvent = tObject({ suggestedFilename: tString, artifact: tChannel(['Artifact']), }); +scheme.PageViewportSizeChangedEvent = tObject({ + viewportSize: tOptional(tObject({ + width: tNumber, + height: tNumber, + })), +}); scheme.PageFileChooserEvent = tObject({ element: tChannel(['ElementHandle']), isMultiple: tBoolean, diff --git a/packages/playwright-core/src/server/bidi/bidiPage.ts b/packages/playwright-core/src/server/bidi/bidiPage.ts index 89f06c81e4..27cc2730d8 100644 --- a/packages/playwright-core/src/server/bidi/bidiPage.ts +++ b/packages/playwright-core/src/server/bidi/bidiPage.ts @@ -268,10 +268,10 @@ export class BidiPage implements PageDelegate { async updateEmulatedViewportSize(): Promise { const options = this._browserContext._options; - const deviceSize = this._page.emulatedSize(); - if (deviceSize === null) + const emulatedSize = this._page.emulatedSize(); + if (!emulatedSize) return; - const viewportSize = deviceSize.viewport; + const viewportSize = emulatedSize.viewport; await this._session.send('browsingContext.setViewport', { context: this._session.sessionId, viewport: { diff --git a/packages/playwright-core/src/server/chromium/crPage.ts b/packages/playwright-core/src/server/chromium/crPage.ts index bd36ae6941..263999c3f4 100644 --- a/packages/playwright-core/src/server/chromium/crPage.ts +++ b/packages/playwright-core/src/server/chromium/crPage.ts @@ -108,7 +108,7 @@ export class CRPage implements PageDelegate { const features = opener._nextWindowOpenPopupFeatures.shift() || []; const viewportSize = helper.getViewportSizeFromWindowFeatures(features); if (viewportSize) - this._page.setEmulatedSize({ viewport: viewportSize, screen: viewportSize }); + this._page.setEmulatedSizeFromWindowOpen({ viewport: viewportSize, screen: viewportSize }); } const createdEvent = this._isBackgroundPage ? CRBrowserContext.CREvents.BackgroundPage : BrowserContext.Events.Page; @@ -953,7 +953,7 @@ class FrameSession { assert(this._isMainFrame()); const options = this._crPage._browserContext._options; const emulatedSize = this._page.emulatedSize(); - if (emulatedSize === null) + if (!emulatedSize) return; const viewportSize = emulatedSize.viewport; const screenSize = emulatedSize.screen; diff --git a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts index 1c308a6d37..818ff50991 100644 --- a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts @@ -66,7 +66,7 @@ export class PageDispatcher extends Dispatcher this._dispatchEvent('viewportSizeChanged', { viewportSize: page.emulatedSize()?.viewport })); this.addObjectListener(Page.Events.FileChooser, (fileChooser: FileChooser) => this._dispatchEvent('fileChooser', { element: ElementHandleDispatcher.from(mainFrame, fileChooser.element()), isMultiple: fileChooser.isMultiple() diff --git a/packages/playwright-core/src/server/firefox/ffPage.ts b/packages/playwright-core/src/server/firefox/ffPage.ts index 8195f36739..ef37a1bfef 100644 --- a/packages/playwright-core/src/server/firefox/ffPage.ts +++ b/packages/playwright-core/src/server/firefox/ffPage.ts @@ -334,7 +334,7 @@ export class FFPage implements PageDelegate { } async updateEmulatedViewportSize(): Promise { - const viewportSize = this._page.viewportSize(); + const viewportSize = this._page.emulatedSize()?.viewport ?? null; await this._session.send('Page.setViewportSize', { viewportSize }); } diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index 70f2d1b3c2..501a5b50be 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -125,6 +125,7 @@ export class Page extends SdkObject { Close: 'close', Crash: 'crash', Download: 'download', + EmulatedSizeChanged: 'emulatedsizechanged', FileChooser: 'filechooser', FrameAttached: 'frameattached', FrameDetached: 'framedetached', @@ -546,23 +547,24 @@ export class Page extends SdkObject { } async setViewportSize(viewportSize: types.Size) { - this._emulatedSize = { viewport: { ...viewportSize }, screen: { ...viewportSize } }; + this._setEmulatedSize({ viewport: { ...viewportSize }, screen: { ...viewportSize } }); await this.delegate.updateEmulatedViewportSize(); } - viewportSize(): types.Size | null { - return this.emulatedSize()?.viewport || null; + setEmulatedSizeFromWindowOpen(emulatedSize: EmulatedSize) { + this._setEmulatedSize(emulatedSize); } - setEmulatedSize(emulatedSize: EmulatedSize) { + private _setEmulatedSize(emulatedSize: EmulatedSize) { this._emulatedSize = emulatedSize; + this.emit(Page.Events.EmulatedSizeChanged); } - emulatedSize(): EmulatedSize | null { + emulatedSize(): EmulatedSize | undefined { if (this._emulatedSize) return this._emulatedSize; const contextOptions = this.browserContext._options; - return contextOptions.viewport ? { viewport: contextOptions.viewport, screen: contextOptions.screen || contextOptions.viewport } : null; + return contextOptions.viewport ? { viewport: contextOptions.viewport, screen: contextOptions.screen || contextOptions.viewport } : undefined; } async bringToFront(): Promise { diff --git a/packages/playwright-core/src/server/screenshotter.ts b/packages/playwright-core/src/server/screenshotter.ts index cd76958ad8..30e8c9ae1f 100644 --- a/packages/playwright-core/src/server/screenshotter.ts +++ b/packages/playwright-core/src/server/screenshotter.ts @@ -173,12 +173,11 @@ export class Screenshotter { this._queue = new TaskQueue(); } - private async _originalViewportSize(progress: Progress): Promise<{ viewportSize: types.Size, originalViewportSize: types.Size | null }> { - const originalViewportSize = this._page.viewportSize(); - let viewportSize = originalViewportSize; + private async _originalViewportSize(progress: Progress): Promise { + let viewportSize = this._page.emulatedSize()?.viewport; if (!viewportSize) viewportSize = await this._page.mainFrame().waitForFunctionValueInUtility(progress, () => ({ width: window.innerWidth, height: window.innerHeight })); - return { viewportSize, originalViewportSize }; + return viewportSize; } private async _fullPageSize(progress: Progress): Promise { @@ -205,7 +204,7 @@ export class Screenshotter { const format = validateScreenshotOptions(options); return this._queue.postTask(async () => { progress.log('taking page screenshot'); - const { viewportSize } = await this._originalViewportSize(progress); + const viewportSize = await this._originalViewportSize(progress); await this._preparePageForScreenshot(progress, this._page.mainFrame(), options.style, options.caret !== 'initial', options.animations === 'disabled'); progress.throwIfAborted(); // Avoid restoring after failure - should be done by cleanup. @@ -233,7 +232,7 @@ export class Screenshotter { const format = validateScreenshotOptions(options); return this._queue.postTask(async () => { progress.log('taking element screenshot'); - const { viewportSize } = await this._originalViewportSize(progress); + const viewportSize = await this._originalViewportSize(progress); await this._preparePageForScreenshot(progress, handle._frame, options.style, options.caret !== 'initial', options.animations === 'disabled'); progress.throwIfAborted(); // Do not do extra work. diff --git a/packages/playwright-core/src/server/webkit/wkBrowser.ts b/packages/playwright-core/src/server/webkit/wkBrowser.ts index 0ccc9b4791..33f6742bd9 100644 --- a/packages/playwright-core/src/server/webkit/wkBrowser.ts +++ b/packages/playwright-core/src/server/webkit/wkBrowser.ts @@ -358,7 +358,7 @@ export class WKBrowserContext extends BrowserContext { await this._browser._browserSession.send('Playwright.cancelDownload', { uuid }); } - _validateEmulatedViewport(viewportSize?: types.Size | null) { + _validateEmulatedViewport(viewportSize: types.Size | undefined) { if (!viewportSize) return; if (process.platform === 'win32' && this._browser.options.headful && (viewportSize.width < 250 || viewportSize.height < 240)) diff --git a/packages/playwright-core/src/server/webkit/wkPage.ts b/packages/playwright-core/src/server/webkit/wkPage.ts index b15f313141..e61c78bd71 100644 --- a/packages/playwright-core/src/server/webkit/wkPage.ts +++ b/packages/playwright-core/src/server/webkit/wkPage.ts @@ -109,7 +109,7 @@ export class WKPage implements PageDelegate { const viewportSize = helper.getViewportSizeFromWindowFeatures(opener._nextWindowOpenPopupFeatures); opener._nextWindowOpenPopupFeatures = undefined; if (viewportSize) - this._page.setEmulatedSize({ viewport: viewportSize, screen: viewportSize }); + this._page.setEmulatedSizeFromWindowOpen({ viewport: viewportSize, screen: viewportSize }); } } @@ -677,7 +677,7 @@ export class WKPage implements PageDelegate { } async updateEmulatedViewportSize(): Promise { - this._browserContext._validateEmulatedViewport(this._page.viewportSize()); + this._browserContext._validateEmulatedViewport(this._page.emulatedSize()?.viewport); await this._updateViewport(); } @@ -694,11 +694,11 @@ export class WKPage implements PageDelegate { async _updateViewport(): Promise { const options = this._browserContext._options; - const deviceSize = this._page.emulatedSize(); - if (deviceSize === null) + const emulatedSize = this._page.emulatedSize(); + if (!emulatedSize) return; - const viewportSize = deviceSize.viewport; - const screenSize = deviceSize.screen; + const viewportSize = emulatedSize.viewport; + const screenSize = emulatedSize.screen; const promises: Promise[] = [ this._pageProxySession.send('Emulation.setDeviceMetricsOverride', { width: viewportSize.width, diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index 62ea817120..3ac6b9a9a9 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -2013,6 +2013,7 @@ export interface PageEventTarget { on(event: 'close', callback: (params: PageCloseEvent) => void): this; on(event: 'crash', callback: (params: PageCrashEvent) => void): this; on(event: 'download', callback: (params: PageDownloadEvent) => void): this; + on(event: 'viewportSizeChanged', callback: (params: PageViewportSizeChangedEvent) => void): this; on(event: 'fileChooser', callback: (params: PageFileChooserEvent) => void): this; on(event: 'frameAttached', callback: (params: PageFrameAttachedEvent) => void): this; on(event: 'frameDetached', callback: (params: PageFrameDetachedEvent) => void): this; @@ -2075,6 +2076,12 @@ export type PageDownloadEvent = { suggestedFilename: string, artifact: ArtifactChannel, }; +export type PageViewportSizeChangedEvent = { + viewportSize?: { + width: number, + height: number, + }, +}; export type PageFileChooserEvent = { element: ElementHandleChannel, isMultiple: boolean, @@ -2566,6 +2573,7 @@ export interface PageEvents { 'close': PageCloseEvent; 'crash': PageCrashEvent; 'download': PageDownloadEvent; + 'viewportSizeChanged': PageViewportSizeChangedEvent; 'fileChooser': PageFileChooserEvent; 'frameAttached': PageFrameAttachedEvent; 'frameDetached': PageFrameDetachedEvent; diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 03eaba7837..fad3581f09 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -2001,6 +2001,14 @@ Page: suggestedFilename: string artifact: Artifact + viewportSizeChanged: + parameters: + viewportSize: + type: object? + properties: + width: number + height: number + fileChooser: parameters: element: ElementHandle