diff --git a/packages/playwright-core/src/server/chromium/crPage.ts b/packages/playwright-core/src/server/chromium/crPage.ts index 47de15ba00..f4bef720dc 100644 --- a/packages/playwright-core/src/server/chromium/crPage.ts +++ b/packages/playwright-core/src/server/chromium/crPage.ts @@ -98,7 +98,7 @@ export class CRPage implements PageDelegate { const features = opener._nextWindowOpenPopupFeatures.shift() || []; const viewportSize = helper.getViewportSizeFromWindowFeatures(features); if (viewportSize) - this._page._state.emulatedSize = { viewport: viewportSize, screen: viewportSize }; + this._page._emulatedSize = { viewport: viewportSize, screen: viewportSize }; } // Note: it is important to call |reportAsNew| before resolving pageOrError promise, // so that anyone who awaits pageOrError got a ready and reported page. @@ -199,8 +199,7 @@ export class CRPage implements PageDelegate { await this._forAllFrameSessions(frame => frame._updateHttpCredentials(false)); } - async setEmulatedSize(emulatedSize: types.EmulatedSize): Promise { - assert(this._page._state.emulatedSize === emulatedSize); + async updateEmulatedViewportSize(): Promise { await this._mainFrameSession._updateViewport(); } @@ -972,7 +971,7 @@ class FrameSession { async _updateExtraHTTPHeaders(initial: boolean): Promise { const headers = network.mergeHeaders([ this._crPage._browserContext._options.extraHTTPHeaders, - this._page._state.extraHTTPHeaders + this._page.extraHTTPHeaders() ]); if (!initial || headers.length) await this._client.send('Network.setExtraHTTPHeaders', { headers: headersArrayToObject(headers, false /* lowerCase */) }); @@ -1001,7 +1000,7 @@ class FrameSession { return; assert(this._isMainFrame()); const options = this._crPage._browserContext._options; - const emulatedSize = this._page._state.emulatedSize; + const emulatedSize = this._page.emulatedSize(); if (emulatedSize === null) return; const viewportSize = emulatedSize.viewport; @@ -1059,16 +1058,17 @@ class FrameSession { } async _updateEmulateMedia(initial: boolean): Promise { - const colorScheme = this._page._state.colorScheme === null ? '' : this._page._state.colorScheme; - const reducedMotion = this._page._state.reducedMotion === null ? '' : this._page._state.reducedMotion; - const forcedColors = this._page._state.forcedColors === null ? '' : this._page._state.forcedColors; + const emulatedMedia = this._page.emulatedMedia(); + const colorScheme = emulatedMedia.colorScheme === null ? '' : emulatedMedia.colorScheme; + const reducedMotion = emulatedMedia.reducedMotion === null ? '' : emulatedMedia.reducedMotion; + const forcedColors = emulatedMedia.forcedColors === null ? '' : emulatedMedia.forcedColors; const features = [ { name: 'prefers-color-scheme', value: colorScheme }, { name: 'prefers-reduced-motion', value: reducedMotion }, { name: 'forced-colors', value: forcedColors }, ]; // Empty string disables the override. - await this._client.send('Emulation.setEmulatedMedia', { media: this._page._state.mediaType || '', features }); + await this._client.send('Emulation.setEmulatedMedia', { media: emulatedMedia.media || '', features }); } private async _setDefaultFontFamilies(session: CRSession) { @@ -1081,7 +1081,7 @@ class FrameSession { } async _updateFileChooserInterception(initial: boolean) { - const enabled = this._page._state.interceptFileChooser; + const enabled = this._page.fileChooserIntercepted(); if (initial && !enabled) return; await this._client.send('Page.setInterceptFileChooserDialog', { enabled }).catch(e => {}); // target can be closed. diff --git a/packages/playwright-core/src/server/firefox/ffPage.ts b/packages/playwright-core/src/server/firefox/ffPage.ts index 4e079fe07b..a045b511fb 100644 --- a/packages/playwright-core/src/server/firefox/ffPage.ts +++ b/packages/playwright-core/src/server/firefox/ffPage.ts @@ -20,7 +20,6 @@ import * as dom from '../dom'; import type * as frames from '../frames'; import type { RegisteredListener } from '../../utils/eventsHelper'; import { eventsHelper } from '../../utils/eventsHelper'; -import { assert } from '../../utils'; import type { PageBinding, PageDelegate } from '../page'; import { Page, Worker } from '../page'; import type * as types from '../types'; @@ -352,17 +351,12 @@ export class FFPage implements PageDelegate { } async updateExtraHTTPHeaders(): Promise { - await this._session.send('Network.setExtraHTTPHeaders', { headers: this._page._state.extraHTTPHeaders || [] }); + await this._session.send('Network.setExtraHTTPHeaders', { headers: this._page.extraHTTPHeaders() || [] }); } - async setEmulatedSize(emulatedSize: types.EmulatedSize): Promise { - assert(this._page._state.emulatedSize === emulatedSize); - await this._session.send('Page.setViewportSize', { - viewportSize: { - width: emulatedSize.viewport.width, - height: emulatedSize.viewport.height, - }, - }); + async updateEmulatedViewportSize(): Promise { + const viewportSize = this._page.viewportSize(); + await this._session.send('Page.setViewportSize', { viewportSize }); } async bringToFront(): Promise { @@ -370,12 +364,13 @@ export class FFPage implements PageDelegate { } async updateEmulateMedia(): Promise { - const colorScheme = this._page._state.colorScheme === null ? undefined : this._page._state.colorScheme; - const reducedMotion = this._page._state.reducedMotion === null ? undefined : this._page._state.reducedMotion; - const forcedColors = this._page._state.forcedColors === null ? undefined : this._page._state.forcedColors; + const emulatedMedia = this._page.emulatedMedia(); + const colorScheme = emulatedMedia.colorScheme ?? undefined; + const reducedMotion = emulatedMedia.reducedMotion ?? undefined; + const forcedColors = emulatedMedia.forcedColors ?? undefined; await this._session.send('Page.setEmulatedMedia', { // Empty string means reset. - type: this._page._state.mediaType === null ? '' : this._page._state.mediaType, + type: emulatedMedia.media === null ? '' : emulatedMedia.media, colorScheme, reducedMotion, forcedColors, diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index a160cf973f..fba84b0e82 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -653,7 +653,7 @@ export class Frame extends SdkObject { private async _gotoAction(progress: Progress, url: string, options: types.GotoOptions): Promise { const waitUntil = verifyLifecycle('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil); progress.log(`navigating to "${url}", waiting until "${waitUntil}"`); - const headers = this._page._state.extraHTTPHeaders || []; + const headers = this._page.extraHTTPHeaders() || []; const refererHeader = headers.find(h => h.name.toLowerCase() === 'referer'); let referer = refererHeader ? refererHeader.value : undefined; if (options.referer !== undefined) { diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index 1124c8b28e..1a7ed2171e 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -65,7 +65,7 @@ export interface PageDelegate { navigateFrame(frame: frames.Frame, url: string, referrer: string | undefined): Promise; updateExtraHTTPHeaders(): Promise; - setEmulatedSize(emulatedSize: types.EmulatedSize): Promise; + updateEmulatedViewportSize(): Promise; updateEmulateMedia(): Promise; updateRequestInterception(): Promise; updateFileChooserInterception(enabled: boolean): Promise; @@ -98,14 +98,13 @@ export interface PageDelegate { readonly cspErrorsAsynchronousForInlineScipts?: boolean; } -type PageState = { - emulatedSize: { screen: types.Size, viewport: types.Size } | null; - mediaType: types.MediaType | null; +type EmulatedSize = { screen: types.Size, viewport: types.Size }; + +type EmulatedMedia = { + media: types.MediaType | null; colorScheme: types.ColorScheme | null; reducedMotion: types.ReducedMotion | null; forcedColors: types.ForcedColors | null; - extraHTTPHeaders: types.HeadersArray | null; - interceptFileChooser: boolean; }; type ExpectScreenshotOptions = { @@ -152,7 +151,10 @@ export class Page extends SdkObject { readonly touchscreen: input.Touchscreen; readonly _timeoutSettings: TimeoutSettings; readonly _delegate: PageDelegate; - readonly _state: PageState; + _emulatedSize: EmulatedSize | undefined; + private _extraHTTPHeaders: types.HeadersArray | undefined; + private _emulatedMedia: Partial = {}; + private _interceptFileChooser = false; private readonly _pageBindings = new Map(); readonly initScripts: string[] = []; readonly _screenshotter: Screenshotter; @@ -176,15 +178,6 @@ export class Page extends SdkObject { this.attribution.page = this; this._delegate = delegate; this._browserContext = browserContext; - this._state = { - emulatedSize: browserContext._options.viewport ? { viewport: browserContext._options.viewport, screen: browserContext._options.screen || browserContext._options.viewport } : null, - mediaType: null, - colorScheme: browserContext._options.colorScheme !== undefined ? browserContext._options.colorScheme : 'light', - reducedMotion: browserContext._options.reducedMotion !== undefined ? browserContext._options.reducedMotion : 'no-preference', - forcedColors: browserContext._options.forcedColors !== undefined ? browserContext._options.forcedColors : 'none', - extraHTTPHeaders: null, - interceptFileChooser: false, - }; this.accessibility = new accessibility.Accessibility(delegate.getAccessibilityTree.bind(delegate)); this.keyboard = new input.Keyboard(delegate.rawKeyboard, this); this.mouse = new input.Mouse(delegate.rawMouse, this); @@ -327,10 +320,14 @@ export class Page extends SdkObject { } setExtraHTTPHeaders(headers: types.HeadersArray) { - this._state.extraHTTPHeaders = headers; + this._extraHTTPHeaders = headers; return this._delegate.updateExtraHTTPHeaders(); } + extraHTTPHeaders(): types.HeadersArray | undefined { + return this._extraHTTPHeaders; + } + async _onBindingCalled(payload: string, context: dom.FrameExecutionContext) { if (this._disconnected || this._closedState === 'closed') return; @@ -402,27 +399,44 @@ export class Page extends SdkObject { }), this._timeoutSettings.navigationTimeout(options)); } - async emulateMedia(options: { media?: types.MediaType | null, colorScheme?: types.ColorScheme | null, reducedMotion?: types.ReducedMotion | null, forcedColors?: types.ForcedColors | null }) { + async emulateMedia(options: Partial) { if (options.media !== undefined) - this._state.mediaType = options.media; + this._emulatedMedia.media = options.media; if (options.colorScheme !== undefined) - this._state.colorScheme = options.colorScheme; + this._emulatedMedia.colorScheme = options.colorScheme; if (options.reducedMotion !== undefined) - this._state.reducedMotion = options.reducedMotion; + this._emulatedMedia.reducedMotion = options.reducedMotion; if (options.forcedColors !== undefined) - this._state.forcedColors = options.forcedColors; + this._emulatedMedia.forcedColors = options.forcedColors; await this._delegate.updateEmulateMedia(); await this._doSlowMo(); } + emulatedMedia(): EmulatedMedia { + const contextOptions = this._browserContext._options; + return { + media: this._emulatedMedia.media || null, + colorScheme: this._emulatedMedia.colorScheme !== undefined ? this._emulatedMedia.colorScheme : contextOptions.colorScheme ?? 'light', + reducedMotion: this._emulatedMedia.reducedMotion !== undefined ? this._emulatedMedia.reducedMotion : contextOptions.reducedMotion ?? 'no-preference', + forcedColors: this._emulatedMedia.forcedColors !== undefined ? this._emulatedMedia.forcedColors : contextOptions.forcedColors ?? 'none', + }; + } + async setViewportSize(viewportSize: types.Size) { - this._state.emulatedSize = { viewport: { ...viewportSize }, screen: { ...viewportSize } }; - await this._delegate.setEmulatedSize(this._state.emulatedSize); + this._emulatedSize = { viewport: { ...viewportSize }, screen: { ...viewportSize } }; + await this._delegate.updateEmulatedViewportSize(); await this._doSlowMo(); } viewportSize(): types.Size | null { - return this._state.emulatedSize?.viewport || null; + return this.emulatedSize()?.viewport || null; + } + + emulatedSize(): EmulatedSize | null { + if (this._emulatedSize) + return this._emulatedSize; + const contextOptions = this._browserContext._options; + return contextOptions.viewport ? { viewport: contextOptions.viewport, screen: contextOptions.screen || contextOptions.viewport } : null; } async bringToFront(): Promise { @@ -604,10 +618,14 @@ export class Page extends SdkObject { } async setFileChooserIntercepted(enabled: boolean): Promise { - this._state.interceptFileChooser = enabled; + this._interceptFileChooser = enabled; await this._delegate.updateFileChooserInterception(enabled); } + fileChooserIntercepted() { + return this._interceptFileChooser; + } + frameNavigatedToNewDocument(frame: frames.Frame) { this.emit(Page.Events.InternalFrameNavigatedToNewDocument, frame); const url = frame.url(); diff --git a/packages/playwright-core/src/server/webkit/wkPage.ts b/packages/playwright-core/src/server/webkit/wkPage.ts index b36578bdd6..6735792205 100644 --- a/packages/playwright-core/src/server/webkit/wkPage.ts +++ b/packages/playwright-core/src/server/webkit/wkPage.ts @@ -106,7 +106,7 @@ export class WKPage implements PageDelegate { const viewportSize = helper.getViewportSizeFromWindowFeatures(opener._nextWindowOpenPopupFeatures); opener._nextWindowOpenPopupFeatures = undefined; if (viewportSize) - this._page._state.emulatedSize = { viewport: viewportSize, screen: viewportSize }; + this._page._emulatedSize = { viewport: viewportSize, screen: viewportSize }; } } @@ -194,18 +194,20 @@ export class WKPage implements PageDelegate { const contextOptions = this._browserContext._options; if (contextOptions.userAgent) promises.push(session.send('Page.overrideUserAgent', { value: contextOptions.userAgent })); - if (this._page._state.mediaType || this._page._state.colorScheme || this._page._state.reducedMotion) - promises.push(WKPage._setEmulateMedia(session, this._page._state.mediaType, this._page._state.colorScheme, this._page._state.reducedMotion)); + const emulatedMedia = this._page.emulatedMedia(); + if (emulatedMedia.media || emulatedMedia.colorScheme || emulatedMedia.reducedMotion) + promises.push(WKPage._setEmulateMedia(session, emulatedMedia.media, emulatedMedia.colorScheme, emulatedMedia.reducedMotion)); const bootstrapScript = this._calculateBootstrapScript(); if (bootstrapScript.length) promises.push(session.send('Page.setBootstrapScript', { source: bootstrapScript })); this._page.frames().map(frame => frame.evaluateExpression(bootstrapScript, false, undefined).catch(e => {})); if (contextOptions.bypassCSP) promises.push(session.send('Page.setBypassCSP', { enabled: true })); - if (this._page._state.emulatedSize) { + const emulatedSize = this._page.emulatedSize(); + if (emulatedSize) { promises.push(session.send('Page.setScreenSizeOverride', { - width: this._page._state.emulatedSize.screen.width, - height: this._page._state.emulatedSize.screen.height, + width: emulatedSize.screen.width, + height: emulatedSize.screen.height, })); } promises.push(this.updateEmulateMedia()); @@ -217,7 +219,7 @@ export class WKPage implements PageDelegate { promises.push(session.send('Page.setTimeZone', { timeZone: contextOptions.timezoneId }). catch(e => { throw new Error(`Invalid timezone ID: ${contextOptions.timezoneId}`); })); } - if (this._page._state.interceptFileChooser) + if (this._page.fileChooserIntercepted()) promises.push(session.send('Page.setInterceptFileChooserDialog', { enabled: true })); promises.push(session.send('Page.overrideSetting', { setting: 'DeviceOrientationEventEnabled' as any, value: contextOptions.isMobile })); promises.push(session.send('Page.overrideSetting', { setting: 'FullScreenEnabled' as any, value: !contextOptions.isMobile })); @@ -658,20 +660,20 @@ export class WKPage implements PageDelegate { const locale = this._browserContext._options.locale; const headers = network.mergeHeaders([ this._browserContext._options.extraHTTPHeaders, - this._page._state.extraHTTPHeaders, + this._page.extraHTTPHeaders(), locale ? network.singleHeader('Accept-Language', locale) : undefined, ]); return headers; } async updateEmulateMedia(): Promise { - const colorScheme = this._page._state.colorScheme; - const reducedMotion = this._page._state.reducedMotion; - await this._forAllSessions(session => WKPage._setEmulateMedia(session, this._page._state.mediaType, colorScheme, reducedMotion)); + const emulatedMedia = this._page.emulatedMedia(); + const colorScheme = emulatedMedia.colorScheme; + const reducedMotion = emulatedMedia.reducedMotion; + await this._forAllSessions(session => WKPage._setEmulateMedia(session, emulatedMedia.media, colorScheme, reducedMotion)); } - async setEmulatedSize(emulatedSize: types.EmulatedSize): Promise { - assert(this._page._state.emulatedSize === emulatedSize); + async updateEmulatedViewportSize(): Promise { await this._updateViewport(); } @@ -683,7 +685,7 @@ export class WKPage implements PageDelegate { async _updateViewport(): Promise { const options = this._browserContext._options; - const deviceSize = this._page._state.emulatedSize; + const deviceSize = this._page.emulatedSize(); if (deviceSize === null) return; const viewportSize = deviceSize.viewport; @@ -725,7 +727,7 @@ export class WKPage implements PageDelegate { } async updateFileChooserInterception() { - const enabled = this._page._state.interceptFileChooser; + const enabled = this._page.fileChooserIntercepted(); await this._session.send('Page.setInterceptFileChooserDialog', { enabled }).catch(e => {}); // target can be closed. }