From cba2fc0752e1c97e673858dcd6bd813a04acacc9 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 5 Oct 2023 13:46:41 -0700 Subject: [PATCH] chore: align `FFConnection` with `CRConnection` (#27450) --- .../src/server/firefox/ffBrowser.ts | 90 +++++++------- .../src/server/firefox/ffConnection.ts | 114 ++++++------------ 2 files changed, 85 insertions(+), 119 deletions(-) diff --git a/packages/playwright-core/src/server/firefox/ffBrowser.ts b/packages/playwright-core/src/server/firefox/ffBrowser.ts index 3b13eda242..7a9a24cff6 100644 --- a/packages/playwright-core/src/server/firefox/ffBrowser.ts +++ b/packages/playwright-core/src/server/firefox/ffBrowser.ts @@ -25,13 +25,14 @@ import type { Page, PageBinding, PageDelegate } from '../page'; import type { ConnectionTransport } from '../transport'; import type * as types from '../types'; import type * as channels from '@protocol/channels'; -import { ConnectionEvents, FFConnection } from './ffConnection'; +import { ConnectionEvents, FFConnection, type FFSession } from './ffConnection'; import { FFPage } from './ffPage'; import type { Protocol } from './protocol'; import type { SdkObject } from '../instrumentation'; export class FFBrowser extends Browser { - _connection: FFConnection; + private _connection: FFConnection; + readonly session: FFSession; readonly _ffPages: Map; readonly _contexts: Map; private _version = ''; @@ -46,7 +47,7 @@ export class FFBrowser extends Browser { if (Object.keys(kBandaidFirefoxUserPrefs).length) firefoxUserPrefs = { ...kBandaidFirefoxUserPrefs, ...firefoxUserPrefs }; const promises: Promise[] = [ - connection.send('Browser.enable', { + browser.session.send('Browser.enable', { attachToDefaultContext: !!options.persistent, userPrefs: Object.entries(firefoxUserPrefs).map(([name, value]) => ({ name, value })), }), @@ -57,7 +58,7 @@ export class FFBrowser extends Browser { promises.push((browser._defaultContext as FFBrowserContext)._initialize()); } if (options.proxy) - promises.push(browser._connection.send('Browser.setBrowserProxy', toJugglerProxyOptions(options.proxy))); + promises.push(browser.session.send('Browser.setBrowserProxy', toJugglerProxyOptions(options.proxy))); await Promise.all(promises); return browser; } @@ -65,18 +66,19 @@ export class FFBrowser extends Browser { constructor(parent: SdkObject, connection: FFConnection, options: BrowserOptions) { super(parent, options); this._connection = connection; + this.session = connection.rootSession; this._ffPages = new Map(); this._contexts = new Map(); this._connection.on(ConnectionEvents.Disconnected, () => this._onDisconnect()); - this._connection.on('Browser.attachedToTarget', this._onAttachedToTarget.bind(this)); - this._connection.on('Browser.detachedFromTarget', this._onDetachedFromTarget.bind(this)); - this._connection.on('Browser.downloadCreated', this._onDownloadCreated.bind(this)); - this._connection.on('Browser.downloadFinished', this._onDownloadFinished.bind(this)); - this._connection.on('Browser.videoRecordingFinished', this._onVideoRecordingFinished.bind(this)); + this.session.on('Browser.attachedToTarget', this._onAttachedToTarget.bind(this)); + this.session.on('Browser.detachedFromTarget', this._onDetachedFromTarget.bind(this)); + this.session.on('Browser.downloadCreated', this._onDownloadCreated.bind(this)); + this.session.on('Browser.downloadFinished', this._onDownloadFinished.bind(this)); + this.session.on('Browser.videoRecordingFinished', this._onVideoRecordingFinished.bind(this)); } async _initVersion() { - const result = await this._connection.send('Browser.getInfo'); + const result = await this.session.send('Browser.getInfo'); this._version = result.version.substring(result.version.indexOf('/') + 1); this._userAgent = result.userAgent; } @@ -88,7 +90,7 @@ export class FFBrowser extends Browser { async doCreateNewContext(options: channels.BrowserNewContextParams): Promise { if (options.isMobile) throw new Error('options.isMobile is not supported in Firefox'); - const { browserContextId } = await this._connection.send('Browser.createBrowserContext', { removeOnDetach: true }); + const { browserContextId } = await this.session.send('Browser.createBrowserContext', { removeOnDetach: true }); const context = new FFBrowserContext(this, browserContextId, options); await context._initialize(); this._contexts.set(browserContextId, context); @@ -178,7 +180,7 @@ export class FFBrowserContext extends BrowserContext { const browserContextId = this._browserContextId; const promises: Promise[] = [super._initialize()]; if (this._options.acceptDownloads !== 'internal-browser-default') { - promises.push(this._browser._connection.send('Browser.setDownloadOptions', { + promises.push(this._browser.session.send('Browser.setDownloadOptions', { browserContextId, downloadOptions: { behavior: this._options.acceptDownloads === 'accept' ? 'saveToDisk' : 'cancel', @@ -191,22 +193,22 @@ export class FFBrowserContext extends BrowserContext { viewportSize: { width: this._options.viewport.width, height: this._options.viewport.height }, deviceScaleFactor: this._options.deviceScaleFactor || 1, }; - promises.push(this._browser._connection.send('Browser.setDefaultViewport', { browserContextId, viewport })); + promises.push(this._browser.session.send('Browser.setDefaultViewport', { browserContextId, viewport })); } if (this._options.hasTouch) - promises.push(this._browser._connection.send('Browser.setTouchOverride', { browserContextId, hasTouch: true })); + promises.push(this._browser.session.send('Browser.setTouchOverride', { browserContextId, hasTouch: true })); if (this._options.userAgent) - promises.push(this._browser._connection.send('Browser.setUserAgentOverride', { browserContextId, userAgent: this._options.userAgent })); + promises.push(this._browser.session.send('Browser.setUserAgentOverride', { browserContextId, userAgent: this._options.userAgent })); if (this._options.bypassCSP) - promises.push(this._browser._connection.send('Browser.setBypassCSP', { browserContextId, bypassCSP: true })); + promises.push(this._browser.session.send('Browser.setBypassCSP', { browserContextId, bypassCSP: true })); if (this._options.ignoreHTTPSErrors) - promises.push(this._browser._connection.send('Browser.setIgnoreHTTPSErrors', { browserContextId, ignoreHTTPSErrors: true })); + promises.push(this._browser.session.send('Browser.setIgnoreHTTPSErrors', { browserContextId, ignoreHTTPSErrors: true })); if (this._options.javaScriptEnabled === false) - promises.push(this._browser._connection.send('Browser.setJavaScriptDisabled', { browserContextId, javaScriptDisabled: true })); + promises.push(this._browser.session.send('Browser.setJavaScriptDisabled', { browserContextId, javaScriptDisabled: true })); if (this._options.locale) - promises.push(this._browser._connection.send('Browser.setLocaleOverride', { browserContextId, locale: this._options.locale })); + promises.push(this._browser.session.send('Browser.setLocaleOverride', { browserContextId, locale: this._options.locale })); if (this._options.timezoneId) - promises.push(this._browser._connection.send('Browser.setTimezoneOverride', { browserContextId, timezoneId: this._options.timezoneId })); + promises.push(this._browser.session.send('Browser.setTimezoneOverride', { browserContextId, timezoneId: this._options.timezoneId })); if (this._options.extraHTTPHeaders || this._options.locale) promises.push(this.setExtraHTTPHeaders(this._options.extraHTTPHeaders || [])); if (this._options.httpCredentials) @@ -216,26 +218,26 @@ export class FFBrowserContext extends BrowserContext { if (this._options.offline) promises.push(this.setOffline(this._options.offline)); if (this._options.colorScheme !== 'no-override') { - promises.push(this._browser._connection.send('Browser.setColorScheme', { + promises.push(this._browser.session.send('Browser.setColorScheme', { browserContextId, colorScheme: this._options.colorScheme !== undefined ? this._options.colorScheme : 'light', })); } if (this._options.reducedMotion !== 'no-override') { - promises.push(this._browser._connection.send('Browser.setReducedMotion', { + promises.push(this._browser.session.send('Browser.setReducedMotion', { browserContextId, reducedMotion: this._options.reducedMotion !== undefined ? this._options.reducedMotion : 'no-preference', })); } if (this._options.forcedColors !== 'no-override') { - promises.push(this._browser._connection.send('Browser.setForcedColors', { + promises.push(this._browser.session.send('Browser.setForcedColors', { browserContextId, forcedColors: this._options.forcedColors !== undefined ? this._options.forcedColors : 'none', })); } if (this._options.recordVideo) { promises.push(this._ensureVideosPath().then(() => { - return this._browser._connection.send('Browser.setVideoRecordingOptions', { + return this._browser.session.send('Browser.setVideoRecordingOptions', { // validateBrowserContextOptions ensures correct video size. options: { ...this._options.recordVideo!.size!, @@ -246,7 +248,7 @@ export class FFBrowserContext extends BrowserContext { })); } if (this._options.proxy) { - promises.push(this._browser._connection.send('Browser.setContextProxy', { + promises.push(this._browser.session.send('Browser.setContextProxy', { browserContextId: this._browserContextId, ...toJugglerProxyOptions(this._options.proxy) })); @@ -265,7 +267,7 @@ export class FFBrowserContext extends BrowserContext { async newPageDelegate(): Promise { assertBrowserContextIsNotOwned(this); - const { targetId } = await this._browser._connection.send('Browser.newPage', { + const { targetId } = await this._browser.session.send('Browser.newPage', { browserContextId: this._browserContextId }).catch(e => { if (e.message.includes('Failed to override timezone')) @@ -276,7 +278,7 @@ export class FFBrowserContext extends BrowserContext { } async doGetCookies(urls: string[]): Promise { - const { cookies } = await this._browser._connection.send('Browser.getCookies', { browserContextId: this._browserContextId }); + const { cookies } = await this._browser.session.send('Browser.getCookies', { browserContextId: this._browserContextId }); return network.filterCookies(cookies.map(c => { const copy: any = { ... c }; delete copy.size; @@ -290,11 +292,11 @@ export class FFBrowserContext extends BrowserContext { ...c, expires: c.expires === -1 ? undefined : c.expires, })); - await this._browser._connection.send('Browser.setCookies', { browserContextId: this._browserContextId, cookies: cc }); + await this._browser.session.send('Browser.setCookies', { browserContextId: this._browserContextId, cookies: cc }); } async clearCookies() { - await this._browser._connection.send('Browser.clearCookies', { browserContextId: this._browserContextId }); + await this._browser.session.send('Browser.clearCookies', { browserContextId: this._browserContextId }); } async doGrantPermissions(origin: string, permissions: string[]) { @@ -310,17 +312,17 @@ export class FFBrowserContext extends BrowserContext { throw new Error('Unknown permission: ' + permission); return protocolPermission; }); - await this._browser._connection.send('Browser.grantPermissions', { origin: origin, browserContextId: this._browserContextId, permissions: filtered }); + await this._browser.session.send('Browser.grantPermissions', { origin: origin, browserContextId: this._browserContextId, permissions: filtered }); } async doClearPermissions() { - await this._browser._connection.send('Browser.resetPermissions', { browserContextId: this._browserContextId }); + await this._browser.session.send('Browser.resetPermissions', { browserContextId: this._browserContextId }); } async setGeolocation(geolocation?: types.Geolocation): Promise { verifyGeolocation(geolocation); this._options.geolocation = geolocation; - await this._browser._connection.send('Browser.setGeolocationOverride', { browserContextId: this._browserContextId, geolocation: geolocation || null }); + await this._browser.session.send('Browser.setGeolocationOverride', { browserContextId: this._browserContextId, geolocation: geolocation || null }); } async setExtraHTTPHeaders(headers: types.HeadersArray): Promise { @@ -328,33 +330,33 @@ export class FFBrowserContext extends BrowserContext { let allHeaders = this._options.extraHTTPHeaders; if (this._options.locale) allHeaders = network.mergeHeaders([allHeaders, network.singleHeader('Accept-Language', this._options.locale)]); - await this._browser._connection.send('Browser.setExtraHTTPHeaders', { browserContextId: this._browserContextId, headers: allHeaders }); + await this._browser.session.send('Browser.setExtraHTTPHeaders', { browserContextId: this._browserContextId, headers: allHeaders }); } async setUserAgent(userAgent: string | undefined): Promise { - await this._browser._connection.send('Browser.setUserAgentOverride', { browserContextId: this._browserContextId, userAgent: userAgent || null }); + await this._browser.session.send('Browser.setUserAgentOverride', { browserContextId: this._browserContextId, userAgent: userAgent || null }); } async setOffline(offline: boolean): Promise { this._options.offline = offline; - await this._browser._connection.send('Browser.setOnlineOverride', { browserContextId: this._browserContextId, override: offline ? 'offline' : 'online' }); + await this._browser.session.send('Browser.setOnlineOverride', { browserContextId: this._browserContextId, override: offline ? 'offline' : 'online' }); } async doSetHTTPCredentials(httpCredentials?: types.Credentials): Promise { this._options.httpCredentials = httpCredentials; - await this._browser._connection.send('Browser.setHTTPCredentials', { browserContextId: this._browserContextId, credentials: httpCredentials || null }); + await this._browser.session.send('Browser.setHTTPCredentials', { browserContextId: this._browserContextId, credentials: httpCredentials || null }); } async doAddInitScript(source: string) { - await this._browser._connection.send('Browser.setInitScripts', { browserContextId: this._browserContextId, scripts: this.initScripts.map(script => ({ script })) }); + await this._browser.session.send('Browser.setInitScripts', { browserContextId: this._browserContextId, scripts: this.initScripts.map(script => ({ script })) }); } async doRemoveInitScripts() { - await this._browser._connection.send('Browser.setInitScripts', { browserContextId: this._browserContextId, scripts: [] }); + await this._browser.session.send('Browser.setInitScripts', { browserContextId: this._browserContextId, scripts: [] }); } async doExposeBinding(binding: PageBinding) { - await this._browser._connection.send('Browser.addBinding', { browserContextId: this._browserContextId, name: binding.name, script: binding.source }); + await this._browser.session.send('Browser.addBinding', { browserContextId: this._browserContextId, name: binding.name, script: binding.source }); } async doRemoveExposedBindings() { @@ -364,20 +366,20 @@ export class FFBrowserContext extends BrowserContext { } async doUpdateRequestInterception(): Promise { - await this._browser._connection.send('Browser.setRequestInterception', { browserContextId: this._browserContextId, enabled: !!this._requestInterceptor }); + await this._browser.session.send('Browser.setRequestInterception', { browserContextId: this._browserContextId, enabled: !!this._requestInterceptor }); } onClosePersistent() {} override async clearCache(): Promise { // Clearing only the context cache does not work: https://bugzilla.mozilla.org/show_bug.cgi?id=1819147 - await this._browser._connection.send('Browser.clearCache'); + await this._browser.session.send('Browser.clearCache'); } async doClose() { if (!this._browserContextId) { if (this._options.recordVideo) { - await this._browser._connection.send('Browser.setVideoRecordingOptions', { + await this._browser.session.send('Browser.setVideoRecordingOptions', { options: undefined, browserContextId: this._browserContextId }); @@ -385,13 +387,13 @@ export class FFBrowserContext extends BrowserContext { // Closing persistent context should close the browser. await this._browser.close(); } else { - await this._browser._connection.send('Browser.removeBrowserContext', { browserContextId: this._browserContextId }); + await this._browser.session.send('Browser.removeBrowserContext', { browserContextId: this._browserContextId }); this._browser._contexts.delete(this._browserContextId); } } async cancelDownload(uuid: string) { - await this._browser._connection.send('Browser.cancelDownload', { uuid }); + await this._browser.session.send('Browser.cancelDownload', { uuid }); } } diff --git a/packages/playwright-core/src/server/firefox/ffConnection.ts b/packages/playwright-core/src/server/firefox/ffConnection.ts index 90cfba66f0..04cdebf7ae 100644 --- a/packages/playwright-core/src/server/firefox/ffConnection.ts +++ b/packages/playwright-core/src/server/firefox/ffConnection.ts @@ -16,7 +16,6 @@ */ import { EventEmitter } from 'events'; -import { assert } from '../../utils'; import type { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport'; import type { Protocol } from './protocol'; import { rewriteErrorMessage } from '../../utils/stackTrace'; @@ -36,19 +35,14 @@ export const kBrowserCloseMessageId = -9999; export class FFConnection extends EventEmitter { private _lastId: number; - private _callbacks: Map void, reject: (e: ProtocolError) => void, error: ProtocolError, method: string}>; private _transport: ConnectionTransport; private readonly _protocolLogger: ProtocolLogger; private readonly _browserLogsCollector: RecentLogsCollector; + _browserDisconnectedLogs: string | undefined; + readonly rootSession: FFSession; readonly _sessions: Map; _closed: boolean; - override on: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - override addListener: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - override off: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - override removeListener: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - override once: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - constructor(transport: ConnectionTransport, protocolLogger: ProtocolLogger, browserLogsCollector: RecentLogsCollector) { super(); this.setMaxListeners(0); @@ -56,43 +50,20 @@ export class FFConnection extends EventEmitter { this._protocolLogger = protocolLogger; this._browserLogsCollector = browserLogsCollector; this._lastId = 0; - this._callbacks = new Map(); - this._sessions = new Map(); this._closed = false; - - this.on = super.on; - this.addListener = super.addListener; - this.off = super.removeListener; - this.removeListener = super.removeListener; - this.once = super.once; + this.rootSession = new FFSession(this, '', message => this._rawSend(message)); + this._sessions.set('', this.rootSession); this._transport.onmessage = this._onMessage.bind(this); // onclose should be set last, since it can be immediately called. this._transport.onclose = this._onClose.bind(this); } - async send( - method: T, - params?: Protocol.CommandParameters[T] - ): Promise { - this._checkClosed(method); - const id = this.nextMessageId(); - this._rawSend({ id, method, params }); - return new Promise((resolve, reject) => { - this._callbacks.set(id, { resolve, reject, error: new ProtocolError(false), method }); - }); - } - nextMessageId(): number { return ++this._lastId; } - _checkClosed(method: string) { - if (this._closed) - throw new ProtocolError(true, `${method}): Browser closed.` + helper.formatBrowserLogs(this._browserLogsCollector.recentLogs())); - } - _rawSend(message: ProtocolRequest) { this._protocolLogger('send', message); this._transport.send(message); @@ -102,36 +73,17 @@ export class FFConnection extends EventEmitter { this._protocolLogger('receive', message); if (message.id === kBrowserCloseMessageId) return; - if (message.sessionId) { - const session = this._sessions.get(message.sessionId); - if (session) - session.dispatchMessage(message); - } else if (message.id) { - const callback = this._callbacks.get(message.id); - // Callbacks could be all rejected if someone has called `.dispose()`. - if (callback) { - this._callbacks.delete(message.id); - if (message.error) - callback.reject(createProtocolError(callback.error, callback.method, message.error)); - else - callback.resolve(message.result); - } - } else { - Promise.resolve().then(() => this.emit(message.method!, message.params)); - } + const session = this._sessions.get(message.sessionId || ''); + if (session) + session.dispatchMessage(message); } _onClose() { this._closed = true; this._transport.onmessage = undefined; this._transport.onclose = undefined; - const formattedBrowserLogs = helper.formatBrowserLogs(this._browserLogsCollector.recentLogs()); - for (const callback of this._callbacks.values()) { - const error = rewriteErrorMessage(callback.error, `Protocol error (${callback.method}): Browser closed.` + formattedBrowserLogs); - error.sessionClosed = true; - callback.reject(error); - } - this._callbacks.clear(); + this._browserDisconnectedLogs = helper.formatBrowserLogs(this._browserLogsCollector.recentLogs()); + this.rootSession.dispose(); Promise.resolve().then(() => this.emit(ConnectionEvents.Disconnected)); } @@ -179,15 +131,24 @@ export class FFSession extends EventEmitter { this._crashed = true; } + private _closedErrorMessage() { + if (this._crashed) + return 'Target crashed'; + if (this._connection._browserDisconnectedLogs !== undefined) + return `Browser closed.` + this._connection._browserDisconnectedLogs; + if (this._disposed) + return `Target closed`; + if (this._connection._closed) + return 'Browser closed'; + } + async send( method: T, params?: Protocol.CommandParameters[T] ): Promise { - if (this._crashed) - throw new ProtocolError(true, 'Target crashed'); - this._connection._checkClosed(method); - if (this._disposed) - throw new ProtocolError(true, 'Target closed'); + const closedErrorMessage = this._closedErrorMessage(); + if (closedErrorMessage) + throw new ProtocolError(true, closedErrorMessage); const id = this._connection.nextMessageId(); this._rawSend({ method, params, id }); return new Promise((resolve, reject) => { @@ -200,27 +161,30 @@ export class FFSession extends EventEmitter { } dispatchMessage(object: ProtocolResponse) { - if (object.id && this._callbacks.has(object.id)) { - const callback = this._callbacks.get(object.id)!; - this._callbacks.delete(object.id); - if (object.error) - callback.reject(createProtocolError(callback.error, callback.method, object.error)); - else - callback.resolve(object.result); + if (object.id) { + const callback = this._callbacks.get(object.id); + // Callbacks could be all rejected if someone has called `.dispose()`. + if (callback) { + this._callbacks.delete(object.id); + if (object.error) + callback.reject(createProtocolError(callback.error, callback.method, object.error)); + else + callback.resolve(object.result); + } } else { - assert(!object.id); Promise.resolve().then(() => this.emit(object.method!, object.params)); } } dispose() { - for (const callback of this._callbacks.values()) { - callback.error.sessionClosed = true; - callback.reject(rewriteErrorMessage(callback.error, 'Target closed')); - } - this._callbacks.clear(); this._disposed = true; this._connection._sessions.delete(this._sessionId); + const errorMessage = this._closedErrorMessage()!; + for (const callback of this._callbacks.values()) { + callback.error.sessionClosed = true; + callback.reject(rewriteErrorMessage(callback.error, errorMessage)); + } + this._callbacks.clear(); } }