feat(firefox): support workers (#532)
This commit is contained in:
parent
bb3f12245c
commit
d64c38b586
|
|
@ -9,7 +9,7 @@
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"playwright": {
|
"playwright": {
|
||||||
"chromium_revision": "724623",
|
"chromium_revision": "724623",
|
||||||
"firefox_revision": "1014",
|
"firefox_revision": "1016",
|
||||||
"webkit_revision": "1099"
|
"webkit_revision": "1099"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ export class FFConnection extends platform.EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromSession(session: FFSession): FFConnection {
|
static fromSession(session: FFSession): FFConnection {
|
||||||
return session._connection!;
|
return session._connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
session(sessionId: string): FFSession | null {
|
session(sessionId: string): FFSession | null {
|
||||||
|
|
@ -69,18 +69,21 @@ export class FFConnection extends platform.EventEmitter {
|
||||||
method: T,
|
method: T,
|
||||||
params?: Protocol.CommandParameters[T]
|
params?: Protocol.CommandParameters[T]
|
||||||
): Promise<Protocol.CommandReturnValues[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) => {
|
return new Promise((resolve, reject) => {
|
||||||
this._callbacks.set(id, {resolve, reject, error: new Error(), method});
|
this._callbacks.set(id, {resolve, reject, error: new Error(), method});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_rawSend(message: any): number {
|
nextMessageId(): number {
|
||||||
const id = ++this._lastId;
|
return ++this._lastId;
|
||||||
message = JSON.stringify(Object.assign({}, message, {id}));
|
}
|
||||||
|
|
||||||
|
_rawSend(message: any) {
|
||||||
|
message = JSON.stringify(message);
|
||||||
debugProtocol('SEND ► ' + message);
|
debugProtocol('SEND ► ' + message);
|
||||||
this._transport.send(message);
|
this._transport.send(message);
|
||||||
return id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async _onMessage(message: string) {
|
async _onMessage(message: string) {
|
||||||
|
|
@ -88,7 +91,7 @@ export class FFConnection extends platform.EventEmitter {
|
||||||
const object = JSON.parse(message);
|
const object = JSON.parse(message);
|
||||||
if (object.method === 'Target.attachedToTarget') {
|
if (object.method === 'Target.attachedToTarget') {
|
||||||
const sessionId = object.params.sessionId;
|
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);
|
this._sessions.set(sessionId, session);
|
||||||
} else if (object.method === 'Browser.detachedFromTarget') {
|
} else if (object.method === 'Browser.detachedFromTarget') {
|
||||||
const session = this._sessions.get(object.params.sessionId);
|
const session = this._sessions.get(object.params.sessionId);
|
||||||
|
|
@ -100,7 +103,7 @@ export class FFConnection extends platform.EventEmitter {
|
||||||
if (object.sessionId) {
|
if (object.sessionId) {
|
||||||
const session = this._sessions.get(object.sessionId);
|
const session = this._sessions.get(object.sessionId);
|
||||||
if (session)
|
if (session)
|
||||||
session._onMessage(object);
|
session.dispatchMessage(object);
|
||||||
} else if (object.id) {
|
} else if (object.id) {
|
||||||
const callback = this._callbacks.get(object.id);
|
const callback = this._callbacks.get(object.id);
|
||||||
// Callbacks could be all rejected if someone has called `.dispose()`.
|
// Callbacks could be all rejected if someone has called `.dispose()`.
|
||||||
|
|
@ -147,22 +150,25 @@ export const FFSessionEvents = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export class FFSession extends platform.EventEmitter {
|
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 _callbacks: Map<number, {resolve: Function, reject: Function, error: Error, method: string}>;
|
||||||
private _targetType: string;
|
private _targetType: string;
|
||||||
private _sessionId: 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;
|
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;
|
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;
|
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;
|
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;
|
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();
|
super();
|
||||||
this._callbacks = new Map();
|
this._callbacks = new Map();
|
||||||
this._connection = connection;
|
this._connection = connection;
|
||||||
this._targetType = targetType;
|
this._targetType = targetType;
|
||||||
this._sessionId = sessionId;
|
this._sessionId = sessionId;
|
||||||
|
this._rawSend = rawSend;
|
||||||
|
|
||||||
this.on = super.on;
|
this.on = super.on;
|
||||||
this.addListener = super.addListener;
|
this.addListener = super.addListener;
|
||||||
|
|
@ -175,15 +181,16 @@ export class FFSession extends platform.EventEmitter {
|
||||||
method: T,
|
method: T,
|
||||||
params?: Protocol.CommandParameters[T]
|
params?: Protocol.CommandParameters[T]
|
||||||
): Promise<Protocol.CommandReturnValues[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.`));
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
this._callbacks.set(id, {resolve, reject, error: new Error(), method});
|
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)) {
|
if (object.id && this._callbacks.has(object.id)) {
|
||||||
const callback = this._callbacks.get(object.id)!;
|
const callback = this._callbacks.get(object.id)!;
|
||||||
this._callbacks.delete(object.id);
|
this._callbacks.delete(object.id);
|
||||||
|
|
@ -201,7 +208,7 @@ export class FFSession extends platform.EventEmitter {
|
||||||
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._connection = null;
|
this._disposed = true;
|
||||||
Promise.resolve().then(() => this.emit(FFSessionEvents.Disconnected));
|
Promise.resolve().then(() => this.emit(FFSessionEvents.Disconnected));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import { helper, RegisteredListener, debugError } from '../helper';
|
||||||
import * as dom from '../dom';
|
import * as dom from '../dom';
|
||||||
import { FFSession } from './ffConnection';
|
import { FFSession } from './ffConnection';
|
||||||
import { FFExecutionContext } from './ffExecutionContext';
|
import { FFExecutionContext } from './ffExecutionContext';
|
||||||
import { Page, PageDelegate, Coverage } from '../page';
|
import { Page, PageDelegate, Coverage, Worker } from '../page';
|
||||||
import { FFNetworkManager } from './ffNetworkManager';
|
import { FFNetworkManager } from './ffNetworkManager';
|
||||||
import { Events } from '../events';
|
import { Events } from '../events';
|
||||||
import * as dialog from '../dialog';
|
import * as dialog from '../dialog';
|
||||||
|
|
@ -43,6 +43,7 @@ export class FFPage implements PageDelegate {
|
||||||
readonly _networkManager: FFNetworkManager;
|
readonly _networkManager: FFNetworkManager;
|
||||||
private readonly _contextIdToContext: Map<string, dom.FrameExecutionContext>;
|
private readonly _contextIdToContext: Map<string, dom.FrameExecutionContext>;
|
||||||
private _eventListeners: RegisteredListener[];
|
private _eventListeners: RegisteredListener[];
|
||||||
|
private _workers = new Map<string, { frameId: string, session: FFSession }>();
|
||||||
|
|
||||||
constructor(session: FFSession, browserContext: BrowserContext) {
|
constructor(session: FFSession, browserContext: BrowserContext) {
|
||||||
this._session = session;
|
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.dialogOpened', this._onDialogOpened.bind(this)),
|
||||||
helper.addEventListener(this._session, 'Page.bindingCalled', this._onBindingCalled.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.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) {
|
_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);
|
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);
|
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> {
|
async exposeBinding(name: string, bindingFunction: string): Promise<void> {
|
||||||
await this._session.send('Page.addBinding', {name: name});
|
await this._session.send('Page.addBinding', {name: name});
|
||||||
await this._session.send('Page.addScriptToEvaluateOnNewDocument', {script: bindingFunction});
|
await this._session.send('Page.addScriptToEvaluateOnNewDocument', {script: bindingFunction});
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
|
||||||
const {it, fit, xit, dit} = testRunner;
|
const {it, fit, xit, dit} = testRunner;
|
||||||
const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;
|
const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;
|
||||||
|
|
||||||
describe.skip(FFOX)('Workers', function() {
|
describe('Workers', function() {
|
||||||
it('Page.workers', async function({page, server}) {
|
it('Page.workers', async function({page, server}) {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
page.waitForEvent('workercreated'),
|
page.waitForEvent('workercreated'),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue