diff --git a/package.json b/package.json index a2bbf71f76..972ec55b76 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "argparse": "^2.0.1", "chai-exclude": "^2.1.0", "debug": "^4.3.4", + "mitt": "3.0.0", "puppeteer": "14.1.2", "ts-node": "^10.8.0", "tslib": "^2.4.0", @@ -59,7 +60,6 @@ "prettier": "2.6.2", "rimraf": "^3.0.2", "rollup": "^2.74.1", - "rollup-plugin-node-polyfills": "^0.2.1", "rollup-plugin-string": "^3.0.0", "rollup-plugin-terser": "^7.0.2", "sinon": "^14.0.0", diff --git a/src/bidiMapper/domains/context/browsingContextProcessor.ts b/src/bidiMapper/domains/context/browsingContextProcessor.ts index a633b51b7a..b4556578a2 100644 --- a/src/bidiMapper/domains/context/browsingContextProcessor.ts +++ b/src/bidiMapper/domains/context/browsingContextProcessor.ts @@ -68,13 +68,13 @@ export class BrowsingContextProcessor { this.#setTargetEventListeners(sessionCdpClient); - sessionCdpClient.on('event', async (method, params) => { + sessionCdpClient.on('*', async (method, params) => { await this.#eventManager.registerEvent( { method: 'cdp.eventReceived', params: { cdpMethod: method, - cdpParams: params, + cdpParams: params || {}, cdpSession: sessionId, }, }, @@ -299,10 +299,7 @@ export class BrowsingContextProcessor { eventParams: Protocol.Target.DetachedFromTargetEvent ) => { if (eventParams.targetId === commandParams.context) { - browserCdpClient.removeListener( - 'Target.detachedFromTarget', - onContextDestroyed - ); + browserCdpClient.off('Target.detachedFromTarget', onContextDestroyed); resolve(); } }; diff --git a/src/bidiMapper/rollup.config.js b/src/bidiMapper/rollup.config.js index 31b830feaf..d8529da02c 100644 --- a/src/bidiMapper/rollup.config.js +++ b/src/bidiMapper/rollup.config.js @@ -16,7 +16,6 @@ */ import typescript from '@rollup/plugin-typescript'; -import nodePolyfills from 'rollup-plugin-node-polyfills'; import json from '@rollup/plugin-json'; import {string} from 'rollup-plugin-string'; import {terser} from 'rollup-plugin-terser'; @@ -32,7 +31,6 @@ export default { }, plugins: [ json(), - nodePolyfills(), string({ include: 'src/bidiMapper/scripts/*.es', }), diff --git a/src/bidiMapper/utils/bidiServer.ts b/src/bidiMapper/utils/bidiServer.ts index b2a8840155..55b2406e74 100644 --- a/src/bidiMapper/utils/bidiServer.ts +++ b/src/bidiMapper/utils/bidiServer.ts @@ -16,7 +16,7 @@ */ import {log, LogType} from '../../utils/log'; -import {EventEmitter} from 'events'; +import {EventEmitter} from '../../utils/EventEmitter'; import {ITransport} from '../../utils/transport'; import {Message} from '../domains/protocol/bidiProtocolTypes'; @@ -36,9 +36,9 @@ export interface IBidiServer { close(): void; } -interface BidiServerEvents { +type BidiServerEvents = { message: Message.RawCommandRequest; -} +}; export class BiDiMessageEntry { readonly #message: Message.OutgoingMessage; @@ -74,19 +74,10 @@ export class BiDiMessageEntry { } } -export declare interface BidiServer { - on( - event: U, - listener: (params: BidiServerEvents[U]) => void - ): this; - - emit( - event: U, - params: BidiServerEvents[U] - ): boolean; -} - -export class BidiServer extends EventEmitter implements IBidiServer { +export class BidiServer + extends EventEmitter + implements IBidiServer +{ #messageQueue: ProcessingQueue; constructor(private _transport: ITransport) { diff --git a/src/cdp/cdpClient.spec.ts b/src/cdp/cdpClient.spec.ts index 58a83074b8..a9d5802c04 100644 --- a/src/cdp/cdpClient.spec.ts +++ b/src/cdp/cdpClient.spec.ts @@ -121,7 +121,7 @@ describe('CdpClient tests.', function () { // Register event callbacks. const genericCallback = sinon.fake(); - cdpClient.on('event', genericCallback); + cdpClient.on('*', genericCallback); const typedCallback = sinon.fake(); cdpClient.on('Target.attachedToTarget', typedCallback); @@ -146,8 +146,8 @@ describe('CdpClient tests.', function () { typedCallback.resetHistory(); // Unregister callbacks. - cdpClient.removeListener('event', genericCallback); - cdpClient.removeListener('Target.attachedToTarget', typedCallback); + cdpClient.off('*', genericCallback); + cdpClient.off('Target.attachedToTarget', typedCallback); // Send another CDP event. await mockCdpServer.emulateIncomingMessage({ diff --git a/src/cdp/cdpClient.ts b/src/cdp/cdpClient.ts index 0fb9e8a7e2..7423e4663e 100644 --- a/src/cdp/cdpClient.ts +++ b/src/cdp/cdpClient.ts @@ -15,44 +15,16 @@ * limitations under the License. */ -import {EventEmitter} from 'events'; +import {EventEmitter} from '../utils/EventEmitter'; import {CdpConnection} from './cdpConnection'; import type {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js'; -export interface CdpClient { - on( - eventName: K, - handler: (...params: ProtocolMapping.Events[K]) => void - ): EventEmitter; - on( - eventName: 'event', - handler: (method: keyof ProtocolMapping.Events, ...params: any) => void - ): EventEmitter; - removeListener( - eventName: K, - handler: (...params: ProtocolMapping.Events[K]) => void - ): EventEmitter; - removeListener( - eventName: 'event', - handler: (method: keyof ProtocolMapping.Events, ...params: any) => void - ): EventEmitter; - emit( - eventName: K, - ...args: ProtocolMapping.Events[K] - ): void; - emit( - eventName: 'event', - methodName: K, - ...args: ProtocolMapping.Events[K] - ): void; - sendCommand( - method: T, - ...params: ProtocolMapping.Commands[T]['paramsType'] - ): Promise; -} +type Mapping = { + [Property in keyof ProtocolMapping.Events]: ProtocolMapping.Events[Property][0]; +}; -class CdpClientImpl extends EventEmitter implements CdpClient { +export class CdpClient extends EventEmitter { constructor( private _cdpConnection: CdpConnection, private _sessionId: string | null @@ -84,5 +56,5 @@ export function createClient( cdpConnection: CdpConnection, sessionId: string | null ): CdpClient { - return new CdpClientImpl(cdpConnection, sessionId); + return new CdpClient(cdpConnection, sessionId); } diff --git a/src/cdp/cdpConnection.ts b/src/cdp/cdpConnection.ts index e29cebcb46..dac2891d4b 100644 --- a/src/cdp/cdpConnection.ts +++ b/src/cdp/cdpConnection.ts @@ -132,7 +132,6 @@ export class CdpConnection { : this.#browserCdpClient; if (client) { client.emit(parsed.method, parsed.params || {}); - client.emit('event', parsed.method, parsed.params || {}); } } }; diff --git a/src/mapperServer.ts b/src/mapperServer.ts index 8f312d37be..6110ce1378 100644 --- a/src/mapperServer.ts +++ b/src/mapperServer.ts @@ -158,10 +158,7 @@ export class MapperServer { try { const parsed = JSON.parse(payload); if (parsed.launched) { - mapperCdpClient.removeListener( - 'Runtime.bindingCalled', - onBindingCalled - ); + mapperCdpClient.off('Runtime.bindingCalled', onBindingCalled); resolve(); } } catch (e) { diff --git a/src/utils/EventEmitter.spec.ts b/src/utils/EventEmitter.spec.ts new file mode 100644 index 0000000000..0e59db7d4a --- /dev/null +++ b/src/utils/EventEmitter.spec.ts @@ -0,0 +1,122 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {EventEmitter} from './EventEmitter'; +import sinon from 'sinon'; +import {expect} from 'chai'; + +describe('EventEmitter', () => { + type Events = { + foo: undefined; + bar: undefined; + }; + let emitter: EventEmitter; + + beforeEach(() => { + emitter = new EventEmitter(); + }); + + describe('on', () => { + const onTests = (methodName: 'on'): void => { + it(`${methodName}: adds an event listener that is fired when the event is emitted`, () => { + const listener = sinon.spy(); + emitter[methodName]('foo', listener); + emitter.emit('foo', undefined); + expect(listener.callCount).to.equal(1); + }); + + it(`${methodName} sends the event data to the handler`, () => { + const listener = sinon.spy(); + const data = {}; + emitter[methodName]('foo', listener); + emitter.emit('foo', data); + expect(listener.callCount).to.equal(1); + expect(listener.firstCall.args[0]!).to.equal(data); + }); + + it(`${methodName}: supports chaining`, () => { + const listener = sinon.spy(); + const returnValue = emitter[methodName]('foo', listener); + expect(returnValue).to.equal(emitter); + }); + }; + onTests('on'); + }); + + describe('off', () => { + const offTests = (methodName: 'off'): void => { + it(`${methodName}: removes the listener so it is no longer called`, () => { + const listener = sinon.spy(); + emitter.on('foo', listener); + emitter.emit('foo', undefined); + expect(listener.callCount).to.equal(1); + emitter.off('foo', listener); + emitter.emit('foo', undefined); + expect(listener.callCount).to.equal(1); + }); + + it(`${methodName}: supports chaining`, () => { + const listener = sinon.spy(); + emitter.on('foo', listener); + const returnValue = emitter.off('foo', listener); + expect(returnValue).to.equal(emitter); + }); + }; + offTests('off'); + }); + + describe('once', () => { + it('only calls the listener once and then removes it', () => { + const listener = sinon.spy(); + emitter.once('foo', listener); + emitter.emit('foo', undefined); + expect(listener.callCount).to.equal(1); + emitter.emit('foo', undefined); + expect(listener.callCount).to.equal(1); + }); + + it('supports chaining', () => { + const listener = sinon.spy(); + const returnValue = emitter.once('foo', listener); + expect(returnValue).to.equal(emitter); + }); + }); + + describe('emit', () => { + it('calls all the listeners for an event', () => { + const listener1 = sinon.spy(); + const listener2 = sinon.spy(); + const listener3 = sinon.spy(); + emitter.on('foo', listener1).on('foo', listener2).on('bar', listener3); + + emitter.emit('foo', undefined); + + expect(listener1.callCount).to.equal(1); + expect(listener2.callCount).to.equal(1); + expect(listener3.callCount).to.equal(0); + }); + + it('passes data through to the listener', () => { + const listener = sinon.spy(); + emitter.on('foo', listener); + const data = {}; + + emitter.emit('foo', data); + expect(listener.callCount).to.equal(1); + expect(listener.firstCall.args[0]!).to.equal(data); + }); + }); +}); diff --git a/src/utils/EventEmitter.ts b/src/utils/EventEmitter.ts new file mode 100644 index 0000000000..1c38fa3b85 --- /dev/null +++ b/src/utils/EventEmitter.ts @@ -0,0 +1,80 @@ +/** + * Copyright 2022 Google LLC. + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import mitt, {Emitter, EventType, Handler, WildcardHandler} from 'mitt'; + +export class EventEmitter> { + #emitter: Emitter = mitt(); + + /** + * Bind an event listener to fire when an event occurs. + * @param event - the event type you'd like to listen to. Can be a string or symbol. + * @param handler - the function to be called when the event occurs. + * @returns `this` to enable you to chain method calls. + */ + on(type: '*', handler: WildcardHandler): EventEmitter; + on( + type: Key, + handler: Handler + ): EventEmitter; + on(type: any, handler: any): EventEmitter { + this.#emitter.on(type, handler); + return this; + } + + /** + * Like `on` but the listener will only be fired once and then it will be removed. + * @param event - the event you'd like to listen to + * @param handler - the handler function to run when the event occurs + * @returns `this` to enable you to chain method calls. + */ + once(event: EventType, handler: Handler): EventEmitter { + const onceHandler: Handler = (eventData) => { + handler(eventData); + this.off(event, onceHandler); + }; + + return this.on(event, onceHandler); + } + + /** + * Remove an event listener from firing. + * @param event - the event type you'd like to stop listening to. + * @param handler - the function that should be removed. + * @returns `this` to enable you to chain method calls. + */ + off(type: '*', handler: WildcardHandler): EventEmitter; + off( + type: Key, + handler: Handler + ): EventEmitter; + off(type: any, handler: any): EventEmitter { + this.#emitter.off(type, handler); + return this; + } + + /** + * Emit an event and call any associated listeners. + * + * @param event - the event you'd like to emit + * @param eventData - any data you'd like to emit with the event + * @returns `true` if there are any listeners, `false` if there are not. + */ + emit(event: EventType, eventData: Events[EventType]): void { + this.#emitter.emit(event, eventData); + } +}