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 * 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<string, FFPage>;
readonly _contexts: Map<string, FFBrowserContext>;
private _version = '';
@ -46,7 +47,7 @@ export class FFBrowser extends Browser {
if (Object.keys(kBandaidFirefoxUserPrefs).length)
firefoxUserPrefs = { ...kBandaidFirefoxUserPrefs, ...firefoxUserPrefs };
const promises: Promise<any>[] = [
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<BrowserContext> {
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<any>[] = [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<PageDelegate> {
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<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 => {
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<void> {
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<void> {
@ -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<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> {
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> {
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<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() {}
override async clearCache(): Promise<void> {
// 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 });
}
}

View File

@ -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<number, {resolve: (o: any) => 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<string, FFSession>;
_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) {
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<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 {
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<T extends keyof Protocol.CommandParameters>(
method: T,
params?: Protocol.CommandParameters[T]
): Promise<Protocol.CommandReturnValues[T]> {
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();
}
}