chore: align FFConnection with CRConnection (#27450)

This commit is contained in:
Dmitry Gozman 2023-10-05 13:46:41 -07:00 committed by GitHub
parent 293c85935a
commit cba2fc0752
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 85 additions and 119 deletions

View File

@ -25,13 +25,14 @@ import type { Page, PageBinding, PageDelegate } from '../page';
import type { ConnectionTransport } from '../transport'; import type { ConnectionTransport } from '../transport';
import type * as types from '../types'; import type * as types from '../types';
import type * as channels from '@protocol/channels'; import type * as channels from '@protocol/channels';
import { ConnectionEvents, FFConnection } from './ffConnection'; import { ConnectionEvents, FFConnection, type FFSession } from './ffConnection';
import { FFPage } from './ffPage'; import { FFPage } from './ffPage';
import type { Protocol } from './protocol'; import type { Protocol } from './protocol';
import type { SdkObject } from '../instrumentation'; import type { SdkObject } from '../instrumentation';
export class FFBrowser extends Browser { export class FFBrowser extends Browser {
_connection: FFConnection; private _connection: FFConnection;
readonly session: FFSession;
readonly _ffPages: Map<string, FFPage>; readonly _ffPages: Map<string, FFPage>;
readonly _contexts: Map<string, FFBrowserContext>; readonly _contexts: Map<string, FFBrowserContext>;
private _version = ''; private _version = '';
@ -46,7 +47,7 @@ export class FFBrowser extends Browser {
if (Object.keys(kBandaidFirefoxUserPrefs).length) if (Object.keys(kBandaidFirefoxUserPrefs).length)
firefoxUserPrefs = { ...kBandaidFirefoxUserPrefs, ...firefoxUserPrefs }; firefoxUserPrefs = { ...kBandaidFirefoxUserPrefs, ...firefoxUserPrefs };
const promises: Promise<any>[] = [ const promises: Promise<any>[] = [
connection.send('Browser.enable', { browser.session.send('Browser.enable', {
attachToDefaultContext: !!options.persistent, attachToDefaultContext: !!options.persistent,
userPrefs: Object.entries(firefoxUserPrefs).map(([name, value]) => ({ name, value })), 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()); promises.push((browser._defaultContext as FFBrowserContext)._initialize());
} }
if (options.proxy) 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); await Promise.all(promises);
return browser; return browser;
} }
@ -65,18 +66,19 @@ export class FFBrowser extends Browser {
constructor(parent: SdkObject, connection: FFConnection, options: BrowserOptions) { constructor(parent: SdkObject, connection: FFConnection, options: BrowserOptions) {
super(parent, options); super(parent, options);
this._connection = connection; this._connection = connection;
this.session = connection.rootSession;
this._ffPages = new Map(); this._ffPages = new Map();
this._contexts = new Map(); this._contexts = new Map();
this._connection.on(ConnectionEvents.Disconnected, () => this._onDisconnect()); this._connection.on(ConnectionEvents.Disconnected, () => this._onDisconnect());
this._connection.on('Browser.attachedToTarget', this._onAttachedToTarget.bind(this)); this.session.on('Browser.attachedToTarget', this._onAttachedToTarget.bind(this));
this._connection.on('Browser.detachedFromTarget', this._onDetachedFromTarget.bind(this)); this.session.on('Browser.detachedFromTarget', this._onDetachedFromTarget.bind(this));
this._connection.on('Browser.downloadCreated', this._onDownloadCreated.bind(this)); this.session.on('Browser.downloadCreated', this._onDownloadCreated.bind(this));
this._connection.on('Browser.downloadFinished', this._onDownloadFinished.bind(this)); this.session.on('Browser.downloadFinished', this._onDownloadFinished.bind(this));
this._connection.on('Browser.videoRecordingFinished', this._onVideoRecordingFinished.bind(this)); this.session.on('Browser.videoRecordingFinished', this._onVideoRecordingFinished.bind(this));
} }
async _initVersion() { 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._version = result.version.substring(result.version.indexOf('/') + 1);
this._userAgent = result.userAgent; this._userAgent = result.userAgent;
} }
@ -88,7 +90,7 @@ export class FFBrowser extends Browser {
async doCreateNewContext(options: channels.BrowserNewContextParams): Promise<BrowserContext> { async doCreateNewContext(options: channels.BrowserNewContextParams): Promise<BrowserContext> {
if (options.isMobile) if (options.isMobile)
throw new Error('options.isMobile is not supported in Firefox'); 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); const context = new FFBrowserContext(this, browserContextId, options);
await context._initialize(); await context._initialize();
this._contexts.set(browserContextId, context); this._contexts.set(browserContextId, context);
@ -178,7 +180,7 @@ export class FFBrowserContext extends BrowserContext {
const browserContextId = this._browserContextId; const browserContextId = this._browserContextId;
const promises: Promise<any>[] = [super._initialize()]; const promises: Promise<any>[] = [super._initialize()];
if (this._options.acceptDownloads !== 'internal-browser-default') { if (this._options.acceptDownloads !== 'internal-browser-default') {
promises.push(this._browser._connection.send('Browser.setDownloadOptions', { promises.push(this._browser.session.send('Browser.setDownloadOptions', {
browserContextId, browserContextId,
downloadOptions: { downloadOptions: {
behavior: this._options.acceptDownloads === 'accept' ? 'saveToDisk' : 'cancel', 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 }, viewportSize: { width: this._options.viewport.width, height: this._options.viewport.height },
deviceScaleFactor: this._options.deviceScaleFactor || 1, 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) 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) 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) 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) 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) 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) 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) 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) if (this._options.extraHTTPHeaders || this._options.locale)
promises.push(this.setExtraHTTPHeaders(this._options.extraHTTPHeaders || [])); promises.push(this.setExtraHTTPHeaders(this._options.extraHTTPHeaders || []));
if (this._options.httpCredentials) if (this._options.httpCredentials)
@ -216,26 +218,26 @@ export class FFBrowserContext extends BrowserContext {
if (this._options.offline) if (this._options.offline)
promises.push(this.setOffline(this._options.offline)); promises.push(this.setOffline(this._options.offline));
if (this._options.colorScheme !== 'no-override') { if (this._options.colorScheme !== 'no-override') {
promises.push(this._browser._connection.send('Browser.setColorScheme', { promises.push(this._browser.session.send('Browser.setColorScheme', {
browserContextId, browserContextId,
colorScheme: this._options.colorScheme !== undefined ? this._options.colorScheme : 'light', colorScheme: this._options.colorScheme !== undefined ? this._options.colorScheme : 'light',
})); }));
} }
if (this._options.reducedMotion !== 'no-override') { if (this._options.reducedMotion !== 'no-override') {
promises.push(this._browser._connection.send('Browser.setReducedMotion', { promises.push(this._browser.session.send('Browser.setReducedMotion', {
browserContextId, browserContextId,
reducedMotion: this._options.reducedMotion !== undefined ? this._options.reducedMotion : 'no-preference', reducedMotion: this._options.reducedMotion !== undefined ? this._options.reducedMotion : 'no-preference',
})); }));
} }
if (this._options.forcedColors !== 'no-override') { if (this._options.forcedColors !== 'no-override') {
promises.push(this._browser._connection.send('Browser.setForcedColors', { promises.push(this._browser.session.send('Browser.setForcedColors', {
browserContextId, browserContextId,
forcedColors: this._options.forcedColors !== undefined ? this._options.forcedColors : 'none', forcedColors: this._options.forcedColors !== undefined ? this._options.forcedColors : 'none',
})); }));
} }
if (this._options.recordVideo) { if (this._options.recordVideo) {
promises.push(this._ensureVideosPath().then(() => { promises.push(this._ensureVideosPath().then(() => {
return this._browser._connection.send('Browser.setVideoRecordingOptions', { return this._browser.session.send('Browser.setVideoRecordingOptions', {
// validateBrowserContextOptions ensures correct video size. // validateBrowserContextOptions ensures correct video size.
options: { options: {
...this._options.recordVideo!.size!, ...this._options.recordVideo!.size!,
@ -246,7 +248,7 @@ export class FFBrowserContext extends BrowserContext {
})); }));
} }
if (this._options.proxy) { if (this._options.proxy) {
promises.push(this._browser._connection.send('Browser.setContextProxy', { promises.push(this._browser.session.send('Browser.setContextProxy', {
browserContextId: this._browserContextId, browserContextId: this._browserContextId,
...toJugglerProxyOptions(this._options.proxy) ...toJugglerProxyOptions(this._options.proxy)
})); }));
@ -265,7 +267,7 @@ export class FFBrowserContext extends BrowserContext {
async newPageDelegate(): Promise<PageDelegate> { async newPageDelegate(): Promise<PageDelegate> {
assertBrowserContextIsNotOwned(this); assertBrowserContextIsNotOwned(this);
const { targetId } = await this._browser._connection.send('Browser.newPage', { const { targetId } = await this._browser.session.send('Browser.newPage', {
browserContextId: this._browserContextId browserContextId: this._browserContextId
}).catch(e => { }).catch(e => {
if (e.message.includes('Failed to override timezone')) if (e.message.includes('Failed to override timezone'))
@ -276,7 +278,7 @@ export class FFBrowserContext extends BrowserContext {
} }
async doGetCookies(urls: string[]): Promise<channels.NetworkCookie[]> { async doGetCookies(urls: string[]): Promise<channels.NetworkCookie[]> {
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 => { return network.filterCookies(cookies.map(c => {
const copy: any = { ... c }; const copy: any = { ... c };
delete copy.size; delete copy.size;
@ -290,11 +292,11 @@ export class FFBrowserContext extends BrowserContext {
...c, ...c,
expires: c.expires === -1 ? undefined : c.expires, 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() { 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[]) { async doGrantPermissions(origin: string, permissions: string[]) {
@ -310,17 +312,17 @@ export class FFBrowserContext extends BrowserContext {
throw new Error('Unknown permission: ' + permission); throw new Error('Unknown permission: ' + permission);
return protocolPermission; 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() { 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<void> { async setGeolocation(geolocation?: types.Geolocation): Promise<void> {
verifyGeolocation(geolocation); verifyGeolocation(geolocation);
this._options.geolocation = 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<void> { async setExtraHTTPHeaders(headers: types.HeadersArray): Promise<void> {
@ -328,33 +330,33 @@ export class FFBrowserContext extends BrowserContext {
let allHeaders = this._options.extraHTTPHeaders; let allHeaders = this._options.extraHTTPHeaders;
if (this._options.locale) if (this._options.locale)
allHeaders = network.mergeHeaders([allHeaders, network.singleHeader('Accept-Language', 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<void> { async setUserAgent(userAgent: string | undefined): Promise<void> {
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<void> { async setOffline(offline: boolean): Promise<void> {
this._options.offline = offline; 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<void> { async doSetHTTPCredentials(httpCredentials?: types.Credentials): Promise<void> {
this._options.httpCredentials = httpCredentials; 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) { 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() { 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) { 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() { async doRemoveExposedBindings() {
@ -364,20 +366,20 @@ export class FFBrowserContext extends BrowserContext {
} }
async doUpdateRequestInterception(): Promise<void> { async doUpdateRequestInterception(): Promise<void> {
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() {} onClosePersistent() {}
override async clearCache(): Promise<void> { override async clearCache(): Promise<void> {
// Clearing only the context cache does not work: https://bugzilla.mozilla.org/show_bug.cgi?id=1819147 // 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() { async doClose() {
if (!this._browserContextId) { if (!this._browserContextId) {
if (this._options.recordVideo) { if (this._options.recordVideo) {
await this._browser._connection.send('Browser.setVideoRecordingOptions', { await this._browser.session.send('Browser.setVideoRecordingOptions', {
options: undefined, options: undefined,
browserContextId: this._browserContextId browserContextId: this._browserContextId
}); });
@ -385,13 +387,13 @@ export class FFBrowserContext extends BrowserContext {
// Closing persistent context should close the browser. // Closing persistent context should close the browser.
await this._browser.close(); await this._browser.close();
} else { } 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); this._browser._contexts.delete(this._browserContextId);
} }
} }
async cancelDownload(uuid: string) { async cancelDownload(uuid: string) {
await this._browser._connection.send('Browser.cancelDownload', { uuid }); await this._browser.session.send('Browser.cancelDownload', { uuid });
} }
} }

View File

@ -16,7 +16,6 @@
*/ */
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { assert } from '../../utils';
import type { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport'; import type { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
import type { Protocol } from './protocol'; import type { Protocol } from './protocol';
import { rewriteErrorMessage } from '../../utils/stackTrace'; import { rewriteErrorMessage } from '../../utils/stackTrace';
@ -36,19 +35,14 @@ export const kBrowserCloseMessageId = -9999;
export class FFConnection extends EventEmitter { export class FFConnection extends EventEmitter {
private _lastId: number; private _lastId: number;
private _callbacks: Map<number, {resolve: (o: any) => void, reject: (e: ProtocolError) => void, error: ProtocolError, method: string}>;
private _transport: ConnectionTransport; private _transport: ConnectionTransport;
private readonly _protocolLogger: ProtocolLogger; private readonly _protocolLogger: ProtocolLogger;
private readonly _browserLogsCollector: RecentLogsCollector; private readonly _browserLogsCollector: RecentLogsCollector;
_browserDisconnectedLogs: string | undefined;
readonly rootSession: FFSession;
readonly _sessions: Map<string, FFSession>; readonly _sessions: Map<string, FFSession>;
_closed: boolean; _closed: boolean;
override on: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
override addListener: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
override off: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
override removeListener: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
override once: <T extends keyof Protocol.Events | symbol>(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) { constructor(transport: ConnectionTransport, protocolLogger: ProtocolLogger, browserLogsCollector: RecentLogsCollector) {
super(); super();
this.setMaxListeners(0); this.setMaxListeners(0);
@ -56,43 +50,20 @@ export class FFConnection extends EventEmitter {
this._protocolLogger = protocolLogger; this._protocolLogger = protocolLogger;
this._browserLogsCollector = browserLogsCollector; this._browserLogsCollector = browserLogsCollector;
this._lastId = 0; this._lastId = 0;
this._callbacks = new Map();
this._sessions = new Map(); this._sessions = new Map();
this._closed = false; this._closed = false;
this.rootSession = new FFSession(this, '', message => this._rawSend(message));
this.on = super.on; this._sessions.set('', this.rootSession);
this.addListener = super.addListener;
this.off = super.removeListener;
this.removeListener = super.removeListener;
this.once = super.once;
this._transport.onmessage = this._onMessage.bind(this); this._transport.onmessage = this._onMessage.bind(this);
// onclose should be set last, since it can be immediately called. // onclose should be set last, since it can be immediately called.
this._transport.onclose = this._onClose.bind(this); this._transport.onclose = this._onClose.bind(this);
} }
async send<T extends keyof Protocol.CommandParameters>(
method: T,
params?: Protocol.CommandParameters[T]
): Promise<Protocol.CommandReturnValues[T]> {
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 { nextMessageId(): number {
return ++this._lastId; return ++this._lastId;
} }
_checkClosed(method: string) {
if (this._closed)
throw new ProtocolError(true, `${method}): Browser closed.` + helper.formatBrowserLogs(this._browserLogsCollector.recentLogs()));
}
_rawSend(message: ProtocolRequest) { _rawSend(message: ProtocolRequest) {
this._protocolLogger('send', message); this._protocolLogger('send', message);
this._transport.send(message); this._transport.send(message);
@ -102,36 +73,17 @@ export class FFConnection extends EventEmitter {
this._protocolLogger('receive', message); this._protocolLogger('receive', message);
if (message.id === kBrowserCloseMessageId) if (message.id === kBrowserCloseMessageId)
return; return;
if (message.sessionId) { const session = this._sessions.get(message.sessionId || '');
const session = this._sessions.get(message.sessionId); if (session)
if (session) session.dispatchMessage(message);
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));
}
} }
_onClose() { _onClose() {
this._closed = true; this._closed = true;
this._transport.onmessage = undefined; this._transport.onmessage = undefined;
this._transport.onclose = undefined; this._transport.onclose = undefined;
const formattedBrowserLogs = helper.formatBrowserLogs(this._browserLogsCollector.recentLogs()); this._browserDisconnectedLogs = helper.formatBrowserLogs(this._browserLogsCollector.recentLogs());
for (const callback of this._callbacks.values()) { this.rootSession.dispose();
const error = rewriteErrorMessage(callback.error, `Protocol error (${callback.method}): Browser closed.` + formattedBrowserLogs);
error.sessionClosed = true;
callback.reject(error);
}
this._callbacks.clear();
Promise.resolve().then(() => this.emit(ConnectionEvents.Disconnected)); Promise.resolve().then(() => this.emit(ConnectionEvents.Disconnected));
} }
@ -179,15 +131,24 @@ export class FFSession extends EventEmitter {
this._crashed = true; 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<T extends keyof Protocol.CommandParameters>( async send<T extends keyof Protocol.CommandParameters>(
method: T, method: T,
params?: Protocol.CommandParameters[T] params?: Protocol.CommandParameters[T]
): Promise<Protocol.CommandReturnValues[T]> { ): Promise<Protocol.CommandReturnValues[T]> {
if (this._crashed) const closedErrorMessage = this._closedErrorMessage();
throw new ProtocolError(true, 'Target crashed'); if (closedErrorMessage)
this._connection._checkClosed(method); throw new ProtocolError(true, closedErrorMessage);
if (this._disposed)
throw new ProtocolError(true, 'Target closed');
const id = this._connection.nextMessageId(); const id = this._connection.nextMessageId();
this._rawSend({ method, params, id }); this._rawSend({ method, params, id });
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -200,27 +161,30 @@ export class FFSession extends EventEmitter {
} }
dispatchMessage(object: ProtocolResponse) { dispatchMessage(object: ProtocolResponse) {
if (object.id && this._callbacks.has(object.id)) { if (object.id) {
const callback = this._callbacks.get(object.id)!; const callback = this._callbacks.get(object.id);
this._callbacks.delete(object.id); // Callbacks could be all rejected if someone has called `.dispose()`.
if (object.error) if (callback) {
callback.reject(createProtocolError(callback.error, callback.method, object.error)); this._callbacks.delete(object.id);
else if (object.error)
callback.resolve(object.result); callback.reject(createProtocolError(callback.error, callback.method, object.error));
else
callback.resolve(object.result);
}
} else { } else {
assert(!object.id);
Promise.resolve().then(() => this.emit(object.method!, object.params)); Promise.resolve().then(() => this.emit(object.method!, object.params));
} }
} }
dispose() { 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._disposed = true;
this._connection._sessions.delete(this._sessionId); 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();
} }
} }