mirror of
https://github.com/ChromeDevTools/chrome-devtools-mcp.git
synced 2026-09-14 19:45:30 +08:00
feat: implement extension service worker logs (#1915)
This PR introduces the possiblity to collect console messages from service workers.
This commit is contained in:
@@ -394,6 +394,7 @@ so returned values have to be JSON-serializable.
|
||||
- **includePreservedMessages** (boolean) _(optional)_: Set to true to return the preserved messages over the last 3 navigations.
|
||||
- **pageIdx** (integer) _(optional)_: Page number to return (0-based). When omitted, returns the first page.
|
||||
- **pageSize** (integer) _(optional)_: Maximum number of messages to return. When omitted, returns all messages.
|
||||
- **serviceWorkerId** (string) _(optional)_: Filter messages to only return messages of the specified service worker.
|
||||
- **types** (array) _(optional)_: Filter messages to only return messages of the specified resource types. When omitted or empty, returns all messages.
|
||||
|
||||
---
|
||||
|
||||
+14
-1
@@ -21,6 +21,7 @@ import {
|
||||
type ListenerMap,
|
||||
type UncaughtError,
|
||||
} from './PageCollector.js';
|
||||
import {ServiceWorkerConsoleCollector} from './ServiceWorkerCollector.js';
|
||||
import {
|
||||
Locator,
|
||||
PredefinedNetworkConditions,
|
||||
@@ -84,6 +85,7 @@ export class McpContext implements Context {
|
||||
#networkCollector: NetworkCollector;
|
||||
#consoleCollector: ConsoleCollector;
|
||||
#devtoolsUniverseManager: UniverseManager;
|
||||
#serviceWorkerConsoleCollector: ServiceWorkerConsoleCollector;
|
||||
|
||||
#isRunningTrace = false;
|
||||
#screenRecorderData: {recorder: ScreenRecorder; filePath: string} | null =
|
||||
@@ -128,21 +130,26 @@ export class McpContext implements Context {
|
||||
},
|
||||
} as ListenerMap;
|
||||
});
|
||||
this.#serviceWorkerConsoleCollector = new ServiceWorkerConsoleCollector(
|
||||
this.browser,
|
||||
);
|
||||
this.#devtoolsUniverseManager = new UniverseManager(this.browser);
|
||||
}
|
||||
|
||||
async #init() {
|
||||
const pages = await this.createPagesSnapshot();
|
||||
await this.createExtensionServiceWorkersSnapshot();
|
||||
const workers = await this.createExtensionServiceWorkersSnapshot();
|
||||
await this.#networkCollector.init(pages);
|
||||
await this.#consoleCollector.init(pages);
|
||||
await this.#devtoolsUniverseManager.init(pages);
|
||||
await this.#serviceWorkerConsoleCollector.init(workers);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.#networkCollector.dispose();
|
||||
this.#consoleCollector.dispose();
|
||||
this.#devtoolsUniverseManager.dispose();
|
||||
this.#serviceWorkerConsoleCollector.dispose();
|
||||
for (const mcpPage of this.#mcpPages.values()) {
|
||||
mcpPage.dispose();
|
||||
}
|
||||
@@ -590,6 +597,12 @@ export class McpContext implements Context {
|
||||
return this.#extensionServiceWorkers;
|
||||
}
|
||||
|
||||
getServiceWorkerConsoleData(
|
||||
extensionId: string,
|
||||
): Array<ConsoleMessage | UncaughtError> {
|
||||
return this.#serviceWorkerConsoleCollector.getData(extensionId);
|
||||
}
|
||||
|
||||
async createPagesSnapshot(): Promise<Page[]> {
|
||||
const {pages: allPages, isolatedContextNames} = await this.#getAllPages();
|
||||
|
||||
|
||||
+22
-8
@@ -232,6 +232,7 @@ export class McpResponse implements Response {
|
||||
pagination?: PaginationOptions;
|
||||
types?: string[];
|
||||
includePreservedMessages?: boolean;
|
||||
serviceWorkerId?: string;
|
||||
};
|
||||
#listExtensions?: boolean;
|
||||
#listThirdPartyDeveloperTools?: boolean;
|
||||
@@ -328,6 +329,7 @@ export class McpResponse implements Response {
|
||||
options?: PaginationOptions & {
|
||||
types?: string[];
|
||||
includePreservedMessages?: boolean;
|
||||
serviceWorkerId?: string;
|
||||
},
|
||||
): void {
|
||||
if (!value) {
|
||||
@@ -346,6 +348,7 @@ export class McpResponse implements Response {
|
||||
: undefined,
|
||||
types: options?.types,
|
||||
includePreservedMessages: options?.includePreservedMessages,
|
||||
serviceWorkerId: options?.serviceWorkerId,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -620,14 +623,23 @@ export class McpResponse implements Response {
|
||||
|
||||
let consoleMessages: Array<ConsoleFormatter | IssueFormatter> | undefined;
|
||||
if (this.#consoleDataOptions?.include) {
|
||||
if (!this.#page) {
|
||||
throw new Error(`Response must have an McpPage`);
|
||||
let messages;
|
||||
let page: McpPage | undefined;
|
||||
|
||||
if (this.#consoleDataOptions.serviceWorkerId) {
|
||||
messages = context.getServiceWorkerConsoleData(
|
||||
this.#consoleDataOptions.serviceWorkerId,
|
||||
);
|
||||
} else {
|
||||
page = this.#page;
|
||||
if (!page) {
|
||||
throw new Error(`Response must have an McpPage`);
|
||||
}
|
||||
messages = context.getConsoleData(
|
||||
page,
|
||||
this.#consoleDataOptions.includePreservedMessages,
|
||||
);
|
||||
}
|
||||
const page = this.#page;
|
||||
let messages = context.getConsoleData(
|
||||
this.#page,
|
||||
this.#consoleDataOptions.includePreservedMessages,
|
||||
);
|
||||
|
||||
if (this.#consoleDataOptions.types?.length) {
|
||||
const normalizedTypes = new Set(this.#consoleDataOptions.types);
|
||||
@@ -650,7 +662,9 @@ export class McpResponse implements Response {
|
||||
context.getConsoleMessageStableId(item);
|
||||
if ('args' in item || item instanceof UncaughtError) {
|
||||
const consoleMessage = item as ConsoleMessage | UncaughtError;
|
||||
const devTools = context.getDevToolsUniverse(page);
|
||||
const devTools = page
|
||||
? context.getDevToolsUniverse(page)
|
||||
: null;
|
||||
return await ConsoleFormatter.from(consoleMessage, {
|
||||
id: consoleMessageStableId,
|
||||
fetchDetailedData: false,
|
||||
|
||||
@@ -194,11 +194,11 @@ export class PageCollector<T> {
|
||||
|
||||
const item = this.find(page, item => item[stableIdSymbol] === stableId);
|
||||
|
||||
if (item) {
|
||||
return item;
|
||||
if (!item) {
|
||||
throw new Error('Request not found for selected page');
|
||||
}
|
||||
|
||||
throw new Error('Request not found for selected page');
|
||||
return item;
|
||||
}
|
||||
|
||||
find(
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {UncaughtError} from './PageCollector.js';
|
||||
import type {
|
||||
ConsoleMessage,
|
||||
WebWorker,
|
||||
Target,
|
||||
CDPSession,
|
||||
Protocol,
|
||||
Browser,
|
||||
} from './third_party/index.js';
|
||||
import type {ExtensionServiceWorker} from './types.js';
|
||||
import type {WithSymbolId} from './utils/id.js';
|
||||
import {createIdGenerator, stableIdSymbol} from './utils/id.js';
|
||||
|
||||
const CHROME_EXTENSION_PREFIX = 'chrome-extension://';
|
||||
|
||||
export class ServiceWorkerSubscriber {
|
||||
#target: Target;
|
||||
#callback: (item: ConsoleMessage | UncaughtError) => void;
|
||||
#session?: CDPSession;
|
||||
#worker?: WebWorker;
|
||||
|
||||
constructor(
|
||||
target: Target,
|
||||
callback: (item: ConsoleMessage | UncaughtError) => void,
|
||||
) {
|
||||
this.#target = target;
|
||||
this.#callback = callback;
|
||||
}
|
||||
|
||||
async subscribe() {
|
||||
this.#session = await this.#target.createCDPSession();
|
||||
await this.#session.send('Runtime.enable');
|
||||
this.#session.on('Runtime.exceptionThrown', this.#onExceptionThrown);
|
||||
|
||||
this.#worker = (await this.#target.worker()) ?? undefined;
|
||||
if (this.#worker) {
|
||||
this.#worker.on('console', this.#onConsole);
|
||||
}
|
||||
}
|
||||
|
||||
async unsubscribe() {
|
||||
if (this.#worker) {
|
||||
this.#worker.off('console', this.#onConsole);
|
||||
}
|
||||
await this.#session?.detach();
|
||||
}
|
||||
|
||||
#onConsole = (message: ConsoleMessage) => {
|
||||
this.#callback(message);
|
||||
};
|
||||
|
||||
#onExceptionThrown = (event: Protocol.Runtime.ExceptionThrownEvent) => {
|
||||
const url = this.#target.url();
|
||||
|
||||
const extensionId = extractExtensionId(url);
|
||||
|
||||
if (extensionId) {
|
||||
this.#callback(new UncaughtError(event.exceptionDetails, extensionId));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export class ServiceWorkerConsoleCollector {
|
||||
#storage = new Map<
|
||||
string,
|
||||
Array<WithSymbolId<ConsoleMessage | UncaughtError>>
|
||||
>();
|
||||
#maxLogs: number;
|
||||
#browser?: Browser;
|
||||
#serviceWorkerSubscribers = new Map<Target, ServiceWorkerSubscriber>();
|
||||
#idGenerator = createIdGenerator();
|
||||
|
||||
constructor(browser?: Browser, maxLogs = 1000) {
|
||||
this.#browser = browser;
|
||||
this.#maxLogs = maxLogs;
|
||||
}
|
||||
|
||||
async init(workers: ExtensionServiceWorker[]) {
|
||||
if (!this.#browser) {
|
||||
return;
|
||||
}
|
||||
this.#browser.on('targetcreated', this.#onTargetCreated);
|
||||
this.#browser.on('targetdestroyed', this.#onTargetDestroyed);
|
||||
|
||||
for (const worker of workers) {
|
||||
void this.#onTargetCreated(worker.target);
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
if (!this.#browser) {
|
||||
return;
|
||||
}
|
||||
this.#browser.off('targetcreated', this.#onTargetCreated);
|
||||
this.#browser.off('targetdestroyed', this.#onTargetDestroyed);
|
||||
for (const subscriber of this.#serviceWorkerSubscribers.values()) {
|
||||
subscriber.unsubscribe().catch(err => {
|
||||
if (
|
||||
err instanceof Error &&
|
||||
!err.message.includes('Target closed') &&
|
||||
!err.message.includes('Session closed')
|
||||
) {
|
||||
// Swallow error as we are tearing down the system
|
||||
}
|
||||
});
|
||||
}
|
||||
this.#serviceWorkerSubscribers.clear();
|
||||
}
|
||||
|
||||
#onTargetCreated = async (target: Target) => {
|
||||
if (this.#serviceWorkerSubscribers.has(target)) {
|
||||
return;
|
||||
}
|
||||
const origin = target.url();
|
||||
if (target.type() === 'service_worker' && isExtensionOrigin(origin)) {
|
||||
const extensionId = extractExtensionId(origin);
|
||||
|
||||
if (!extensionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const subscriber = new ServiceWorkerSubscriber(target, item => {
|
||||
this.addLog(extensionId, item);
|
||||
});
|
||||
try {
|
||||
await subscriber.subscribe();
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof Error &&
|
||||
!err.message.includes('Target closed') &&
|
||||
!err.message.includes('Session closed')
|
||||
) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
this.#serviceWorkerSubscribers.set(target, subscriber);
|
||||
}
|
||||
};
|
||||
|
||||
#onTargetDestroyed = async (target: Target) => {
|
||||
const subscriber = this.#serviceWorkerSubscribers.get(target);
|
||||
if (subscriber) {
|
||||
try {
|
||||
await subscriber.unsubscribe();
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof Error &&
|
||||
!err.message.includes('Target closed') &&
|
||||
!err.message.includes('Session closed')
|
||||
) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
this.#serviceWorkerSubscribers.delete(target);
|
||||
}
|
||||
};
|
||||
|
||||
addLog(extensionId: string, log: ConsoleMessage | UncaughtError) {
|
||||
const logs = this.#storage.get(extensionId) ?? [];
|
||||
const withId = log as WithSymbolId<ConsoleMessage | UncaughtError>;
|
||||
withId[stableIdSymbol] = this.#idGenerator();
|
||||
logs.push(withId);
|
||||
if (logs.length > this.#maxLogs) {
|
||||
logs.shift();
|
||||
}
|
||||
this.#storage.set(extensionId, logs);
|
||||
}
|
||||
|
||||
getData(
|
||||
extensionId: string,
|
||||
): Array<WithSymbolId<ConsoleMessage | UncaughtError>> {
|
||||
return this.#storage.get(extensionId) ?? [];
|
||||
}
|
||||
|
||||
getById(
|
||||
extensionId: string,
|
||||
stableId: number,
|
||||
): WithSymbolId<ConsoleMessage | UncaughtError> {
|
||||
const logs = this.#storage.get(extensionId);
|
||||
if (!logs) {
|
||||
throw new Error('No logs found for selected extension');
|
||||
}
|
||||
const item = logs.find(item => item[stableIdSymbol] === stableId);
|
||||
if (item) {
|
||||
return item;
|
||||
}
|
||||
throw new Error('Log not found for selected extension');
|
||||
}
|
||||
|
||||
find(
|
||||
extensionId: string,
|
||||
filter: (item: WithSymbolId<ConsoleMessage | UncaughtError>) => boolean,
|
||||
): WithSymbolId<ConsoleMessage | UncaughtError> | undefined {
|
||||
const logs = this.#storage.get(extensionId);
|
||||
if (!logs) {
|
||||
return;
|
||||
}
|
||||
return logs.find(filter);
|
||||
}
|
||||
|
||||
clearLogs(extensionId: string) {
|
||||
this.#storage.delete(extensionId);
|
||||
}
|
||||
}
|
||||
|
||||
function extractExtensionId(origin: string): string | null {
|
||||
if (!origin || !isExtensionOrigin(origin)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pathPart = origin.substring(CHROME_EXTENSION_PREFIX.length);
|
||||
const slashIndex = pathPart.indexOf('/');
|
||||
|
||||
// if there's no / it means that pathPart is now the extensionId, otherwise
|
||||
// we take everything until the first /
|
||||
return slashIndex === -1 ? pathPart : pathPart.substring(0, slashIndex);
|
||||
}
|
||||
|
||||
function isExtensionOrigin(origin: string) {
|
||||
return origin.startsWith(CHROME_EXTENSION_PREFIX);
|
||||
}
|
||||
@@ -555,6 +555,13 @@ export const commands: Commands = {
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
serviceWorkerId: {
|
||||
name: 'serviceWorkerId',
|
||||
type: 'string',
|
||||
description:
|
||||
'Filter messages to only return messages of the specified service worker.',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
list_extensions: {
|
||||
|
||||
@@ -254,6 +254,10 @@
|
||||
{
|
||||
"name": "include_preserved_messages",
|
||||
"argType": "boolean"
|
||||
},
|
||||
{
|
||||
"name": "service_worker_id_length",
|
||||
"argType": "number"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
Vendored
+3
@@ -47,7 +47,10 @@ export {default as puppeteer} from 'puppeteer-core';
|
||||
export type * from 'puppeteer-core';
|
||||
export {PipeTransport} from 'puppeteer-core/internal/node/PipeTransport.js';
|
||||
export type {CdpPage} from 'puppeteer-core/internal/cdp/Page.js';
|
||||
export type {CdpWebWorker} from 'puppeteer-core/internal/cdp/WebWorker.js';
|
||||
export type {Realm} from 'puppeteer-core/internal/api/Realm.js';
|
||||
export type {JSONSchema7, JSONSchema7Definition} from 'json-schema';
|
||||
|
||||
export {
|
||||
resolveDefaultUserDataDir,
|
||||
detectBrowserPlatform,
|
||||
|
||||
@@ -130,6 +130,7 @@ export interface Response {
|
||||
options?: PaginationOptions & {
|
||||
types?: string[];
|
||||
includePreservedMessages?: boolean;
|
||||
serviceWorkerId?: string;
|
||||
},
|
||||
): void;
|
||||
includeSnapshot(params?: SnapshotParams): void;
|
||||
|
||||
@@ -77,6 +77,12 @@ export const listConsoleMessages = definePageTool(cliArgs => {
|
||||
.describe(
|
||||
'Set to true to return the preserved messages over the last 3 navigations.',
|
||||
),
|
||||
serviceWorkerId: zod
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'Filter messages to only return messages of the specified service worker.',
|
||||
),
|
||||
},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: [],
|
||||
@@ -86,6 +92,7 @@ export const listConsoleMessages = definePageTool(cliArgs => {
|
||||
pageIdx: request.params.pageIdx,
|
||||
types: request.params.types,
|
||||
includePreservedMessages: request.params.includePreservedMessages,
|
||||
serviceWorkerId: request.params.serviceWorkerId,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import assert from 'node:assert';
|
||||
import {describe, it} from 'node:test';
|
||||
|
||||
import {UncaughtError} from '../src/PageCollector.js';
|
||||
import {ServiceWorkerConsoleCollector} from '../src/ServiceWorkerCollector.js';
|
||||
import type {Protocol} from '../src/third_party/index.js';
|
||||
import {stableIdSymbol} from '../src/utils/id.js';
|
||||
|
||||
describe('ServiceWorkerConsoleCollector', () => {
|
||||
it('limits logs to 1000 per extension', () => {
|
||||
const collector = new ServiceWorkerConsoleCollector(undefined, 10);
|
||||
const extensionId = 'test-extension';
|
||||
|
||||
const mockDetails: Protocol.Runtime.ExceptionDetails = {
|
||||
exceptionId: 1,
|
||||
text: 'Error',
|
||||
lineNumber: 1,
|
||||
columnNumber: 1,
|
||||
};
|
||||
|
||||
for (let i = 0; i < 15; i++) {
|
||||
const error = new UncaughtError(
|
||||
{...mockDetails, exceptionId: i},
|
||||
extensionId,
|
||||
);
|
||||
collector.addLog(extensionId, error);
|
||||
}
|
||||
|
||||
const logs = collector.getData(extensionId);
|
||||
assert.strictEqual(logs.length, 10, 'Should limit logs to 10');
|
||||
|
||||
const firstLog = logs[0] as UncaughtError;
|
||||
assert.strictEqual(
|
||||
firstLog.details.exceptionId,
|
||||
5,
|
||||
'Oldest log should be Log 5',
|
||||
);
|
||||
|
||||
const lastLog = logs[logs.length - 1] as UncaughtError;
|
||||
assert.strictEqual(
|
||||
lastLog.details.exceptionId,
|
||||
14,
|
||||
'Last log should be Log 14',
|
||||
);
|
||||
|
||||
const data = collector.getData(extensionId);
|
||||
assert.strictEqual(data.length, 10, 'getData should return limited logs');
|
||||
|
||||
const logToFind = data[0];
|
||||
const logId = logToFind[stableIdSymbol];
|
||||
assert.ok(logId, 'Log should have a stable ID');
|
||||
|
||||
const foundLog = collector.getById(extensionId, logId);
|
||||
assert.strictEqual(
|
||||
foundLog,
|
||||
logToFind,
|
||||
'getById should return correct log',
|
||||
);
|
||||
|
||||
const foundViaFind = collector.find(extensionId, item => {
|
||||
return item[stableIdSymbol] === logId;
|
||||
});
|
||||
assert.strictEqual(
|
||||
foundViaFind,
|
||||
logToFind,
|
||||
'find should return correct log',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,11 @@
|
||||
exports[`console > captures logs and errors from extension service worker 1`] = `
|
||||
## Console messages
|
||||
Showing 1-3 of 3 (Page 1 of 1).
|
||||
msgid=1 [log] Service Worker starting... (1 args)
|
||||
msgid=2 [warn] This is a warning from Service Worker (1 args)
|
||||
msgid=3 [error] Uncaught Error: Intentional error from Service Worker (0 args)
|
||||
`;
|
||||
|
||||
exports[`console > get_console_message > applies source maps to stack traces of Error object (with cause) console.log arguments 1`] = `
|
||||
ID: 1
|
||||
Message: log> foo failed Error: bar failed
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import assert from 'node:assert';
|
||||
import path from 'node:path';
|
||||
import {before, describe, it} from 'node:test';
|
||||
|
||||
import type {Dialog} from 'puppeteer-core';
|
||||
@@ -13,22 +14,134 @@ import type {ParsedArguments} from '../../src/bin/chrome-devtools-mcp-cli-option
|
||||
import {loadIssueDescriptions} from '../../src/issue-descriptions.js';
|
||||
import {McpResponse} from '../../src/McpResponse.js';
|
||||
import {TextSnapshot} from '../../src/TextSnapshot.js';
|
||||
import type {CdpWebWorker} from '../../src/third_party/index.js';
|
||||
import {DevTools} from '../../src/third_party/index.js';
|
||||
import {
|
||||
getConsoleMessage,
|
||||
listConsoleMessages,
|
||||
} from '../../src/tools/console.js';
|
||||
import {installExtension} from '../../src/tools/extensions.js';
|
||||
import {serverHooks} from '../server.js';
|
||||
import {
|
||||
getTextContent,
|
||||
withMcpContext,
|
||||
stabilizeStructuredContent,
|
||||
extractExtensionId,
|
||||
assertNoServiceWorkerReported,
|
||||
waitExecutionFor,
|
||||
} from '../utils.js';
|
||||
|
||||
const EXTENSION_LOGGING_PATH = path.join(
|
||||
import.meta.dirname,
|
||||
'../../../tests/tools/fixtures/extension-logging',
|
||||
);
|
||||
|
||||
describe('console', () => {
|
||||
before(async () => {
|
||||
await loadIssueDescriptions();
|
||||
});
|
||||
|
||||
it('captures logs and errors from extension service worker', async t => {
|
||||
await withMcpContext(
|
||||
async (response, context) => {
|
||||
await installExtension.handler(
|
||||
{params: {path: EXTENSION_LOGGING_PATH}},
|
||||
response,
|
||||
context,
|
||||
);
|
||||
|
||||
const extensionId = extractExtensionId(response);
|
||||
assert.ok(extensionId, 'Extension ID should be returned');
|
||||
|
||||
const swTarget = await context.browser.waitForTarget(
|
||||
t => t.type() === 'service_worker' && t.url().includes(extensionId),
|
||||
);
|
||||
|
||||
const swList = await context.createExtensionServiceWorkersSnapshot();
|
||||
const sw = swList.find(s => s.target === swTarget);
|
||||
assert(sw, 'Service worker not found in context list');
|
||||
|
||||
const response2 = new McpResponse({} as ParsedArguments);
|
||||
|
||||
await context.triggerExtensionAction(extensionId);
|
||||
const worker = await swTarget.worker();
|
||||
|
||||
const timeout = 10000;
|
||||
await waitExecutionFor(async () => {
|
||||
await worker?.evaluate(() => {
|
||||
if (typeof globalThis.setTimeout !== 'function') {
|
||||
throw new Error('Not ready');
|
||||
}
|
||||
});
|
||||
}, timeout);
|
||||
|
||||
const errorPromise = new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('Timeout waiting for Service Worker error'));
|
||||
}, 5000);
|
||||
(worker as unknown as CdpWebWorker).client.on(
|
||||
'Runtime.exceptionThrown',
|
||||
() => {
|
||||
clearTimeout(timeout);
|
||||
resolve(undefined);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
worker?.evaluate(() => {
|
||||
globalThis.setTimeout(() => {
|
||||
throw new Error('Intentional error from Service Worker');
|
||||
}, 100);
|
||||
});
|
||||
|
||||
await errorPromise;
|
||||
response2.resetResponseLineForTesting();
|
||||
|
||||
await listConsoleMessages({
|
||||
categoryExtensions: true,
|
||||
} as ParsedArguments).handler(
|
||||
{
|
||||
params: {serviceWorkerId: extensionId},
|
||||
page: context.getSelectedMcpPage(),
|
||||
},
|
||||
response2,
|
||||
context,
|
||||
);
|
||||
|
||||
const formattedResponse = await response2.handle('test', context);
|
||||
const textContent = getTextContent(formattedResponse.content[0]);
|
||||
|
||||
const sanitizedText = textContent.replaceAll(
|
||||
new RegExp(extensionId, 'g'),
|
||||
'<extension-id>',
|
||||
);
|
||||
|
||||
t.assert.snapshot?.(sanitizedText);
|
||||
|
||||
assert.ok(
|
||||
sanitizedText.includes('Service Worker starting...'),
|
||||
'Should contain start log',
|
||||
);
|
||||
assert.ok(
|
||||
sanitizedText.includes('This is a warning from Service Worker'),
|
||||
'Should contain warning log',
|
||||
);
|
||||
assert.ok(
|
||||
sanitizedText.includes('Intentional error from Service Worker'),
|
||||
'Should contain error log',
|
||||
);
|
||||
|
||||
await context.uninstallExtension(extensionId);
|
||||
const targets = context.browser.targets();
|
||||
assertNoServiceWorkerReported(targets, extensionId);
|
||||
},
|
||||
{},
|
||||
{
|
||||
categoryExtensions: true,
|
||||
} as ParsedArguments,
|
||||
);
|
||||
});
|
||||
|
||||
describe('list_console_messages', () => {
|
||||
it('list messages', async () => {
|
||||
await withMcpContext(async (response, context) => {
|
||||
|
||||
@@ -138,6 +138,7 @@ describe('extension', () => {
|
||||
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');
|
||||
await context.uninstallExtension(extensionId!);
|
||||
},
|
||||
{},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Test Extension for Logging",
|
||||
"version": "1.0",
|
||||
"background": {
|
||||
"service_worker": "sw.js"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// Minimal service worker for testing
|
||||
console.log('Service Worker starting...');
|
||||
console.warn('This is a warning from Service Worker');
|
||||
@@ -332,6 +332,8 @@ describe('script', () => {
|
||||
|
||||
const swId = context.getExtensionServiceWorkerId(sw);
|
||||
|
||||
await context.triggerExtensionAction(extensionId);
|
||||
|
||||
response.resetResponseLineForTesting();
|
||||
await evaluateScript({
|
||||
categoryExtensions: true,
|
||||
|
||||
@@ -408,3 +408,20 @@ export async function assertDaemonIsRunning(sessionId?: string) {
|
||||
'chrome-devtools-mcp daemon is not running',
|
||||
);
|
||||
}
|
||||
|
||||
export async function waitExecutionFor(
|
||||
func: () => Promise<void>,
|
||||
timeout: number,
|
||||
) {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < 10000) {
|
||||
try {
|
||||
await func();
|
||||
return;
|
||||
} catch {
|
||||
await new Promise(resolve => setTimeout(resolve, 100)); // wait and retry
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Timeout of ${timeout} reached.`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user