From 52b12cf79ce2cd53b0f8f8b81796691e570bb532 Mon Sep 17 00:00:00 2001 From: Patrick Hulce Date: Wed, 27 Jan 2021 11:36:22 -0600 Subject: [PATCH] core(fr): add session.onAnyProtocolMessage listener (#11995) --- lighthouse-core/fraggle-rock/gather/driver.js | 1 + .../fraggle-rock/gather/session.js | 22 ++++++ lighthouse-core/gather/driver.js | 8 +++ .../test/fraggle-rock/gather/driver-test.js | 2 +- .../test/fraggle-rock/gather/session-test.js | 68 ++++++++++++++++++- types/gatherer.d.ts | 1 + 6 files changed, 100 insertions(+), 2 deletions(-) diff --git a/lighthouse-core/fraggle-rock/gather/driver.js b/lighthouse-core/fraggle-rock/gather/driver.js index 04d69fb0f095..487fe81cdfc8 100644 --- a/lighthouse-core/fraggle-rock/gather/driver.js +++ b/lighthouse-core/fraggle-rock/gather/driver.js @@ -19,6 +19,7 @@ const defaultSession = { setNextProtocolTimeout: throwNotConnectedFn, on: throwNotConnectedFn, once: throwNotConnectedFn, + onAnyProtocolMessage: throwNotConnectedFn, off: throwNotConnectedFn, sendCommand: throwNotConnectedFn, }; diff --git a/lighthouse-core/fraggle-rock/gather/session.js b/lighthouse-core/fraggle-rock/gather/session.js index 030dcb6575d9..dd15f953546a 100644 --- a/lighthouse-core/fraggle-rock/gather/session.js +++ b/lighthouse-core/fraggle-rock/gather/session.js @@ -5,6 +5,8 @@ */ 'use strict'; +const SessionEmitMonkeypatch = Symbol('monkeypatch'); + /** @implements {LH.Gatherer.FRProtocolSession} */ class ProtocolSession { /** @@ -12,6 +14,18 @@ class ProtocolSession { */ constructor(session) { this._session = session; + + // FIXME: Monkeypatch puppeteer to be able to listen to *all* protocol events. + // This patched method will now emit a copy of every event on `*`. + const originalEmit = session.emit; + // @ts-expect-error - Test for the monkeypatch. + if (originalEmit[SessionEmitMonkeypatch]) return; + session.emit = (method, ...params) => { + originalEmit.call(session, '*', {method, params}); + return originalEmit.call(session, method, ...params); + }; + // @ts-expect-error - It's monkeypatching 🤷‍♂️. + session.emit[SessionEmitMonkeypatch] = true; } /** @@ -55,6 +69,14 @@ class ProtocolSession { this._session.once(eventName, /** @type {*} */ (callback)); } + /** + * Bind to our custom event that fires for *any* protocol event. + * @param {(payload: LH.Protocol.RawEventMessage) => void} callback + */ + onAnyProtocolMessage(callback) { + this._session.on('*', /** @type {*} */ (callback)); + } + /** * Bind listeners for protocol events. * @template {keyof LH.CrdpEvents} E diff --git a/lighthouse-core/gather/driver.js b/lighthouse-core/gather/driver.js index 220feaa22a4f..77b8442f9c25 100644 --- a/lighthouse-core/gather/driver.js +++ b/lighthouse-core/gather/driver.js @@ -276,6 +276,14 @@ class Driver { this._eventEmitter.removeListener(eventName, cb); } + /** + * Bind to *any* protocol event. + * @param {(payload: LH.Protocol.RawEventMessage) => void} callback + */ + onAnyProtocolMessage(callback) { + this._connection.on('protocolevent', callback); + } + /** * Debounce enabling or disabling domains to prevent driver users from * stomping on each other. Maintains an internal count of the times a domain diff --git a/lighthouse-core/test/fraggle-rock/gather/driver-test.js b/lighthouse-core/test/fraggle-rock/gather/driver-test.js index 9a820d2ec1ee..cb92c02e4e38 100644 --- a/lighthouse-core/test/fraggle-rock/gather/driver-test.js +++ b/lighthouse-core/test/fraggle-rock/gather/driver-test.js @@ -34,7 +34,7 @@ beforeEach(() => { // @ts-expect-error - Individual mock functions are applied as necessary. pageTarget = {createCDPSession: () => puppeteerSession}; // @ts-expect-error - Individual mock functions are applied as necessary. - puppeteerSession = {on: jest.fn(), off: jest.fn(), send: jest.fn()}; + puppeteerSession = {on: jest.fn(), off: jest.fn(), send: jest.fn(), emit: jest.fn()}; driver = new Driver(page); }); diff --git a/lighthouse-core/test/fraggle-rock/gather/session-test.js b/lighthouse-core/test/fraggle-rock/gather/session-test.js index 94023b99d900..f336f2c6b18a 100644 --- a/lighthouse-core/test/fraggle-rock/gather/session-test.js +++ b/lighthouse-core/test/fraggle-rock/gather/session-test.js @@ -5,6 +5,7 @@ */ 'use strict'; +const {EventEmitter} = require('events'); const ProtocolSession = require('../../../fraggle-rock/gather/session.js'); /* eslint-env jest */ @@ -17,10 +18,49 @@ describe('ProtocolSession', () => { beforeEach(() => { // @ts-expect-error - Individual mock functions are applied as necessary. - puppeteerSession = {}; + puppeteerSession = {emit: jest.fn()}; session = new ProtocolSession(puppeteerSession); }); + describe('ProtocolSession', () => { + it('should emit a copy of events on "*"', () => { + // @ts-expect-error - we want to use a more limited test of a real event emitter. + puppeteerSession = new EventEmitter(); + session = new ProtocolSession(puppeteerSession); + + const regularListener = jest.fn(); + const allListener = jest.fn(); + + puppeteerSession.on('Foo', regularListener); + puppeteerSession.on('*', allListener); + puppeteerSession.emit('Foo', 1, 2, 3); + puppeteerSession.emit('Bar', 1, 2, 3); + + expect(regularListener).toHaveBeenCalledTimes(1); + expect(allListener).toHaveBeenCalledTimes(2); + expect(allListener).toHaveBeenCalledWith({method: 'Foo', params: [1, 2, 3]}); + expect(allListener).toHaveBeenCalledWith({method: 'Bar', params: [1, 2, 3]}); + }); + + it('should not fire duplicate events', () => { + // @ts-expect-error - we want to use a more limited test of a real event emitter. + puppeteerSession = new EventEmitter(); + session = new ProtocolSession(puppeteerSession); + session = new ProtocolSession(puppeteerSession); + + const regularListener = jest.fn(); + const allListener = jest.fn(); + + puppeteerSession.on('Foo', regularListener); + puppeteerSession.on('*', allListener); + puppeteerSession.emit('Foo', 1, 2, 3); + puppeteerSession.emit('Bar', 1, 2, 3); + + expect(regularListener).toHaveBeenCalledTimes(1); + expect(allListener).toHaveBeenCalledTimes(2); + }); + }); + /** @type {Array<'on'|'off'|'once'>} */ const delegateMethods = ['on', 'once', 'off']; for (const method of delegateMethods) { @@ -35,6 +75,32 @@ describe('ProtocolSession', () => { }); } + describe('.onAnyProtocolMessage', () => { + it('should listen for any event', () => { + // @ts-expect-error - we want to use a more limited test of a real event emitter. + puppeteerSession = new EventEmitter(); + session = new ProtocolSession(puppeteerSession); + + const regularListener = jest.fn(); + const allListener = jest.fn(); + + session.on('Page.frameNavigated', regularListener); + session.onAnyProtocolMessage(allListener); + + puppeteerSession.emit('Page.frameNavigated'); + puppeteerSession.emit('Debugger.scriptParsed', {script: 'details'}); + + expect(regularListener).toHaveBeenCalledTimes(1); + expect(regularListener).toHaveBeenCalledWith(); + expect(allListener).toHaveBeenCalledTimes(2); + expect(allListener).toHaveBeenCalledWith({method: 'Page.frameNavigated', params: []}); + expect(allListener).toHaveBeenCalledWith({ + method: 'Debugger.scriptParsed', + params: [{script: 'details'}], + }); + }); + }); + describe('.sendCommand', () => { it('delegates to puppeteer', async () => { const send = puppeteerSession.send = jest.fn().mockResolvedValue(123); diff --git a/types/gatherer.d.ts b/types/gatherer.d.ts index ea1a71664803..9fc0dd197999 100644 --- a/types/gatherer.d.ts +++ b/types/gatherer.d.ts @@ -18,6 +18,7 @@ declare global { setNextProtocolTimeout(ms: number): void; on(event: TEvent, callback: (...args: LH.CrdpEvents[TEvent]) => void): void; once(event: TEvent, callback: (...args: LH.CrdpEvents[TEvent]) => void): void; + onAnyProtocolMessage(callback: (payload: LH.Protocol.RawEventMessage) => void): void off(event: TEvent, callback: (...args: LH.CrdpEvents[TEvent]) => void): void; sendCommand(method: TMethod, ...params: LH.CrdpCommands[TMethod]['paramsType']): Promise; }