chrome-devtools-frontend
Version:
Chrome DevTools UI
163 lines (138 loc) • 5.96 kB
text/typescript
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as ProtocolClient from '../core/protocol_client/protocol_client.js';
import type * as SDK from '../core/sdk/sdk.js';
import type {ProtocolMapping} from '../generated/protocol-mapping.js';
import type * as ProtocolProxyApi from '../generated/protocol-proxy-api.js';
import {cleanTestDOM} from './DOMHelpers.js';
import {deinitializeGlobalVars, initializeGlobalVars} from './EnvironmentHelpers.js';
import {setMockResourceTree} from './ResourceTreeHelpers.js';
export type ProtocolCommand = keyof ProtocolMapping.Commands;
export type ProtocolCommandParams<C extends ProtocolCommand> = ProtocolMapping.Commands[C]['paramsType'];
export type ProtocolResponse<C extends ProtocolCommand> = ProtocolMapping.Commands[C]['returnType'];
export type ProtocolCommandHandler<C extends ProtocolCommand> = (...params: ProtocolCommandParams<C>) =>
Omit<ProtocolResponse<C>, 'getError'>|{getError(): string};
export type MessageCallback = (result: string|Object) => void;
interface Message {
id: number;
method: ProtocolCommand;
params: unknown;
sessionId: string;
}
interface OutgoingMessageListenerEntry {
promise: Promise<void>;
resolve: () => void;
}
// Note that we can't set the Function to the correct handler on the basis
// that we don't know which ProtocolCommand will be stored.
const responseMap = new Map<ProtocolCommand, ProtocolCommandHandler<ProtocolCommand>>();
const outgoingMessageListenerEntryMap = new Map<ProtocolCommand, OutgoingMessageListenerEntry>();
export function setMockConnectionResponseHandler<C extends ProtocolCommand>(
command: C, handler: ProtocolCommandHandler<C>) {
if (responseMap.get(command)) {
throw new Error(`Response handler already set for ${command}`);
}
responseMap.set(command, handler);
}
export function clearMockConnectionResponseHandler(method: ProtocolCommand) {
responseMap.delete(method);
}
export function clearAllMockConnectionResponseHandlers() {
responseMap.clear();
}
export function registerListenerOnOutgoingMessage(method: ProtocolCommand): Promise<void> {
let outgoingMessageListenerEntry = outgoingMessageListenerEntryMap.get(method);
if (!outgoingMessageListenerEntry) {
const {resolve, promise} = Promise.withResolvers<void>();
outgoingMessageListenerEntry = {promise, resolve};
outgoingMessageListenerEntryMap.set(method, outgoingMessageListenerEntry);
}
return outgoingMessageListenerEntry.promise;
}
export function dispatchEvent<E extends keyof ProtocolMapping.Events>(
target: SDK.Target.Target, eventName: E, ...payload: ProtocolMapping.Events[E]) {
const event = eventName as ProtocolClient.InspectorBackend.QualifiedName;
const [domain] = ProtocolClient.InspectorBackend.splitQualifiedName(event);
const registeredEvents =
ProtocolClient.InspectorBackend.inspectorBackend.getOrCreateEventParameterNamesForDomainForTesting(
domain as keyof ProtocolProxyApi.ProtocolDispatchers);
const eventParameterNames = registeredEvents.get(event);
if (!eventParameterNames) {
// The event is not registered, fake-register with empty parameters.
registeredEvents.set(event, []);
}
target.dispatch({method: event, params: payload[0]});
}
async function enable({reset = true} = {}) {
if (reset) {
responseMap.clear();
}
// The DevTools frontend code expects certain things to be in place
// before it can run. This function will ensure those things are
// minimally there.
await initializeGlobalVars({reset});
setMockResourceTree(true);
ProtocolClient.InspectorBackend.Connection.setFactory(() => new MockConnection());
}
class MockConnection extends ProtocolClient.InspectorBackend.Connection {
messageCallback?: MessageCallback;
override setOnMessage(callback: MessageCallback) {
this.messageCallback = callback;
}
override sendRawMessage(message: string) {
void (async () => {
const outgoingMessage = JSON.parse(message) as Message;
const entry = outgoingMessageListenerEntryMap.get(outgoingMessage.method);
if (entry) {
outgoingMessageListenerEntryMap.delete(outgoingMessage.method);
entry.resolve();
}
const handler = responseMap.get(outgoingMessage.method);
if (!handler) {
return;
}
let result = handler.call(undefined, outgoingMessage.params) || {};
if ('then' in result) {
result = await result;
}
const errorMessage: string = ('getError' in result) ? result.getError() : undefined;
const error = errorMessage ? {message: errorMessage, code: -32000} : undefined;
this.messageCallback?.call(undefined, {
id: outgoingMessage.id,
method: outgoingMessage.method,
result,
error,
sessionId: outgoingMessage.sessionId,
});
})();
}
}
async function disable() {
if (outgoingMessageListenerEntryMap.size > 0) {
throw new Error('MockConnection still has pending listeners. All promises should be awaited.');
}
await cleanTestDOM();
await deinitializeGlobalVars();
// @ts-expect-error Setting back to undefined as a hard reset.
ProtocolClient.InspectorBackend.Connection.setFactory(undefined);
}
export function describeWithMockConnection(title: string, fn: (this: Mocha.Suite) => void, opts: {reset: boolean} = {
reset: true,
}) {
return describe(title, function() {
beforeEach(async () => await enable(opts));
fn.call(this);
afterEach(disable);
});
}
describeWithMockConnection.only = function(title: string, fn: (this: Mocha.Suite) => void, opts: {reset: boolean} = {
reset: true,
}) {
// eslint-disable-next-line mocha/no-exclusive-tests
return describe.only(title, function() {
beforeEach(async () => await enable(opts));
fn.call(this);
afterEach(disable);
});
};