mirror of
https://github.com/ChromeDevTools/chrome-devtools-mcp.git
synced 2026-09-14 19:45:30 +08:00
refactor: use puppeteer Extension API (#1911)
This PR refactor the extension usages using the new Puppeteer API. Requires puppeteer release before merging
This commit is contained in:
Generated
+8
-8
@@ -37,7 +37,7 @@
|
||||
"globals": "^17.0.0",
|
||||
"lighthouse": "13.1.0",
|
||||
"prettier": "^3.6.2",
|
||||
"puppeteer": "24.41.0",
|
||||
"puppeteer": "24.42.0",
|
||||
"rollup": "4.60.2",
|
||||
"rollup-plugin-cleanup": "^3.2.1",
|
||||
"rollup-plugin-license": "^3.6.0",
|
||||
@@ -7486,9 +7486,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/puppeteer": {
|
||||
"version": "24.41.0",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.41.0.tgz",
|
||||
"integrity": "sha512-W6Fk0J3TPjjtwjXOyR/qf+YaL0H/Uq8HIgHcXG4mNM/IgbKMCH/HPyK0Fi2qbTU/QpSl9bCte2yBpGHKejTpIw==",
|
||||
"version": "24.42.0",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.42.0.tgz",
|
||||
"integrity": "sha512-94MoPfFp2eY3eYIMdINkez4IOP5TMHntlZbVx06fHlQTtiQiYgaY0L2Zzfod8PVUkPqP7m3Qlre2v8YS8cudPA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
@@ -7497,7 +7497,7 @@
|
||||
"chromium-bidi": "14.0.0",
|
||||
"cosmiconfig": "^9.0.0",
|
||||
"devtools-protocol": "0.0.1595872",
|
||||
"puppeteer-core": "24.41.0",
|
||||
"puppeteer-core": "24.42.0",
|
||||
"typed-query-selector": "^2.12.1"
|
||||
},
|
||||
"bin": {
|
||||
@@ -7508,9 +7508,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/puppeteer-core": {
|
||||
"version": "24.41.0",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.41.0.tgz",
|
||||
"integrity": "sha512-rLIUri7E/NQ3APSEYCCozaSJx0u8Tu9wxO6BJwnvXmIgILSK3L0TombaVh3izp1njAGrO6H2ru0hcIrLF+gWLw==",
|
||||
"version": "24.42.0",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.42.0.tgz",
|
||||
"integrity": "sha512-T4zXokk/izH01fYPhyyev1A4piWiOKrYq7CUFpdoYQxmOnXoV6YjUabmfIjCYkNspSoAXIxRid3Tw+Vg0fthYg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@
|
||||
"globals": "^17.0.0",
|
||||
"lighthouse": "13.1.0",
|
||||
"prettier": "^3.6.2",
|
||||
"puppeteer": "24.41.0",
|
||||
"puppeteer": "24.42.0",
|
||||
"rollup": "4.60.2",
|
||||
"rollup-plugin-cleanup": "^3.2.1",
|
||||
"rollup-plugin-license": "^3.6.0",
|
||||
|
||||
+1
-4
@@ -77,7 +77,7 @@ const nodeArgs = [
|
||||
...files,
|
||||
];
|
||||
|
||||
function installChrome(version) {
|
||||
function _installChrome(version) {
|
||||
try {
|
||||
return execSync(
|
||||
`npx puppeteer browsers install chrome@${version} --format "{{path}}"`,
|
||||
@@ -112,9 +112,6 @@ async function runTests(attempt) {
|
||||
});
|
||||
}
|
||||
|
||||
const chromePath = installChrome('146.0.7680.31');
|
||||
process.env.CHROME_M146_EXECUTABLE_PATH = chromePath;
|
||||
|
||||
const maxAttempts = shouldRetry ? 3 : 1;
|
||||
let exitCode = 1;
|
||||
|
||||
|
||||
+12
-24
@@ -28,6 +28,7 @@ import type {
|
||||
SerializedAXNode,
|
||||
Viewport,
|
||||
Target,
|
||||
Extension,
|
||||
} from './third_party/index.js';
|
||||
import type {DevTools} from './third_party/index.js';
|
||||
import {Locator} from './third_party/index.js';
|
||||
@@ -47,10 +48,6 @@ import type {
|
||||
TextSnapshotNode,
|
||||
ExtensionServiceWorker,
|
||||
} from './types.js';
|
||||
import {
|
||||
ExtensionRegistry,
|
||||
type InstalledExtension,
|
||||
} from './utils/ExtensionRegistry.js';
|
||||
import {ensureExtension, saveTemporaryFile} from './utils/files.js';
|
||||
import {getNetworkMultiplierFromString} from './WaitForHelper.js';
|
||||
|
||||
@@ -83,7 +80,6 @@ export class McpContext implements Context {
|
||||
#networkCollector: NetworkCollector;
|
||||
#consoleCollector: ConsoleCollector;
|
||||
#devtoolsUniverseManager: UniverseManager;
|
||||
#extensionRegistry = new ExtensionRegistry();
|
||||
|
||||
#isRunningTrace = false;
|
||||
#screenRecorderData: {recorder: ScreenRecorder; filePath: string} | null =
|
||||
@@ -882,38 +878,30 @@ export class McpContext implements Context {
|
||||
|
||||
async installExtension(extensionPath: string): Promise<string> {
|
||||
const id = await this.browser.installExtension(extensionPath);
|
||||
await this.#extensionRegistry.registerExtension(id, extensionPath);
|
||||
return id;
|
||||
}
|
||||
|
||||
async uninstallExtension(id: string): Promise<void> {
|
||||
await this.browser.uninstallExtension(id);
|
||||
this.#extensionRegistry.remove(id);
|
||||
}
|
||||
|
||||
async triggerExtensionAction(id: string): Promise<void> {
|
||||
const page = this.getSelectedPptrPage();
|
||||
// @ts-expect-error internal puppeteer api is needed since we don't have a way to get
|
||||
// a tab id at the moment
|
||||
const theTarget = page._tabId;
|
||||
const session = await this.browser.target().createCDPSession();
|
||||
|
||||
try {
|
||||
await session.send('Extensions.triggerAction', {
|
||||
id,
|
||||
targetId: theTarget,
|
||||
});
|
||||
} finally {
|
||||
await session.detach();
|
||||
const extensions = await this.browser.extensions();
|
||||
const extension = extensions.get(id);
|
||||
if (!extension) {
|
||||
throw new Error(`Extension with ID ${id} not found.`);
|
||||
}
|
||||
const page = this.getSelectedPptrPage();
|
||||
await extension.triggerAction(page);
|
||||
}
|
||||
|
||||
listExtensions(): InstalledExtension[] {
|
||||
return this.#extensionRegistry.list();
|
||||
listExtensions(): Promise<Map<string, Extension>> {
|
||||
return this.browser.extensions();
|
||||
}
|
||||
|
||||
getExtension(id: string): InstalledExtension | undefined {
|
||||
return this.#extensionRegistry.getById(id);
|
||||
async getExtension(id: string): Promise<Extension | undefined> {
|
||||
const pptrExtensions = await this.browser.extensions();
|
||||
return pptrExtensions.get(id);
|
||||
}
|
||||
|
||||
async getHeapSnapshotAggregates(
|
||||
|
||||
+9
-8
@@ -23,6 +23,7 @@ import type {
|
||||
ResourceType,
|
||||
TextContent,
|
||||
JSONSchema7Definition,
|
||||
Extension,
|
||||
} from './third_party/index.js';
|
||||
import type {ToolGroup, ToolDefinition} from './tools/inPage.js';
|
||||
import {handleDialog} from './tools/pages.js';
|
||||
@@ -35,7 +36,6 @@ import type {
|
||||
} from './tools/ToolDefinition.js';
|
||||
import type {InsightName, TraceResult} from './trace-processing/parse.js';
|
||||
import {getInsightOutput, getTraceSummary} from './trace-processing/parse.js';
|
||||
import type {InstalledExtension} from './utils/ExtensionRegistry.js';
|
||||
import {paginate} from './utils/pagination.js';
|
||||
import type {PaginationOptions} from './utils/types.js';
|
||||
|
||||
@@ -527,9 +527,9 @@ export class McpResponse implements Response {
|
||||
}
|
||||
}
|
||||
|
||||
let extensions: InstalledExtension[] | undefined;
|
||||
let extensions: Map<string, Extension> | undefined;
|
||||
if (this.#listExtensions) {
|
||||
extensions = context.listExtensions();
|
||||
extensions = await context.listExtensions();
|
||||
}
|
||||
|
||||
let inPageTools: ToolGroup<ToolDefinition> | undefined;
|
||||
@@ -665,7 +665,7 @@ export class McpResponse implements Response {
|
||||
networkRequests?: NetworkFormatter[];
|
||||
traceSummary?: TraceResult;
|
||||
traceInsight?: TraceInsightData;
|
||||
extensions?: InstalledExtension[];
|
||||
extensions?: Map<string, Extension>;
|
||||
lighthouseResult?: LighthouseData;
|
||||
inPageTools?: ToolGroup<ToolDefinition>;
|
||||
webmcpTools?: WebMCPTool[];
|
||||
@@ -947,14 +947,15 @@ Call ${handleDialog.name} to handle it before continuing.`);
|
||||
}
|
||||
|
||||
if (data.extensions) {
|
||||
structuredContent.extensions = data.extensions;
|
||||
const extensionArray = Array.from(data.extensions.values());
|
||||
structuredContent.extensions = extensionArray;
|
||||
response.push('## Extensions');
|
||||
if (data.extensions.length === 0) {
|
||||
if (extensionArray.length === 0) {
|
||||
response.push('No extensions installed.');
|
||||
} else {
|
||||
const extensionsMessage = data.extensions
|
||||
const extensionsMessage = extensionArray
|
||||
.map(extension => {
|
||||
return `id=${extension.id} "${extension.name}" v${extension.version} ${extension.isEnabled ? 'Enabled' : 'Disabled'}`;
|
||||
return `id=${extension.id} "${extension.name}" v${extension.version} ${extension.enabled ? 'Enabled' : 'Disabled'}`;
|
||||
})
|
||||
.join('\n');
|
||||
response.push(extensionsMessage);
|
||||
|
||||
@@ -10,6 +10,7 @@ import {zod} from '../third_party/index.js';
|
||||
import type {
|
||||
Dialog,
|
||||
ElementHandle,
|
||||
Extension,
|
||||
Page,
|
||||
ScreenRecorder,
|
||||
Viewport,
|
||||
@@ -21,7 +22,6 @@ import type {
|
||||
GeolocationOptions,
|
||||
ExtensionServiceWorker,
|
||||
} from '../types.js';
|
||||
import type {InstalledExtension} from '../utils/ExtensionRegistry.js';
|
||||
import type {PaginationOptions} from '../utils/types.js';
|
||||
|
||||
import type {ToolCategory} from './categories.js';
|
||||
@@ -218,8 +218,8 @@ export type Context = Readonly<{
|
||||
installExtension(path: string): Promise<string>;
|
||||
uninstallExtension(id: string): Promise<void>;
|
||||
triggerExtensionAction(id: string): Promise<void>;
|
||||
listExtensions(): InstalledExtension[];
|
||||
getExtension(id: string): InstalledExtension | undefined;
|
||||
listExtensions(): Promise<Map<string, Extension>>;
|
||||
getExtension(id: string): Promise<Extension | undefined>;
|
||||
getSelectedMcpPage(): McpPage;
|
||||
getExtensionServiceWorkers(): ExtensionServiceWorker[];
|
||||
getExtensionServiceWorkerId(
|
||||
|
||||
@@ -77,7 +77,7 @@ export const reloadExtension = defineTool({
|
||||
},
|
||||
handler: async (request, response, context) => {
|
||||
const {id} = request.params;
|
||||
const extension = context.getExtension(id);
|
||||
const extension = await context.getExtension(id);
|
||||
if (!extension) {
|
||||
throw new Error(`Extension with ID ${id} not found.`);
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
export interface InstalledExtension {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
isEnabled: boolean;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export class ExtensionRegistry {
|
||||
#extensions = new Map<string, InstalledExtension>();
|
||||
|
||||
async registerExtension(
|
||||
id: string,
|
||||
extensionPath: string,
|
||||
): Promise<InstalledExtension> {
|
||||
const manifestPath = path.join(extensionPath, 'manifest.json');
|
||||
const manifestContent = await fs.readFile(manifestPath, 'utf-8');
|
||||
const manifest = JSON.parse(manifestContent);
|
||||
const name = manifest.name ?? 'Unknown';
|
||||
const version = manifest.version ?? 'Unknown';
|
||||
|
||||
const extension = {
|
||||
id,
|
||||
name,
|
||||
version,
|
||||
isEnabled: true,
|
||||
path: extensionPath,
|
||||
};
|
||||
this.#extensions.set(extension.id, extension);
|
||||
return extension;
|
||||
}
|
||||
|
||||
remove(id: string): void {
|
||||
this.#extensions.delete(id);
|
||||
}
|
||||
|
||||
list(): InstalledExtension[] {
|
||||
return Array.from(this.#extensions.values());
|
||||
}
|
||||
|
||||
getById(id: string): InstalledExtension | undefined {
|
||||
return this.#extensions.get(id);
|
||||
}
|
||||
}
|
||||
@@ -1192,14 +1192,14 @@ exports[`extensions > lists extensions 2`] = `
|
||||
"id": "id1",
|
||||
"name": "Extension 1",
|
||||
"version": "1.0",
|
||||
"isEnabled": true,
|
||||
"enabled": true,
|
||||
"path": "/path/to/ext1"
|
||||
},
|
||||
{
|
||||
"id": "id2",
|
||||
"name": "Extension 2",
|
||||
"version": "2.0",
|
||||
"isEnabled": false,
|
||||
"enabled": false,
|
||||
"path": "/path/to/ext2"
|
||||
}
|
||||
]
|
||||
|
||||
+29
-17
@@ -16,7 +16,10 @@ import type {ParsedArguments} from '../src/bin/chrome-devtools-mcp-cli-options.j
|
||||
import type {McpContext} from '../src/McpContext.js';
|
||||
import type {McpResponse} from '../src/McpResponse.js';
|
||||
import {replaceHtmlElementsWithUids} from '../src/McpResponse.js';
|
||||
import type {JSONSchema7Definition} from '../src/third_party/index.js';
|
||||
import type {
|
||||
Extension,
|
||||
JSONSchema7Definition,
|
||||
} from '../src/third_party/index.js';
|
||||
import {
|
||||
closePage,
|
||||
listPages,
|
||||
@@ -955,22 +958,31 @@ describe('extensions', () => {
|
||||
|
||||
response.resetResponseLineForTesting();
|
||||
// Testing with extensions
|
||||
context.listExtensions = () => [
|
||||
{
|
||||
id: 'id1',
|
||||
name: 'Extension 1',
|
||||
version: '1.0',
|
||||
isEnabled: true,
|
||||
path: '/path/to/ext1',
|
||||
},
|
||||
{
|
||||
id: 'id2',
|
||||
name: 'Extension 2',
|
||||
version: '2.0',
|
||||
isEnabled: false,
|
||||
path: '/path/to/ext2',
|
||||
},
|
||||
];
|
||||
context.listExtensions = async () =>
|
||||
Promise.resolve(
|
||||
new Map<string, Extension>([
|
||||
[
|
||||
'id1',
|
||||
{
|
||||
id: 'id1',
|
||||
name: 'Extension 1',
|
||||
version: '1.0',
|
||||
enabled: true,
|
||||
path: '/path/to/ext1',
|
||||
} as Extension,
|
||||
],
|
||||
[
|
||||
'id2',
|
||||
{
|
||||
id: 'id2',
|
||||
name: 'Extension 2',
|
||||
version: '2.0',
|
||||
enabled: false,
|
||||
path: '/path/to/ext2',
|
||||
} as Extension,
|
||||
],
|
||||
]),
|
||||
);
|
||||
response.setListExtensions();
|
||||
const {content, structuredContent} = await response.handle(
|
||||
'test',
|
||||
|
||||
@@ -18,7 +18,11 @@ import {
|
||||
reloadExtension,
|
||||
triggerExtensionAction,
|
||||
} from '../../src/tools/extensions.js';
|
||||
import {extractExtensionId, withMcpContext} from '../utils.js';
|
||||
import {
|
||||
assertNoServiceWorkerReported,
|
||||
extractExtensionId,
|
||||
withMcpContext,
|
||||
} from '../utils.js';
|
||||
|
||||
const EXTENSION_WITH_SW_PATH = path.join(
|
||||
import.meta.dirname,
|
||||
@@ -91,38 +95,45 @@ describe('extension', () => {
|
||||
});
|
||||
});
|
||||
it('reloads an extension', async () => {
|
||||
await withMcpContext(async (response, context) => {
|
||||
await installExtension.handler(
|
||||
{params: {path: EXTENSION_PATH}},
|
||||
response,
|
||||
context,
|
||||
);
|
||||
await withMcpContext(
|
||||
async (response, context) => {
|
||||
await installExtension.handler(
|
||||
{params: {path: EXTENSION_PATH}},
|
||||
response,
|
||||
context,
|
||||
);
|
||||
|
||||
const extensionId = extractExtensionId(response);
|
||||
const installSpy = sinon.spy(context, 'installExtension');
|
||||
response.resetResponseLineForTesting();
|
||||
const extensionId = extractExtensionId(response);
|
||||
const installSpy = sinon.spy(context, 'installExtension');
|
||||
response.resetResponseLineForTesting();
|
||||
|
||||
await reloadExtension.handler(
|
||||
{params: {id: extensionId!}},
|
||||
response,
|
||||
context,
|
||||
);
|
||||
assert.ok(
|
||||
installSpy.calledOnceWithExactly(EXTENSION_PATH),
|
||||
'installExtension should be called with the extension path',
|
||||
);
|
||||
await reloadExtension.handler(
|
||||
{params: {id: extensionId!}},
|
||||
response,
|
||||
context,
|
||||
);
|
||||
assert.ok(
|
||||
installSpy.calledOnceWithExactly(EXTENSION_PATH),
|
||||
'installExtension should be called with the extension path',
|
||||
);
|
||||
|
||||
const reloadResponseLine = response.responseLines[0];
|
||||
assert.ok(
|
||||
reloadResponseLine.includes('Extension reloaded'),
|
||||
'Response should indicate reload',
|
||||
);
|
||||
const reloadResponseLine = response.responseLines[0];
|
||||
assert.ok(
|
||||
reloadResponseLine.includes('Extension reloaded'),
|
||||
'Response should indicate reload',
|
||||
);
|
||||
|
||||
const list = context.listExtensions();
|
||||
assert.ok(list.length === 1, 'List should have only one extension');
|
||||
const reinstalled = list.find(e => e.id === extensionId);
|
||||
assert.ok(reinstalled, 'Extension should be present after reload');
|
||||
});
|
||||
const list = Array.from((await context.listExtensions()).values());
|
||||
|
||||
assert.ok(list.length === 1, 'List should have only one extension');
|
||||
const reinstalled = list.find(e => e.id === extensionId);
|
||||
assert.ok(reinstalled, 'Extension should be present after reload');
|
||||
},
|
||||
{},
|
||||
{
|
||||
categoryExtensions: true,
|
||||
} as ParsedArguments,
|
||||
);
|
||||
});
|
||||
it('triggers an extension action', async () => {
|
||||
await withMcpContext(
|
||||
@@ -147,10 +158,11 @@ describe('extension', () => {
|
||||
t => t.type() === 'page' && t.url().includes(extensionId),
|
||||
);
|
||||
assert.ok(pageTargetAfter, 'Page should exist after action');
|
||||
await context.uninstallExtension(extensionId);
|
||||
const targets = context.browser.targets();
|
||||
assertNoServiceWorkerReported(targets, extensionId);
|
||||
},
|
||||
{
|
||||
executablePath: process.env.CHROME_M146_EXECUTABLE_PATH,
|
||||
},
|
||||
{},
|
||||
{
|
||||
categoryExtensions: true,
|
||||
} as ParsedArguments,
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
handleDialog,
|
||||
getTabId,
|
||||
} from '../../src/tools/pages.js';
|
||||
import {html, withMcpContext} from '../utils.js';
|
||||
import {assertNoServiceWorkerReported, html, withMcpContext} from '../utils.js';
|
||||
|
||||
const EXTENSION_SW_PATH = path.join(
|
||||
import.meta.dirname,
|
||||
@@ -94,10 +94,9 @@ describe('pages', () => {
|
||||
'<extension-id>',
|
||||
);
|
||||
t.assert.snapshot?.(text);
|
||||
await context.uninstallExtension(extensionId);
|
||||
},
|
||||
{
|
||||
executablePath: process.env.CHROME_M146_EXECUTABLE_PATH,
|
||||
},
|
||||
{},
|
||||
{
|
||||
categoryExtensions: true,
|
||||
} as ParsedArguments,
|
||||
@@ -146,6 +145,9 @@ describe('pages', () => {
|
||||
'<extension-id>',
|
||||
);
|
||||
t.assert.snapshot?.(text);
|
||||
await context.uninstallExtension(extensionId);
|
||||
const targets = context.browser.targets();
|
||||
assertNoServiceWorkerReported(targets, extensionId);
|
||||
},
|
||||
{},
|
||||
{
|
||||
@@ -193,10 +195,11 @@ describe('pages', () => {
|
||||
'<extension-id>',
|
||||
);
|
||||
t.assert.snapshot?.(text);
|
||||
await context.uninstallExtension(extensionId);
|
||||
const targets = context.browser.targets();
|
||||
assertNoServiceWorkerReported(targets, extensionId);
|
||||
},
|
||||
{
|
||||
executablePath: process.env.CHROME_M146_EXECUTABLE_PATH,
|
||||
},
|
||||
{},
|
||||
{
|
||||
categoryExtensions: true,
|
||||
} as ParsedArguments,
|
||||
|
||||
@@ -12,7 +12,12 @@ import type {ParsedArguments} from '../../src/bin/chrome-devtools-mcp-cli-option
|
||||
import {installExtension} from '../../src/tools/extensions.js';
|
||||
import {evaluateScript} from '../../src/tools/script.js';
|
||||
import {serverHooks} from '../server.js';
|
||||
import {extractExtensionId, html, withMcpContext} from '../utils.js';
|
||||
import {
|
||||
assertNoServiceWorkerReported,
|
||||
extractExtensionId,
|
||||
html,
|
||||
withMcpContext,
|
||||
} from '../utils.js';
|
||||
|
||||
const EXTENSION_PATH = path.join(
|
||||
import.meta.dirname,
|
||||
@@ -309,6 +314,9 @@ describe('script', () => {
|
||||
|
||||
const lineEvaluation = response.responseLines.at(2)!;
|
||||
assert.strictEqual(JSON.parse(lineEvaluation), 'has-chrome');
|
||||
await context.uninstallExtension(extensionId);
|
||||
const targets = context.browser.targets();
|
||||
assertNoServiceWorkerReported(targets, extensionId);
|
||||
},
|
||||
{},
|
||||
{categoryExtensions: true} as ParsedArguments,
|
||||
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
HTTPResponse,
|
||||
LaunchOptions,
|
||||
Page,
|
||||
Target,
|
||||
} from 'puppeteer-core';
|
||||
import sinon from 'sinon';
|
||||
|
||||
@@ -27,6 +28,13 @@ import {McpResponse} from '../src/McpResponse.js';
|
||||
import {stableIdSymbol} from '../src/PageCollector.js';
|
||||
import {DevTools} from '../src/third_party/index.js';
|
||||
|
||||
export function assertNoServiceWorkerReported(targets: Target[], id: string) {
|
||||
const target = targets.find(target => {
|
||||
return target.url().includes(id) && target.type() === 'service_worker';
|
||||
});
|
||||
assert(target === undefined);
|
||||
}
|
||||
|
||||
export function getTextContent(
|
||||
content: CallToolResult['content'][number],
|
||||
): string {
|
||||
|
||||
Reference in New Issue
Block a user