Skip to content

Commit

Permalink
core(fr): add session.onAnyProtocolMessage listener (#11995)
Browse files Browse the repository at this point in the history
  • Loading branch information
patrickhulce authored Jan 27, 2021
1 parent 36e666c commit 52b12cf
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 2 deletions.
1 change: 1 addition & 0 deletions lighthouse-core/fraggle-rock/gather/driver.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const defaultSession = {
setNextProtocolTimeout: throwNotConnectedFn,
on: throwNotConnectedFn,
once: throwNotConnectedFn,
onAnyProtocolMessage: throwNotConnectedFn,
off: throwNotConnectedFn,
sendCommand: throwNotConnectedFn,
};
Expand Down
22 changes: 22 additions & 0 deletions lighthouse-core/fraggle-rock/gather/session.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,27 @@
*/
'use strict';

const SessionEmitMonkeypatch = Symbol('monkeypatch');

/** @implements {LH.Gatherer.FRProtocolSession} */
class ProtocolSession {
/**
* @param {import('puppeteer').CDPSession} session
*/
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;
}

/**
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions lighthouse-core/gather/driver.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lighthouse-core/test/fraggle-rock/gather/driver-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand Down
68 changes: 67 additions & 1 deletion lighthouse-core/test/fraggle-rock/gather/session-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/
'use strict';

const {EventEmitter} = require('events');
const ProtocolSession = require('../../../fraggle-rock/gather/session.js');

/* eslint-env jest */
Expand All @@ -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) {
Expand All @@ -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);
Expand Down
1 change: 1 addition & 0 deletions types/gatherer.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ declare global {
setNextProtocolTimeout(ms: number): void;
on<TEvent extends keyof LH.CrdpEvents>(event: TEvent, callback: (...args: LH.CrdpEvents[TEvent]) => void): void;
once<TEvent extends keyof LH.CrdpEvents>(event: TEvent, callback: (...args: LH.CrdpEvents[TEvent]) => void): void;
onAnyProtocolMessage(callback: (payload: LH.Protocol.RawEventMessage) => void): void
off<TEvent extends keyof LH.CrdpEvents>(event: TEvent, callback: (...args: LH.CrdpEvents[TEvent]) => void): void;
sendCommand<TMethod extends keyof LH.CrdpCommands>(method: TMethod, ...params: LH.CrdpCommands[TMethod]['paramsType']): Promise<LH.CrdpCommands[TMethod]['returnType']>;
}
Expand Down

0 comments on commit 52b12cf

Please sign in to comment.