feat(firefox): roll firefox (#1273)

This commit is contained in:
Dmitry Gozman 2020-03-06 16:49:48 -08:00 committed by GitHub
parent 3c35d7b058
commit aee6324bba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 38 additions and 66 deletions

View File

@ -9,7 +9,7 @@
"main": "index.js", "main": "index.js",
"playwright": { "playwright": {
"chromium_revision": "747023", "chromium_revision": "747023",
"firefox_revision": "1039", "firefox_revision": "1040",
"webkit_revision": "1168" "webkit_revision": "1168"
}, },
"scripts": { "scripts": {

View File

@ -29,6 +29,8 @@ import { headersArray } from './ffNetworkManager';
import { FFPage } from './ffPage'; import { FFPage } from './ffPage';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
const kAttachedToTarget = Symbol('kAttachedToTarget');
export class FFBrowser extends platform.EventEmitter implements Browser { export class FFBrowser extends platform.EventEmitter implements Browser {
_connection: FFConnection; _connection: FFConnection;
_targets: Map<string, Target>; _targets: Map<string, Target>;
@ -36,10 +38,10 @@ export class FFBrowser extends platform.EventEmitter implements Browser {
readonly _contexts: Map<string, FFBrowserContext>; readonly _contexts: Map<string, FFBrowserContext>;
private _eventListeners: RegisteredListener[]; private _eventListeners: RegisteredListener[];
static async connect(transport: ConnectionTransport, slowMo?: number): Promise<FFBrowser> { static async connect(transport: ConnectionTransport, attachToDefaultContext: boolean, slowMo?: number): Promise<FFBrowser> {
const connection = new FFConnection(SlowMoTransport.wrap(transport, slowMo)); const connection = new FFConnection(SlowMoTransport.wrap(transport, slowMo));
const browser = new FFBrowser(connection); const browser = new FFBrowser(connection);
await connection.send('Target.enable'); await connection.send('Browser.enable', { attachToDefaultContext });
return browser; return browser;
} }
@ -56,10 +58,8 @@ export class FFBrowser extends platform.EventEmitter implements Browser {
this.emit(Events.Browser.Disconnected); this.emit(Events.Browser.Disconnected);
}); });
this._eventListeners = [ this._eventListeners = [
helper.addEventListener(this._connection, 'Target.targetCreated', this._onTargetCreated.bind(this)), helper.addEventListener(this._connection, 'Browser.attachedToTarget', this._onAttachedToTarget.bind(this)),
helper.addEventListener(this._connection, 'Target.targetDestroyed', this._onTargetDestroyed.bind(this)), helper.addEventListener(this._connection, 'Browser.detachedFromTarget', this._onDetachedFromTarget.bind(this)),
helper.addEventListener(this._connection, 'Target.targetInfoChanged', this._onTargetInfoChanged.bind(this)),
helper.addEventListener(this._connection, 'Target.attachedToTarget', this._onAttachedToTarget.bind(this)),
]; ];
} }
@ -88,7 +88,7 @@ export class FFBrowser extends platform.EventEmitter implements Browser {
hasTouch: false, hasTouch: false,
}; };
} }
const { browserContextId } = await this._connection.send('Target.createBrowserContext', { const { browserContextId } = await this._connection.send('Browser.createBrowserContext', {
userAgent: options.userAgent, userAgent: options.userAgent,
bypassCSP: options.bypassCSP, bypassCSP: options.bypassCSP,
javaScriptDisabled: options.javaScriptEnabled === false ? true : undefined, javaScriptDisabled: options.javaScriptEnabled === false ? true : undefined,
@ -121,13 +121,13 @@ export class FFBrowser extends platform.EventEmitter implements Browser {
return existingTarget; return existingTarget;
let resolve: (t: Target) => void; let resolve: (t: Target) => void;
const targetPromise = new Promise<Target>(x => resolve = x); const targetPromise = new Promise<Target>(x => resolve = x);
this.on('targetchanged', check); this.on(kAttachedToTarget, check);
try { try {
if (!timeout) if (!timeout)
return await targetPromise; return await targetPromise;
return await helper.waitWithTimeout(targetPromise, 'target', timeout); return await helper.waitWithTimeout(targetPromise, 'target', timeout);
} finally { } finally {
this.removeListener('targetchanged', check); this.removeListener(kAttachedToTarget, check);
} }
function check(target: Target) { function check(target: Target) {
@ -140,33 +140,23 @@ export class FFBrowser extends platform.EventEmitter implements Browser {
return Array.from(this._targets.values()); return Array.from(this._targets.values());
} }
async _onTargetCreated(payload: Protocol.Target.targetCreatedPayload) { _onDetachedFromTarget(payload: Protocol.Browser.detachedFromTargetPayload) {
const {targetId, url, browserContextId, openerId, type} = payload;
const context = browserContextId ? this._contexts.get(browserContextId)! : this._defaultContext;
const target = new Target(this._connection, this, context, targetId, type, url, openerId);
this._targets.set(targetId, target);
}
_onTargetDestroyed(payload: Protocol.Target.targetDestroyedPayload) {
const {targetId} = payload; const {targetId} = payload;
const target = this._targets.get(targetId)!; const target = this._targets.get(targetId)!;
this._targets.delete(targetId); this._targets.delete(targetId);
target._didClose(); target._didClose();
} }
_onTargetInfoChanged(payload: Protocol.Target.targetInfoChangedPayload) { async _onAttachedToTarget(payload: Protocol.Browser.attachedToTargetPayload) {
const {targetId, url} = payload; const {targetId, browserContextId, openerId, type} = payload.targetInfo;
const target = this._targets.get(targetId)!; const context = browserContextId ? this._contexts.get(browserContextId)! : this._defaultContext;
target._url = url; const target = new Target(this, context, type, '', openerId);
} this._targets.set(targetId, target);
target._initPagePromise(this._connection.createSession(payload.sessionId, type));
async _onAttachedToTarget(payload: Protocol.Target.attachedToTargetPayload) {
const {targetId} = payload.targetInfo;
const target = this._targets.get(targetId)!;
target._initPagePromise(this._connection.getSession(payload.sessionId)!);
const pageEvent = new PageEvent(target.pageOrError()); const pageEvent = new PageEvent(target.pageOrError());
target.context().emit(Events.BrowserContext.Page, pageEvent); target.context().emit(Events.BrowserContext.Page, pageEvent);
this.emit(kAttachedToTarget, target);
const opener = target.opener(); const opener = target.opener();
if (!opener) if (!opener)
@ -194,23 +184,22 @@ class Target {
_ffPage: FFPage | null = null; _ffPage: FFPage | null = null;
private readonly _browser: FFBrowser; private readonly _browser: FFBrowser;
private readonly _context: FFBrowserContext; private readonly _context: FFBrowserContext;
private readonly _connection: FFConnection;
private readonly _targetId: string;
private readonly _type: 'page' | 'browser'; private readonly _type: 'page' | 'browser';
_url: string; _url: string;
private readonly _openerId: string | undefined; private readonly _openerId: string | undefined;
private _session?: FFSession;
constructor(connection: any, browser: FFBrowser, context: FFBrowserContext, targetId: string, type: 'page' | 'browser', url: string, openerId: string | undefined) { constructor(browser: FFBrowser, context: FFBrowserContext, type: 'page' | 'browser', url: string, openerId: string | undefined) {
this._browser = browser; this._browser = browser;
this._context = context; this._context = context;
this._connection = connection;
this._targetId = targetId;
this._type = type; this._type = type;
this._url = url; this._url = url;
this._openerId = openerId; this._openerId = openerId;
} }
_didClose() { _didClose() {
if (this._session)
this._session.dispose();
if (this._ffPage) if (this._ffPage)
this._ffPage.didClose(); this._ffPage.didClose();
} }
@ -234,12 +223,11 @@ class Target {
async pageOrError(): Promise<Page | Error> { async pageOrError(): Promise<Page | Error> {
if (this._type !== 'page') if (this._type !== 'page')
throw new Error(`Cannot create page for "${this._type}" target`); throw new Error(`Cannot create page for "${this._type}" target`);
if (!this._pagePromise)
await this._connection.send('Target.attachToTarget', {targetId: this._targetId});
return this._pagePromise!; return this._pagePromise!;
} }
_initPagePromise(session: FFSession) { _initPagePromise(session: FFSession) {
this._session = session;
this._pagePromise = new Promise(async f => { this._pagePromise = new Promise(async f => {
this._ffPage = new FFPage(session, this._context, async () => { this._ffPage = new FFPage(session, this._context, async () => {
const openerTarget = this.opener(); const openerTarget = this.opener();
@ -318,7 +306,7 @@ export class FFBrowserContext extends BrowserContextBase {
async newPage(): Promise<Page> { async newPage(): Promise<Page> {
assertBrowserContextIsNotOwned(this); assertBrowserContextIsNotOwned(this);
const {targetId} = await this._browser._connection.send('Target.newPage', { const {targetId} = await this._browser._connection.send('Browser.newPage', {
browserContextId: this._browserContextId || undefined browserContextId: this._browserContextId || undefined
}); });
const target = this._browser._targets.get(targetId)!; const target = this._browser._targets.get(targetId)!;
@ -410,7 +398,7 @@ export class FFBrowserContext extends BrowserContextBase {
if (this._closed) if (this._closed)
return; return;
assert(this._browserContextId, 'Non-incognito profiles cannot be closed!'); assert(this._browserContextId, 'Non-incognito profiles cannot be closed!');
await this._browser._connection.send('Target.removeBrowserContext', { browserContextId: this._browserContextId }); await this._browser._connection.send('Browser.removeBrowserContext', { browserContextId: this._browserContextId });
this._browser._contexts.delete(this._browserContextId); this._browser._contexts.delete(this._browserContextId);
this._didCloseInternal(); this._didCloseInternal();
} }

View File

@ -32,7 +32,7 @@ export class FFConnection extends platform.EventEmitter {
private _lastId: number; private _lastId: number;
private _callbacks: Map<number, {resolve: Function, reject: Function, error: Error, method: string}>; private _callbacks: Map<number, {resolve: Function, reject: Function, error: Error, method: string}>;
private _transport: ConnectionTransport; private _transport: ConnectionTransport;
private _sessions: Map<string, FFSession>; readonly _sessions: Map<string, FFSession>;
_debugProtocol: (message: string) => void = platform.debug('pw:protocol'); _debugProtocol: (message: string) => void = platform.debug('pw:protocol');
_closed: boolean; _closed: boolean;
@ -60,14 +60,6 @@ export class FFConnection extends platform.EventEmitter {
this.once = super.once; this.once = super.once;
} }
static fromSession(session: FFSession): FFConnection {
return session._connection;
}
session(sessionId: string): FFSession | null {
return this._sessions.get(sessionId) || null;
}
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]
@ -94,17 +86,6 @@ export class FFConnection extends platform.EventEmitter {
const object = JSON.parse(message); const object = JSON.parse(message);
if (object.id === kBrowserCloseMessageId) if (object.id === kBrowserCloseMessageId)
return; return;
if (object.method === 'Target.attachedToTarget') {
const sessionId = object.params.sessionId;
const session = new FFSession(this, object.params.targetInfo.type, sessionId, message => this._rawSend({...message, sessionId}));
this._sessions.set(sessionId, session);
} else if (object.method === 'Target.detachedFromTarget') {
const session = this._sessions.get(object.params.sessionId);
if (session) {
session._onClosed();
this._sessions.delete(object.params.sessionId);
}
}
if (object.sessionId) { if (object.sessionId) {
const session = this._sessions.get(object.sessionId); const session = this._sessions.get(object.sessionId);
if (session) if (session)
@ -132,7 +113,7 @@ export class FFConnection extends platform.EventEmitter {
callback.reject(rewriteError(callback.error, `Protocol error (${callback.method}): Target closed.`)); callback.reject(rewriteError(callback.error, `Protocol error (${callback.method}): Target closed.`));
this._callbacks.clear(); this._callbacks.clear();
for (const session of this._sessions.values()) for (const session of this._sessions.values())
session._onClosed(); session.dispose();
this._sessions.clear(); this._sessions.clear();
Promise.resolve().then(() => this.emit(ConnectionEvents.Disconnected)); Promise.resolve().then(() => this.emit(ConnectionEvents.Disconnected));
} }
@ -142,8 +123,10 @@ export class FFConnection extends platform.EventEmitter {
this._transport.close(); this._transport.close();
} }
getSession(sessionId: string): FFSession | null { createSession(sessionId: string, type: string): FFSession {
return this._sessions.get(sessionId) || null; const session = new FFSession(this, type, sessionId, message => this._rawSend({...message, sessionId}));
this._sessions.set(sessionId, session);
return session;
} }
} }
@ -206,11 +189,12 @@ export class FFSession extends platform.EventEmitter {
} }
} }
_onClosed() { dispose() {
for (const callback of this._callbacks.values()) for (const callback of this._callbacks.values())
callback.reject(rewriteError(callback.error, `Protocol error (${callback.method}): Target closed.`)); callback.reject(rewriteError(callback.error, `Protocol error (${callback.method}): Target closed.`));
this._callbacks.clear(); this._callbacks.clear();
this._disposed = true; this._disposed = true;
this._connection._sessions.delete(this._sessionId);
Promise.resolve().then(() => this.emit(FFSessionEvents.Disconnected)); Promise.resolve().then(() => this.emit(FFSessionEvents.Disconnected));
} }
} }

View File

@ -226,7 +226,7 @@ export class FFPage implements PageDelegate {
const worker = this._workers.get(workerId); const worker = this._workers.get(workerId);
if (!worker) if (!worker)
return; return;
worker.session._onClosed(); worker.session.dispose();
this._workers.delete(workerId); this._workers.delete(workerId);
this._page._removeWorker(workerId); this._page._removeWorker(workerId);
} }

View File

@ -65,7 +65,7 @@ export class Firefox implements BrowserType {
throw new Error('userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistent` instead'); throw new Error('userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistent` instead');
const browserServer = await this._launchServer(options, 'local'); const browserServer = await this._launchServer(options, 'local');
const browser = await platform.connectToWebsocket(browserServer.wsEndpoint()!, transport => { const browser = await platform.connectToWebsocket(browserServer.wsEndpoint()!, transport => {
return FFBrowser.connect(transport, options && options.slowMo); return FFBrowser.connect(transport, false, options && options.slowMo);
}); });
// Hack: for typical launch scenario, ensure that close waits for actual process termination. // Hack: for typical launch scenario, ensure that close waits for actual process termination.
browser.close = () => browserServer.close(); browser.close = () => browserServer.close();
@ -81,7 +81,7 @@ export class Firefox implements BrowserType {
const { timeout = 30000 } = options || {}; const { timeout = 30000 } = options || {};
const browserServer = await this._launchServer(options, 'persistent', userDataDir); const browserServer = await this._launchServer(options, 'persistent', userDataDir);
const browser = await platform.connectToWebsocket(browserServer.wsEndpoint()!, transport => { const browser = await platform.connectToWebsocket(browserServer.wsEndpoint()!, transport => {
return FFBrowser.connect(transport); return FFBrowser.connect(transport, true);
}); });
await helper.waitWithTimeout(browser._waitForTarget(t => t.type() === 'page'), 'first page', timeout); await helper.waitWithTimeout(browser._waitForTarget(t => t.type() === 'page'), 'first page', timeout);
// Hack: for typical launch scenario, ensure that close waits for actual process termination. // Hack: for typical launch scenario, ensure that close waits for actual process termination.
@ -166,7 +166,7 @@ export class Firefox implements BrowserType {
async connect(options: ConnectOptions): Promise<FFBrowser> { async connect(options: ConnectOptions): Promise<FFBrowser> {
return await platform.connectToWebsocket(options.wsEndpoint, transport => { return await platform.connectToWebsocket(options.wsEndpoint, transport => {
return FFBrowser.connect(transport, options.slowMo); return FFBrowser.connect(transport, false, options.slowMo);
}); });
} }

View File

@ -37,7 +37,7 @@ const connect = {
firefox: { firefox: {
connect: async (url: string) => { connect: async (url: string) => {
return await platform.connectToWebsocket(url, transport => { return await platform.connectToWebsocket(url, transport => {
return FirefoxBrowser.connect(transport); return FirefoxBrowser.connect(transport, false);
}); });
} }
} }