From d64c38b586a460941b5264ce66049de821fb0038 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Fri, 17 Jan 2020 17:51:02 -0800 Subject: [PATCH] feat(firefox): support workers (#532) --- package.json | 2 +- src/firefox/ffConnection.ts | 35 +++++++++++++++---------- src/firefox/ffPage.ts | 52 ++++++++++++++++++++++++++++++++++++- test/workers.spec.js | 2 +- 4 files changed, 74 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 559f4f95a4..a52f12b684 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "main": "index.js", "playwright": { "chromium_revision": "724623", - "firefox_revision": "1014", + "firefox_revision": "1016", "webkit_revision": "1099" }, "scripts": { diff --git a/src/firefox/ffConnection.ts b/src/firefox/ffConnection.ts index 2016b6a4a8..6cb4c9fac4 100644 --- a/src/firefox/ffConnection.ts +++ b/src/firefox/ffConnection.ts @@ -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 { - 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; private _targetType: string; private _sessionId: string; + private _rawSend: (message: any) => void; on: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; addListener: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; off: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; removeListener: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; once: (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 { - 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)); } } diff --git a/src/firefox/ffPage.ts b/src/firefox/ffPage.ts index aa9bd86833..32c836bf79 100644 --- a/src/firefox/ffPage.ts +++ b/src/firefox/ffPage.ts @@ -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; private _eventListeners: RegisteredListener[]; + private _workers = new Map(); 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 { await this._session.send('Page.addBinding', {name: name}); await this._session.send('Page.addScriptToEvaluateOnNewDocument', {script: bindingFunction}); diff --git a/test/workers.spec.js b/test/workers.spec.js index 6498980eb0..370a12fd5b 100644 --- a/test/workers.spec.js +++ b/test/workers.spec.js @@ -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'),