feat(firefox): support workers (#532)

This commit is contained in:
Dmitry Gozman 2020-01-17 17:51:02 -08:00 committed by Andrey Lushnikov
parent bb3f12245c
commit d64c38b586
4 changed files with 74 additions and 17 deletions

View file

@ -9,7 +9,7 @@
"main": "index.js",
"playwright": {
"chromium_revision": "724623",
"firefox_revision": "1014",
"firefox_revision": "1016",
"webkit_revision": "1099"
},
"scripts": {

View file

@ -58,7 +58,7 @@ export class FFConnection extends platform.EventEmitter {
}
static fromSession(session: FFSession): FFConnection {
return session._connection!;
return session._connection;
}
session(sessionId: string): FFSession | null {
@ -69,18 +69,21 @@ export class FFConnection extends platform.EventEmitter {
method: T,
params?: Protocol.CommandParameters[T]
): Promise<Protocol.CommandReturnValues[T]> {
const id = this._rawSend({method, params});
const id = this.nextMessageId();
this._rawSend({id, method, params});
return new Promise((resolve, reject) => {
this._callbacks.set(id, {resolve, reject, error: new Error(), method});
});
}
_rawSend(message: any): number {
const id = ++this._lastId;
message = JSON.stringify(Object.assign({}, message, {id}));
nextMessageId(): number {
return ++this._lastId;
}
_rawSend(message: any) {
message = JSON.stringify(message);
debugProtocol('SEND ► ' + message);
this._transport.send(message);
return id;
}
async _onMessage(message: string) {
@ -88,7 +91,7 @@ export class FFConnection extends platform.EventEmitter {
const object = JSON.parse(message);
if (object.method === 'Target.attachedToTarget') {
const sessionId = object.params.sessionId;
const session = new FFSession(this, object.params.targetInfo.type, 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 === 'Browser.detachedFromTarget') {
const session = this._sessions.get(object.params.sessionId);
@ -100,7 +103,7 @@ export class FFConnection extends platform.EventEmitter {
if (object.sessionId) {
const session = this._sessions.get(object.sessionId);
if (session)
session._onMessage(object);
session.dispatchMessage(object);
} else if (object.id) {
const callback = this._callbacks.get(object.id);
// Callbacks could be all rejected if someone has called `.dispose()`.
@ -147,22 +150,25 @@ export const FFSessionEvents = {
};
export class FFSession extends platform.EventEmitter {
_connection: FFConnection | null;
_connection: FFConnection;
_disposed = false;
private _callbacks: Map<number, {resolve: Function, reject: Function, error: Error, method: string}>;
private _targetType: string;
private _sessionId: string;
private _rawSend: (message: any) => void;
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;
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;
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;
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;
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(connection: FFConnection, targetType: string, sessionId: string) {
constructor(connection: FFConnection, targetType: string, sessionId: string, rawSend: (message: any) => void) {
super();
this._callbacks = new Map();
this._connection = connection;
this._targetType = targetType;
this._sessionId = sessionId;
this._rawSend = rawSend;
this.on = super.on;
this.addListener = super.addListener;
@ -175,15 +181,16 @@ export class FFSession extends platform.EventEmitter {
method: T,
params?: Protocol.CommandParameters[T]
): Promise<Protocol.CommandReturnValues[T]> {
if (!this._connection)
if (this._disposed)
return Promise.reject(new Error(`Protocol error (${method}): Session closed. Most likely the ${this._targetType} has been closed.`));
const id = this._connection._rawSend({sessionId: this._sessionId, method, params});
const id = this._connection.nextMessageId();
this._rawSend({method, params, id});
return new Promise((resolve, reject) => {
this._callbacks.set(id, {resolve, reject, error: new Error(), method});
});
}
_onMessage(object: { id?: number; method: string; params: object; error: { message: string; data: any; }; result?: any; }) {
dispatchMessage(object: { id?: number; method: string; params: object; error: { message: string; data: any; }; result?: any; }) {
if (object.id && this._callbacks.has(object.id)) {
const callback = this._callbacks.get(object.id)!;
this._callbacks.delete(object.id);
@ -201,7 +208,7 @@ export class FFSession extends platform.EventEmitter {
for (const callback of this._callbacks.values())
callback.reject(rewriteError(callback.error, `Protocol error (${callback.method}): Target closed.`));
this._callbacks.clear();
this._connection = null;
this._disposed = true;
Promise.resolve().then(() => this.emit(FFSessionEvents.Disconnected));
}
}

View file

@ -20,7 +20,7 @@ import { helper, RegisteredListener, debugError } from '../helper';
import * as dom from '../dom';
import { FFSession } from './ffConnection';
import { FFExecutionContext } from './ffExecutionContext';
import { Page, PageDelegate, Coverage } from '../page';
import { Page, PageDelegate, Coverage, Worker } from '../page';
import { FFNetworkManager } from './ffNetworkManager';
import { Events } from '../events';
import * as dialog from '../dialog';
@ -43,6 +43,7 @@ export class FFPage implements PageDelegate {
readonly _networkManager: FFNetworkManager;
private readonly _contextIdToContext: Map<string, dom.FrameExecutionContext>;
private _eventListeners: RegisteredListener[];
private _workers = new Map<string, { frameId: string, session: FFSession }>();
constructor(session: FFSession, browserContext: BrowserContext) {
this._session = session;
@ -66,6 +67,9 @@ export class FFPage implements PageDelegate {
helper.addEventListener(this._session, 'Page.dialogOpened', this._onDialogOpened.bind(this)),
helper.addEventListener(this._session, 'Page.bindingCalled', this._onBindingCalled.bind(this)),
helper.addEventListener(this._session, 'Page.fileChooserOpened', this._onFileChooserOpened.bind(this)),
helper.addEventListener(this._session, 'Page.workerCreated', this._onWorkerCreated.bind(this)),
helper.addEventListener(this._session, 'Page.workerDestroyed', this._onWorkerDestroyed.bind(this)),
helper.addEventListener(this._session, 'Page.dispatchMessageFromWorker', this._onDispatchMessageFromWorker.bind(this)),
];
}
@ -128,6 +132,10 @@ export class FFPage implements PageDelegate {
}
_onNavigationCommitted(params: Protocol.Page.navigationCommittedPayload) {
for (const [workerId, worker] of this._workers) {
if (worker.frameId === params.frameId)
this._onWorkerDestroyed({ workerId });
}
this._page._frameManager.frameCommittedNewDocumentNavigation(params.frameId, params.url, params.name || '', params.navigationId || '', false);
}
@ -185,6 +193,48 @@ export class FFPage implements PageDelegate {
this._page._onFileChooserOpened(handle);
}
async _onWorkerCreated(event: Protocol.Page.workerCreatedPayload) {
const workerId = event.workerId;
const worker = new Worker(event.url);
const workerSession = new FFSession(this._session._connection, 'worker', workerId, (message: any) => {
this._session.send('Page.sendMessageToWorker', {
frameId: event.frameId,
workerId: workerId,
message: JSON.stringify(message)
}).catch(e => {
workerSession.dispatchMessage({ id: message.id, method: '', params: {}, error: { message: e.message, data: undefined } });
});
});
this._workers.set(workerId, { session: workerSession, frameId: event.frameId });
this._page._addWorker(workerId, worker);
workerSession.once('Runtime.executionContextCreated', event => {
worker._createExecutionContext(new FFExecutionContext(workerSession, event.executionContextId));
});
workerSession.on('Runtime.console', event => {
const {type, args, location} = event;
const context = worker._existingExecutionContext!;
this._page._addConsoleMessage(type, args.map(arg => context._createHandle(arg)), location);
});
// Note: we receive worker exceptions directly from the page.
}
async _onWorkerDestroyed(event: Protocol.Page.workerDestroyedPayload) {
const workerId = event.workerId;
const worker = this._workers.get(workerId);
if (!worker)
return;
worker.session._onClosed();
this._workers.delete(workerId);
this._page._removeWorker(workerId);
}
async _onDispatchMessageFromWorker(event: Protocol.Page.dispatchMessageFromWorkerPayload) {
const worker = this._workers.get(event.workerId);
if (!worker)
return;
worker.session.dispatchMessage(JSON.parse(event.message));
}
async exposeBinding(name: string, bindingFunction: string): Promise<void> {
await this._session.send('Page.addBinding', {name: name});
await this._session.send('Page.addScriptToEvaluateOnNewDocument', {script: bindingFunction});

View file

@ -23,7 +23,7 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
const {it, fit, xit, dit} = testRunner;
const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;
describe.skip(FFOX)('Workers', function() {
describe('Workers', function() {
it('Page.workers', async function({page, server}) {
await Promise.all([
page.waitForEvent('workercreated'),