This commit is contained in:
Connor Clark 2020-01-10 14:45:39 -08:00 коммит произвёл GitHub
Родитель 5870cb4d7c
Коммит 8a66059ea5
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
6 изменённых файлов: 300 добавлений и 143 удалений

Просмотреть файл

@ -8,11 +8,11 @@
const Driver = require('../../gather/driver.js');
const Connection = require('../../gather/connections/connection.js');
const LHElement = require('../../lib/lh-element.js');
const EventEmitter = require('events').EventEmitter;
const {protocolGetVersionResponse} = require('./fake-driver.js');
const {createMockSendCommandFn, createMockOnceFn} = require('./mock-commands.js');
const redirectDevtoolsLog = require('../fixtures/wikipedia-redirect.devtoolslog.json');
const redirectDevtoolsLog = /** @type {LH.Protocol.RawEventMessage[]} */ (
require('../fixtures/wikipedia-redirect.devtoolslog.json'));
/* eslint-env jest */
@ -23,12 +23,13 @@ jest.useFakeTimers();
*
* @template T
* @param {Promise<T>} promise
* @return {Promise<T> & {isDone: () => boolean, isResolved: () => boolean, isRejected: () => boolean}}
*/
function makePromiseInspectable(promise) {
let isResolved = false;
let isRejected = false;
/** @type {T=} */
let resolvedValue = undefined;
/** @type {any=} */
let rejectionError = undefined;
const inspectablePromise = promise.then(value => {
isResolved = true;
@ -40,12 +41,51 @@ function makePromiseInspectable(promise) {
throw err;
});
inspectablePromise.isDone = () => isResolved || isRejected;
inspectablePromise.isResolved = () => isResolved;
inspectablePromise.isRejected = () => isRejected;
inspectablePromise.getDebugValues = () => ({resolvedValue, rejectionError});
return Object.assign(inspectablePromise, {
isDone() {
return isResolved || isRejected;
},
isResolved() {
return isResolved;
},
isRejected() {
return isRejected;
},
getDebugValues() {
return {resolvedValue, rejectionError};
},
});
}
return inspectablePromise;
function createDecomposedPromise() {
/** @type {Function} */
let resolve;
/** @type {Function} */
let reject;
const promise = new Promise((r1, r2) => {
resolve = r1;
reject = r2;
});
// @ts-ignore: Ignore 'unused' error.
return {promise, resolve, reject};
}
function createMockWaitForFn() {
const {promise, resolve, reject} = createDecomposedPromise();
const mockCancelFn = jest.fn();
const mockFn = jest.fn().mockReturnValue({promise, cancel: mockCancelFn});
return Object.assign(mockFn, {
mockResolve: resolve,
/** @param {Error=} err */
mockReject(err) {
reject(err || new Error('Rejected'));
},
getMockCancelFn() {
return mockCancelFn;
},
});
}
expect.extend({
@ -53,7 +93,7 @@ expect.extend({
* Asserts that an inspectable promise created by makePromiseInspectable is currently resolved or rejected.
* This is useful for situations where we want to test that we are actually waiting for a particular event.
*
* @param {ReturnType<makePromiseInspectable>} received
* @param {ReturnType<typeof makePromiseInspectable>} received
* @param {string} failureMessage
*/
toBeDone(received, failureMessage) {
@ -81,14 +121,33 @@ async function flushAllTimersAndMicrotasks(ms = 1000) {
}
}
/**
* @typedef DriverMockMethods
* @property {ReturnType<typeof createMockOnceFn>} on
* @property {ReturnType<typeof createMockOnceFn>} once
* @property {ReturnType<typeof createMockWaitForFn>} _waitForFCP
* @property {ReturnType<typeof createMockWaitForFn>} _waitForLoadEvent
* @property {ReturnType<typeof createMockWaitForFn>} _waitForNetworkIdle
* @property {ReturnType<typeof createMockWaitForFn>} _waitForCPUIdle
* @property {(...args: RecursivePartial<Parameters<Driver['gotoURL']>>) => ReturnType<Driver['gotoURL']>} gotoURL
* @property {(...args: RecursivePartial<Parameters<Driver['goOnline']>>) => ReturnType<Driver['goOnline']>} goOnline
*/
/** @typedef {Driver & DriverMockMethods} TestDriver */
/** @type {TestDriver} */
let driver;
/** @type {Connection & {sendCommand: ReturnType<typeof createMockSendCommandFn>}} */
let connectionStub;
beforeEach(() => {
// @ts-ignore - connectionStub has a mocked version of sendCommand implemented in each test
connectionStub = new Connection();
// @ts-ignore
connectionStub.sendCommand = cmd => {
throw new Error(`${cmd} not implemented`);
};
// @ts-ignore - driver has a mocked version of on/once implemented in each test
driver = new Driver(connectionStub);
});
@ -104,7 +163,7 @@ describe('.querySelector(All)', () => {
it('returns element instance when DOM.querySelector finds a node', async () => {
connectionStub.sendCommand = createMockSendCommandFn()
.mockResponse('DOM.getDocument', {root: {nodeId: 249}})
.mockResponse('DOM.getDocument', {root: {nodeId: 249}})
.mockResponse('DOM.querySelector', {nodeId: 231});
const result = await driver.querySelector('meta head');
@ -113,7 +172,7 @@ describe('.querySelector(All)', () => {
it('returns [] when DOM.querySelectorAll finds no node', async () => {
connectionStub.sendCommand = createMockSendCommandFn()
.mockResponse('DOM.getDocument', {root: {nodeId: 249}})
.mockResponse('DOM.getDocument', {root: {nodeId: 249}})
.mockResponse('DOM.querySelectorAll', {nodeIds: []});
const result = await driver.querySelectorAll('#no.matches');
@ -122,7 +181,7 @@ describe('.querySelector(All)', () => {
it('returns element when DOM.querySelectorAll finds node', async () => {
connectionStub.sendCommand = createMockSendCommandFn()
.mockResponse('DOM.getDocument', {root: {nodeId: 249}})
.mockResponse('DOM.getDocument', {root: {nodeId: 249}})
.mockResponse('DOM.querySelectorAll', {nodeIds: [231]});
const result = await driver.querySelectorAll('#no.matches');
@ -168,6 +227,7 @@ describe('.getRequestContent', () => {
it('throws if getRequestContent takes too long', async () => {
const mockTimeout = 5000;
const driverTimeout = 1000;
// @ts-ignore
connectionStub.sendCommand = jest.fn()
.mockImplementation(() => new Promise(r => setTimeout(r, mockTimeout)));
@ -227,7 +287,7 @@ describe('.evaluateAsync', () => {
it('evaluates an expression in isolation', async () => {
connectionStub.sendCommand = createMockSendCommandFn()
.mockResponse('Page.getResourceTree', {frameTree: {frame: {id: 1337}}})
.mockResponse('Page.getResourceTree', {frameTree: {frame: {id: '1337'}}})
.mockResponse('Page.createIsolatedWorld', {executionContextId: 1})
.mockResponse('Runtime.evaluate', {result: {value: 2}});
@ -236,15 +296,14 @@ describe('.evaluateAsync', () => {
// Check that we used the correct frame when creating the isolated context
const createWorldArgs = connectionStub.sendCommand.findInvocation('Page.createIsolatedWorld');
expect(createWorldArgs).toMatchObject({frameId: 1337});
expect(createWorldArgs).toMatchObject({frameId: '1337'});
// Check that we used the isolated context when evaluating
const evaluateArgs = connectionStub.sendCommand.findInvocation('Runtime.evaluate');
expect(evaluateArgs).toMatchObject({contextId: 1});
// Make sure we cached the isolated context from last time
connectionStub.sendCommand = createMockSendCommandFn().mockResponse(
'Runtime.evaluate',
connectionStub.sendCommand = createMockSendCommandFn().mockResponse('Runtime.evaluate',
{result: {value: 2}}
);
await driver.evaluateAsync('1 + 1', {useIsolation: true});
@ -256,10 +315,10 @@ describe('.evaluateAsync', () => {
it('recovers from isolation failures', async () => {
connectionStub.sendCommand = createMockSendCommandFn()
.mockResponse('Page.getResourceTree', {frameTree: {frame: {id: 1337}}})
.mockResponse('Page.getResourceTree', {frameTree: {frame: {id: '1337'}}})
.mockResponse('Page.createIsolatedWorld', {executionContextId: 9001})
.mockResponse('Runtime.evaluate', Promise.reject(new Error('Cannot find context')))
.mockResponse('Page.getResourceTree', {frameTree: {frame: {id: 1337}}})
.mockResponse('Page.getResourceTree', {frameTree: {frame: {id: '1337'}}})
.mockResponse('Page.createIsolatedWorld', {executionContextId: 9002})
.mockResponse('Runtime.evaluate', {result: {value: 'mocked value'}});
@ -271,6 +330,7 @@ describe('.evaluateAsync', () => {
describe('.sendCommand', () => {
it('.sendCommand timesout when commands take too long', async () => {
const mockTimeout = 5000;
// @ts-ignore
connectionStub.sendCommand = jest.fn()
.mockImplementation(() => new Promise(r => setTimeout(r, mockTimeout)));
@ -294,8 +354,8 @@ describe('.beginTrace', () => {
beforeEach(() => {
connectionStub.sendCommand = createMockSendCommandFn()
.mockResponse('Browser.getVersion', protocolGetVersionResponse)
.mockResponse('Page.enable', {})
.mockResponse('Tracing.start', {});
.mockResponse('Page.enable')
.mockResponse('Tracing.start');
});
it('will request default traceCategories', async () => {
@ -320,8 +380,8 @@ describe('.beginTrace', () => {
it('will adjust traceCategories based on chrome version', async () => {
connectionStub.sendCommand = createMockSendCommandFn()
.mockResponse('Browser.getVersion', {product: 'Chrome/70.0.3577.0'})
.mockResponse('Page.enable', {})
.mockResponse('Tracing.start', {});
.mockResponse('Page.enable')
.mockResponse('Tracing.start');
await driver.beginTrace();
@ -334,10 +394,8 @@ describe('.beginTrace', () => {
describe('.setExtraHTTPHeaders', () => {
it('should Network.setExtraHTTPHeaders when there are extra-headers', async () => {
connectionStub.sendCommand = createMockSendCommandFn().mockResponse(
'Network.setExtraHTTPHeaders',
{}
);
connectionStub.sendCommand = createMockSendCommandFn()
.mockResponse('Network.setExtraHTTPHeaders');
await driver.setExtraHTTPHeaders({
'Cookie': 'monster',
@ -351,10 +409,9 @@ describe('.setExtraHTTPHeaders', () => {
);
});
it('should Network.setExtraHTTPHeaders when there are extra-headers', async () => {
it('should not call Network.setExtraHTTPHeaders when there are not extra-headers', async () => {
connectionStub.sendCommand = createMockSendCommandFn();
await driver.setExtraHTTPHeaders();
await driver.setExtraHTTPHeaders(null);
expect(connectionStub.sendCommand).not.toHaveBeenCalled();
});
});
@ -398,8 +455,8 @@ describe('.getAppManifest', () => {
describe('.goOffline', () => {
it('should send offline emulation', async () => {
connectionStub.sendCommand = createMockSendCommandFn()
.mockResponse('Network.enable', {})
.mockResponse('Network.emulateNetworkConditions', {});
.mockResponse('Network.enable')
.mockResponse('Network.emulateNetworkConditions');
await driver.goOffline();
const emulateArgs = connectionStub.sendCommand
@ -414,39 +471,21 @@ describe('.goOffline', () => {
});
describe('.gotoURL', () => {
function createMockWaitForFn() {
let resolve;
let reject;
const promise = new Promise((r1, r2) => {
resolve = r1;
reject = r2;
});
const mockCancelFn = jest.fn();
const mockFn = jest.fn().mockReturnValue({promise, cancel: mockCancelFn});
mockFn.mockResolve = () => resolve();
mockFn.mockReject = err => reject(err || new Error('Rejected'));
mockFn.getMockCancelFn = () => mockCancelFn;
return mockFn;
}
beforeEach(() => {
connectionStub.sendCommand = createMockSendCommandFn()
.mockResponse('Network.enable', {})
.mockResponse('Page.enable', {})
.mockResponse('Page.setLifecycleEventsEnabled', {})
.mockResponse('Emulation.setScriptExecutionDisabled', {})
.mockResponse('Page.navigate', {})
.mockResponse('Target.setAutoAttach', {})
.mockResponse('Runtime.evaluate', {});
.mockResponse('Network.enable')
.mockResponse('Page.enable')
.mockResponse('Page.setLifecycleEventsEnabled')
.mockResponse('Emulation.setScriptExecutionDisabled')
.mockResponse('Page.navigate')
.mockResponse('Target.setAutoAttach')
.mockResponse('Runtime.evaluate');
});
it('will track redirects through gotoURL load', async () => {
const delay = _ => new Promise(resolve => setTimeout(resolve));
const delay = () => new Promise(resolve => setTimeout(resolve));
class ReplayConnection extends EventEmitter {
class ReplayConnection extends Connection {
connect() {
return Promise.resolve();
}
@ -454,9 +493,13 @@ describe('.gotoURL', () => {
return Promise.resolve();
}
replayLog() {
redirectDevtoolsLog.forEach(msg => this.emit('protocolevent', msg));
redirectDevtoolsLog.forEach(msg => this.emitProtocolEvent(msg));
}
sendCommand(method) {
/**
* @param {string} method
* @param {any} _
*/
sendCommand(method, _) {
const resolve = Promise.resolve();
// If navigating, wait, then replay devtools log in parallel to resolve.
@ -468,7 +511,8 @@ describe('.gotoURL', () => {
}
}
const replayConnection = new ReplayConnection();
const driver = new Driver(replayConnection);
const driver = /** @type {TestDriver} */ (new Driver(replayConnection));
// Redirect in log will go through
const startUrl = 'http://en.wikipedia.org/';
@ -486,7 +530,6 @@ describe('.gotoURL', () => {
},
},
};
const loadPromise = driver.gotoURL(startUrl, loadOptions);
await flushAllTimersAndMicrotasks();
@ -527,6 +570,7 @@ describe('.gotoURL', () => {
driver._waitForNetworkIdle = createMockWaitForFn();
driver._waitForCPUIdle = createMockWaitForFn();
// @ts-ignore - dynamic property access, tests will definitely fail if the property were to change
const waitForResult = driver[`_waitFor${name}`];
const otherWaitForResults = [
driver._waitForFCP,
@ -685,23 +729,34 @@ describe('._waitForFCP', () => {
describe('.assertNoSameOriginServiceWorkerClients', () => {
beforeEach(() => {
connectionStub.sendCommand = createMockSendCommandFn()
.mockResponse('ServiceWorker.enable', {})
.mockResponse('ServiceWorker.disable', {})
.mockResponse('ServiceWorker.enable', {})
.mockResponse('ServiceWorker.disable', {});
.mockResponse('ServiceWorker.enable')
.mockResponse('ServiceWorker.disable')
.mockResponse('ServiceWorker.enable')
.mockResponse('ServiceWorker.disable');
});
/**
* @param {number} id
* @param {string} url
* @param {boolean=} isDeleted
*/
function createSWRegistration(id, url, isDeleted) {
return {
isDeleted: !!isDeleted,
registrationId: id,
registrationId: String(id),
scopeURL: url,
};
}
/**
* @param {number} id
* @param {string} url
* @param {string[]} controlledClients
* @param {LH.Crdp.ServiceWorker.ServiceWorkerVersionStatus=} status
*/
function createActiveWorker(id, url, controlledClients, status = 'activated') {
return {
registrationId: id,
registrationId: String(id),
scriptURL: url,
controlledClients,
status,
@ -804,9 +859,9 @@ describe('.assertNoSameOriginServiceWorkerClients', () => {
describe('.goOnline', () => {
beforeEach(() => {
connectionStub.sendCommand = createMockSendCommandFn()
.mockResponse('Network.enable', {})
.mockResponse('Emulation.setCPUThrottlingRate', {})
.mockResponse('Network.emulateNetworkConditions', {});
.mockResponse('Network.enable')
.mockResponse('Emulation.setCPUThrottlingRate')
.mockResponse('Network.emulateNetworkConditions');
});
it('re-establishes previous throttling settings', async () => {
@ -882,22 +937,22 @@ describe('Domain.enable/disable State', () => {
.mockResponse('Fetch.enable')
.mockResponse('Fetch.disable');
await driver.sendCommand('Network.enable', {});
await driver.sendCommand('Network.enable', {});
await driver.sendCommand('Network.enable');
await driver.sendCommand('Network.enable');
expect(connectionStub.sendCommand).toHaveBeenCalledTimes(1);
await driver.sendCommand('Network.disable', {});
await driver.sendCommand('Network.disable');
expect(connectionStub.sendCommand).toHaveBeenCalledTimes(1);
// Network still has one enable.
await driver.sendCommand('Fetch.enable', {});
await driver.sendCommand('Fetch.enable');
expect(connectionStub.sendCommand).toHaveBeenCalledTimes(2);
await driver.sendCommand('Network.disable', {});
await driver.sendCommand('Network.disable');
expect(connectionStub.sendCommand).toHaveBeenCalledTimes(3);
// Network is now disabled.
await driver.sendCommand('Fetch.disable', {});
await driver.sendCommand('Fetch.disable');
expect(connectionStub.sendCommand).toHaveBeenCalledTimes(4);
});
@ -908,24 +963,24 @@ describe('Domain.enable/disable State', () => {
.mockResponse('Network.disable')
.mockResponseToSession('Network.disable', '123');
await driver.sendCommand('Network.enable', {});
await driver.sendCommandToSession('Network.enable', '123', {});
await driver.sendCommand('Network.enable');
await driver.sendCommandToSession('Network.enable', '123');
expect(connectionStub.sendCommand).toHaveBeenCalledTimes(2);
await driver.sendCommand('Network.enable', {});
await driver.sendCommandToSession('Network.enable', '123', {});
await driver.sendCommand('Network.enable');
await driver.sendCommandToSession('Network.enable', '123');
expect(connectionStub.sendCommand).toHaveBeenCalledTimes(2);
await driver.sendCommandToSession('Network.disable', '123', {});
await driver.sendCommandToSession('Network.disable', '123');
expect(connectionStub.sendCommand).toHaveBeenCalledTimes(2);
await driver.sendCommand('Network.disable', {});
await driver.sendCommand('Network.disable');
expect(connectionStub.sendCommand).toHaveBeenCalledTimes(2);
await driver.sendCommandToSession('Network.disable', '123', {});
await driver.sendCommandToSession('Network.disable', '123');
expect(connectionStub.sendCommand).toHaveBeenCalledTimes(3);
await driver.sendCommand('Network.disable', {});
await driver.sendCommand('Network.disable');
expect(connectionStub.sendCommand).toHaveBeenCalledTimes(4);
});
});
@ -933,12 +988,13 @@ describe('Domain.enable/disable State', () => {
describe('Multi-target management', () => {
it('enables the Network domain for iframes', async () => {
connectionStub.sendCommand = createMockSendCommandFn()
.mockResponseToSession('Network.enable', '123', {})
.mockResponseToSession('Target.setAutoAttach', '123', {})
.mockResponseToSession('Runtime.runIfWaitingForDebugger', '123', {});
.mockResponseToSession('Network.enable', '123')
.mockResponseToSession('Target.setAutoAttach', '123')
.mockResponseToSession('Runtime.runIfWaitingForDebugger', '123');
driver._eventEmitter.emit('Target.attachedToTarget', {
sessionId: '123',
// @ts-ignore: Ignore partial targetInfo.
targetInfo: {type: 'iframe'},
});
await flushAllTimersAndMicrotasks();
@ -952,10 +1008,11 @@ describe('Multi-target management', () => {
it('ignores other target types, but still resumes them', async () => {
connectionStub.sendCommand = createMockSendCommandFn()
.mockResponse('Target.sendMessageToTarget', {});
.mockResponse('Target.sendMessageToTarget');
driver._eventEmitter.emit('Target.attachedToTarget', {
sessionId: 'SW1',
// @ts-ignore: Ignore partial targetInfo.
targetInfo: {type: 'service_worker'},
});
await flushAllTimersAndMicrotasks();

Просмотреть файл

@ -5,6 +5,9 @@
*/
'use strict';
/**
* @param {{protocolGetVersionResponse: LH.CrdpCommands['Browser.getVersion']['returnType']}} param0
*/
function makeFakeDriver({protocolGetVersionResponse}) {
let scrollPosition = {x: 0, y: 0};
@ -59,6 +62,7 @@ function makeFakeDriver({protocolGetVersionResponse}) {
evaluateAsync() {
return Promise.resolve({});
},
/** @param {{x: number, y: number}} position */
scrollTo(position) {
scrollPosition = position;
return Promise.resolve();
@ -73,13 +77,15 @@ function makeFakeDriver({protocolGetVersionResponse}) {
return Promise.resolve();
},
endTrace() {
return Promise.resolve(
require('../fixtures/traces/progressive-app.json')
);
// Minimal indirection so TypeScript doesn't crash trying to infer a type.
const modulePath = '../fixtures/traces/progressive-app.json';
return Promise.resolve(require(modulePath));
},
beginDevtoolsLog() {},
endDevtoolsLog() {
return require('../fixtures/artifacts/perflog/defaultPass.devtoolslog.json');
// Minimal indirection so TypeScript doesn't crash trying to infer a type.
const modulePath = '../fixtures/artifacts/perflog/defaultPass.devtoolslog.json';
return require(modulePath);
},
blockUrlPatterns() {
return Promise.resolve();
@ -109,6 +115,8 @@ const fakeDriverUsingRealMobileDevice = makeFakeDriver({
},
});
module.exports = fakeDriver;
module.exports.fakeDriverUsingRealMobileDevice = fakeDriverUsingRealMobileDevice;
module.exports.protocolGetVersionResponse = protocolGetVersionResponse;
module.exports = {
...fakeDriver,
fakeDriverUsingRealMobileDevice,
protocolGetVersionResponse,
};

Просмотреть файл

@ -11,6 +11,16 @@
/* eslint-env jest */
/**
* @template {keyof LH.CrdpCommands} C
* @typedef {((...args: LH.CrdpCommands[C]['paramsType']) => MockResponse<C>) | RecursivePartial<LH.CrdpCommands[C]['returnType']> | Promise<Error>} MockResponse
*/
/**
* @template {keyof LH.CrdpEvents} E
* @typedef {RecursivePartial<LH.CrdpEvents[E][0]>} MockEvent
*/
/**
* Creates a jest mock function whose implementation consumes mocked protocol responses matching the
* requested command in the order they were mocked.
@ -21,33 +31,65 @@
* returns the protocol message argument.
*/
function createMockSendCommandFn() {
/**
* Typescript fails to equate template type `C` here with `C` when pushing to this array.
* Instead of sprinkling a couple ts-ignores, make `command` be any, but leave `C` for just
* documentation purposes. This is an internal type, so it doesn't matter much.
* @template {keyof LH.CrdpCommands} C
* @type {Array<{command: C|any, sessionId?: string, response?: MockResponse<C>, delay?: number}>}
*/
const mockResponses = [];
const mockFn = jest.fn().mockImplementation((command, sessionId, ...args) => {
const indexOfResponse = mockResponses
.findIndex(entry => entry.command === command && entry.sessionId === sessionId);
if (indexOfResponse === -1) throw new Error(`${command} unimplemented`);
const {response, delay} = mockResponses[indexOfResponse];
mockResponses.splice(indexOfResponse, 1);
const returnValue = typeof response === 'function' ? response(...args) : response;
if (delay) return new Promise(resolve => setTimeout(() => resolve(returnValue), delay));
return Promise.resolve(returnValue);
const mockFnImpl = jest.fn().mockImplementation(
/**
* @template {keyof LH.CrdpCommands} C
* @param {C} command
* @param {string|undefined=} sessionId
* @param {LH.CrdpCommands[C]['paramsType']} args
*/
(command, sessionId, ...args) => {
const indexOfResponse = mockResponses
.findIndex(entry => entry.command === command && entry.sessionId === sessionId);
if (indexOfResponse === -1) throw new Error(`${command} unimplemented`);
const {response, delay} = mockResponses[indexOfResponse];
mockResponses.splice(indexOfResponse, 1);
const returnValue = typeof response === 'function' ? response(...args) : response;
if (delay) return new Promise(resolve => setTimeout(() => resolve(returnValue), delay));
// @ts-ignore: Some covariant type stuff doesn't work here. idk, I'm not a type scientist.
return Promise.resolve(returnValue);
});
const mockFn = Object.assign(mockFnImpl, {
/**
* @template {keyof LH.CrdpCommands} C
* @param {C} command
* @param {MockResponse<C>=} response
* @param {number=} delay
*/
mockResponse(command, response, delay) {
mockResponses.push({command, response, delay});
return mockFn;
},
/**
* @template {keyof LH.CrdpCommands} C
* @param {C} command
* @param {string} sessionId
* @param {MockResponse<C>=} response
* @param {number=} delay
*/
mockResponseToSession(command, sessionId, response, delay) {
mockResponses.push({command, sessionId, response, delay});
return mockFn;
},
/**
* @param {keyof LH.CrdpCommands} command
* @param {string=} sessionId
*/
findInvocation(command, sessionId) {
expect(mockFn).toHaveBeenCalledWith(command, sessionId, expect.anything());
return mockFn.mock.calls.find(call => call[0] === command && call[1] === sessionId)[2];
},
});
mockFn.mockResponse = (command, response, delay) => {
mockResponses.push({command, response, delay});
return mockFn;
};
mockFn.mockResponseToSession = (command, sessionId, response, delay) => {
mockResponses.push({command, sessionId, response, delay});
return mockFn;
};
mockFn.findInvocation = (command, sessionId) => {
expect(mockFn).toHaveBeenCalledWith(command, sessionId, expect.anything());
return mockFn.mock.calls.find(call => call[0] === command && call[1] === sessionId)[2];
};
return mockFn;
}
@ -61,8 +103,12 @@ function createMockSendCommandFn() {
* returns the listener .
*/
function createMockOnceFn() {
/**
* @template {keyof LH.CrdpEvents} E
* @type {Array<{event: E|any, response?: MockEvent<E>}>}
*/
const mockEvents = [];
const mockFn = jest.fn().mockImplementation((eventName, listener) => {
const mockFnImpl = jest.fn().mockImplementation((eventName, listener) => {
const indexOfResponse = mockEvents.findIndex(entry => entry.event === eventName);
if (indexOfResponse === -1) return;
const {response} = mockEvents[indexOfResponse];
@ -71,15 +117,24 @@ function createMockOnceFn() {
setTimeout(() => listener(response), 0);
});
mockFn.mockEvent = (event, response) => {
mockEvents.push({event, response});
return mockFn;
};
mockFn.findListener = event => {
expect(mockFn).toHaveBeenCalledWith(event, expect.anything());
return mockFn.mock.calls.find(call => call[0] === event)[1];
};
const mockFn = Object.assign(mockFnImpl, {
/**
* @template {keyof LH.CrdpEvents} E
* @param {E} event
* @param {MockEvent<E>} response
*/
mockEvent(event, response) {
mockEvents.push({event, response});
return mockFn;
},
/**
* @param {keyof LH.CrdpEvents} event
*/
findListener(event) {
expect(mockFn).toHaveBeenCalledWith(event, expect.anything());
return mockFn.mock.calls.find(call => call[0] === event)[1];
},
});
return mockFn;
}
@ -89,8 +144,12 @@ function createMockOnceFn() {
* So it's good for .on w/ many events.
*/
function createMockOnFn() {
/**
* @template {keyof LH.CrdpEvents} E
* @type {Array<{event: E|any, response?: MockEvent<E>}>}
*/
const mockEvents = [];
const mockFn = jest.fn().mockImplementation((eventName, listener) => {
const mockFnImpl = jest.fn().mockImplementation((eventName, listener) => {
const events = mockEvents.filter(entry => entry.event === eventName);
if (!events.length) return;
for (const event of events) {
@ -105,15 +164,24 @@ function createMockOnFn() {
}, 0);
});
mockFn.mockEvent = (event, response) => {
mockEvents.push({event, response});
return mockFn;
};
mockFn.findListener = event => {
expect(mockFn).toHaveBeenCalledWith(event, expect.anything());
return mockFn.mock.calls.find(call => call[0] === event)[1];
};
const mockFn = Object.assign(mockFnImpl, {
/**
* @template {keyof LH.CrdpEvents} E
* @param {E} event
* @param {MockEvent<E>} response
*/
mockEvent(event, response) {
mockEvents.push({event, response});
return mockFn;
},
/**
* @param {keyof LH.CrdpEvents} event
*/
findListener(event) {
expect(mockFn).toHaveBeenCalledWith(event, expect.anything());
return mockFn.mock.calls.find(call => call[0] === event)[1];
},
});
return mockFn;
}

Просмотреть файл

@ -24,5 +24,9 @@
"lighthouse-core/test/**/*.js",
"clients/test/**/*.js",
"lighthouse-cli/test/fixtures/**/*.js",
]
],
"files": [
// Opt-in to typechecking for some core tests.
"lighthouse-core/test/gather/driver-test.js",
],
}

5
types/externs.d.ts поставляемый
Просмотреть файл

@ -28,8 +28,9 @@ declare global {
/** Make optional all properties on T and any properties on object properties of T. */
type RecursivePartial<T> = {
[P in keyof T]+?: T[P] extends object ?
RecursivePartial<T[P]> :
[P in keyof T]+?:
T[P] extends (infer U)[] ? RecursivePartial<U>[] :
T[P] extends (object|undefined) ? RecursivePartial<T[P]> :
T[P];
};

19
types/jest.d.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1,19 @@
/**
* @license Copyright 2019 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.
*/
declare namespace jest {
interface Matchers<R> {
/**
* Asserts that an inspectable promise created by makePromiseInspectable is currently resolved or rejected.
* This is useful for situations where we want to test that we are actually waiting for a particular event.
*/
toBeDone: (failureMessage?: string) => object;
/**
* Asserts that an i18n string (using en-US) matches an expected pattern.
*/
toBeDisplayString: (pattern: RegExp) => object;
}
}