项目文件夹

文件
Alex Rudenko b4546ef86b
release-please / release-please (push) Has been cancelled
Compile and run tests / Tests on macos-latest with node 22 (push) Has been cancelled
Compile and run tests / Tests on ubuntu-latest with node 22 (push) Has been cancelled
Compile and run tests / Tests on windows-latest with node 22 (push) Has been cancelled
Compile and run tests / Tests on macos-latest with node 24 (push) Has been cancelled
Compile and run tests / Tests on ubuntu-latest with node 24 (push) Has been cancelled
Compile and run tests / Tests on windows-latest with node 24 (push) Has been cancelled
Compile and run tests / Tests on macos-latest with node 26 (push) Has been cancelled
Compile and run tests / Tests on ubuntu-latest with node 26 (push) Has been cancelled
Compile and run tests / Tests on windows-latest with node 26 (push) Has been cancelled
Check code before submitting / [Required] Check correct format (push) Has been cancelled
Check code before submitting / [Required] Check docs updated (push) Has been cancelled
Compile and run tests / [Required] Tests passed (push) Has been cancelled
test: fix potential flakiness in tests (#2396)
Assortment of various flakiness conditions found running tests in a loop
locally:


This PR introduces a comprehensive set of hermetic retry layers and
aggressive
timeout handlers across the test suite to insulate it from random
Chromium
startup hangs, CDP deadlocks, and Puppeteer lifecycle flakes. It
guarantees that
temporary browser infrastructure failures are automatically retried
without
failing the CI, while actual code assertion failures still fail fast.

### Test Harness & Retry Improvements

• tests/utils.ts: Rewrote withBrowser to include a 30-second internal
timeout and
a 3-attempt retry loop. If Chromium locks up or disconnects (Target
closed /
socket hang up), the browser is forcibly evicted (via SIGKILL if
browser.close()
hangs) and the test setup is cleanly retried.
• tests/index.test.ts: Wrapped withClient (used by E2E tests) in a
3-attempt
retry loop to handle the daemon/Chromium hanging during launch and
triggering the
60-second MCP client timeout.
• tests/browser.test.ts: Added a safeClose helper that imposes a
2-second timeout
before SIGKILLing browsers, and wrapped raw Puppeteer tests in
runWithRetry to
handle startup hangs.
• tests/shutdown.test.ts: Added a setupServerWithRetry helper to prevent
random
60s RPC timeouts when the server's Chrome instance hangs during boot.

### Flaky Operations & Navigation Fixes

• src/tools/performance.ts & tests/tools/performance.test.ts: Replaced
the
notoriously flaky waitUntil: ['networkidle0'] with 'load' when
navigating to
about:blank in performance_start_trace. This prevents random 10-second
Navigation
timeout exceeded errors. Also stubbed goto in the associated unit tests
for
better hermeticity.
• src/McpContext.ts: Wrapped browser.installExtension() with a 15-second
timeout
to prevent deadlocks when an extension fails to load.
• tests/tools/extensions.test.ts: Removed flaky headless UI navigations
to
chrome://extensions in favor of using the context.listExtensions() API.
• tests/tools/pages.test.js.snapshot: Synced test snapshots to reflect
updated
environment baselines.
2026-07-21 13:47:47 +00:00

215 行
6.2 KiB
TypeScript

/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import assert from 'node:assert';
import path from 'node:path';
import {afterEach, describe, it} from 'node:test';
import sinon from 'sinon';
import type {ParsedArguments} from '../../src/bin/chrome-devtools-mcp-cli-options.js';
import {listConsoleMessages} from '../../src/tools/console.js';
import {
installExtension,
uninstallExtension,
listExtensions,
reloadExtension,
triggerExtensionAction,
} from '../../src/tools/extensions.js';
import {serverHooks} from '../server.js';
import {
assertNoServiceWorkerReported,
extractExtensionId,
withMcpContext,
html,
getTextContent,
} from '../utils.js';
const EXTENSION_WITH_SW_PATH = path.join(
import.meta.dirname,
'../../../tests/tools/fixtures/extension-sw',
);
const EXTENSION_PATH = path.join(
import.meta.dirname,
'../../../tests/tools/fixtures/extension',
);
const EXTENSION_CONTENT_SCRIPT_PATH = path.join(
import.meta.dirname,
'../../../tests/tools/fixtures/extension-content-script',
);
describe('extension', () => {
const server = serverHooks();
afterEach(() => {
sinon.restore();
});
it('installs and uninstalls an extension and verifies it in chrome://extensions', async () => {
await withMcpContext(async (response, context) => {
// Install the extension
await installExtension.handler(
{params: {path: EXTENSION_PATH}},
response,
context,
);
const extensionId = extractExtensionId(response);
let extensions = await context.listExtensions();
assert.ok(
extensions.has(extensionId!),
`Extension with ID "${extensionId}" should be installed`,
);
// Uninstall the extension
await uninstallExtension.handler(
{params: {id: extensionId!}},
response,
context,
);
const uninstallResponseLine = response.responseLines[1];
assert.ok(
uninstallResponseLine.includes('Extension uninstalled'),
'Response should indicate uninstallation',
);
extensions = await context.listExtensions();
assert.ok(
!extensions.has(extensionId!),
`Extension with ID "${extensionId}" should NOT be installed`,
);
});
});
it('lists installed extensions', async () => {
await withMcpContext(async (response, context) => {
const setListExtensionsSpy = sinon.spy(response, 'setListExtensions');
await listExtensions.handler({params: {}}, response, context);
assert.ok(
setListExtensionsSpy.calledOnce,
'setListExtensions should be called',
);
});
});
it('reloads an extension', async () => {
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();
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 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');
await context.uninstallExtension(extensionId!);
},
{},
{
categoryExtensions: true,
},
);
});
it('triggers an extension action', async () => {
await withMcpContext(
async (response, context) => {
const extensionId = await context.installExtension(
EXTENSION_WITH_SW_PATH,
);
const targetsBefore = context.browser.targets();
const pageTargetBefore = targetsBefore.find(
t => t.type() === 'page' && t.url().includes(extensionId),
);
assert.ok(!pageTargetBefore, 'Page should not exist before action');
await triggerExtensionAction.handler(
{params: {id: extensionId}},
response,
context,
);
const pageTargetAfter = await context.browser.waitForTarget(
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);
},
{},
{
categoryExtensions: true,
},
);
});
it('verifies that content script console logs are received', async () => {
await withMcpContext(
async (response, context) => {
server.addHtmlRoute(
'/test-content-script',
html`<h1>Test Content Script</h1>`,
);
const url = server.getRoute('/test-content-script');
const extensionId = await context.installExtension(
EXTENSION_CONTENT_SCRIPT_PATH,
);
const mcpPage = context.getSelectedMcpPage();
const page = mcpPage.pptrPage;
await page.goto(url);
await listConsoleMessages({
categoryExtensions: true,
} as ParsedArguments).handler(
{params: {includePreservedMessages: true}, page: mcpPage},
response,
context,
);
const result = await response.handle(context);
const consoleOutput = getTextContent(result.content[0]);
assert.ok(
consoleOutput.includes('from content script!'),
`Console output should contain message from content script. Got: ${consoleOutput}`,
);
await context.uninstallExtension(extensionId);
},
{},
{
categoryExtensions: true,
},
);
});
});