blob: 3e691c4db15aef24edbf7c7b23cade99b9f766cf [file] [log] [blame]
// Copyright 2021 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 type {
Chrome} from '../../../extension-api/ExtensionAPI.js'; // eslint-disable-line @typescript-eslint/no-unused-vars
import type * as puppeteer from 'puppeteer';
import {getBrowserAndPages, getDevToolsFrontendHostname, getResourcesPath, waitFor} from '../../shared/helper.js';
// TODO: Remove once Chromium updates its version of Node.js to 12+.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const globalThis: any = global;
let loadExtensionPromise: Promise<unknown> = Promise.resolve();
// FIXME(chromium:1248945): Replace with crypto.randomUUID() once Chromium updates its version of node.js
function guid() {
const digits = '0123456789abcdef';
const rnd = () => digits[Math.floor(Math.random() * (digits.length - 1))];
const eight = new Array(8).fill('0').map(rnd).join('');
const four = new Array(4).fill('0').map(rnd).join('');
const version = new Array(3).fill('0').map(rnd).join('');
const variant = new Array(3).fill('0').map(rnd).join('');
const twelve = new Array(12).fill('0').map(rnd).join('');
return `${eight}-${four}-4${version}-8${variant}-${twelve}`;
}
export function getResourcesPathWithDevToolsHostname() {
return getResourcesPath(getDevToolsFrontendHostname());
}
export async function loadExtension(name: string, startPage?: string) {
startPage = startPage || `${getResourcesPathWithDevToolsHostname()}/extensions/empty_extension.html`;
const {frontend} = getBrowserAndPages();
const extensionInfo = {startPage, name};
// Because the injected script is shared across calls for the target, we cannot run multiple instances concurrently.
const load = loadExtensionPromise.then(() => doLoad(frontend, extensionInfo));
loadExtensionPromise = load.catch(() => {});
return load;
async function doLoad(frontend: puppeteer.Page, extensionInfo: {startPage: string, name: string}) {
// @ts-ignore The pptr API doesn't allow us to remove the API injection after we're done.
const session = await frontend._client;
// TODO(chromium:1246836) remove once real extension tests are available
const injectedAPI = await frontend.evaluate(
extensionInfo => globalThis.buildExtensionAPIInjectedScript(
extensionInfo, undefined, 'default', globalThis.UI.shortcutRegistry.globalShortcutKeys()),
extensionInfo);
function declareChrome() {
if (!window.chrome) {
(window.chrome as unknown) = {};
}
}
const extensionScriptId = guid();
const injectedScriptId = await session.send(
'Page.addScriptToEvaluateOnNewDocument',
{source: `(${declareChrome})();${injectedAPI}('${extensionScriptId}')`});
try {
await frontend.evaluate(extensionInfo => {
globalThis.Extensions.extensionServer.addExtension(extensionInfo);
const extensionIFrames = document.body.querySelectorAll(`[data-devtools-extension="${extensionInfo.name}"]`);
if (extensionIFrames.length > 1) {
throw new Error(`Duplicate extension ${extensionInfo.name}`);
}
if (extensionIFrames.length === 0) {
throw new Error('Installing the extension failed.');
}
return new Promise<void>(resolve => {
(extensionIFrames[0] as HTMLIFrameElement).onload = () => resolve();
});
}, extensionInfo);
const iframe = await waitFor(`[data-devtools-extension="${name}"]`);
const frame = await iframe.contentFrame();
if (!frame) {
throw new Error('Installing the extension failed.');
}
return frame;
} finally {
await session.send('Page.removeScriptToEvaluateOnNewDocument', injectedScriptId);
}
}
}