mirror of
https://github.com/ChromeDevTools/chrome-devtools-mcp.git
synced 2026-11-04 15:00:11 +00:00
feat: add --maxNavigationSaved flag to configure retained navigation history (#3018)
Network requests and console messages were only retained for the last 3 navigations per page. Flows that bounce through many pages, such as sign-in flows, OAuth handshakes, SSO, or corporate networks with proxies and captive portals, can easily exceed that, so earlier hops were dropped before an agent could inspect them. The new `--maxNavigationSaved` flag (default: 3) lets users raise the limit so agents can trace the full redirect chain end to end. For earlier navigations, request URLs, status codes, and request and response headers (including Location and Set-Cookie) are retained, but response bodies are generally no longer available. This is sufficient for network analysis of redirect hops. Each retained navigation can hold up to 1,000 network requests and 10,000 console messages, so the value is capped at 25 to avoid running out of memory. Values outside 1-25 are rejected at startup, whether passed on the command line or in a config file. The value is passed from the server args through `McpContext` and `McpPage` to the network and console collectors. The `includePreservedRequests` and `includePreservedMessages` tool descriptions now show the configured value instead of a hardcoded 3.
This commit is contained in:
1 parent
bf8c6d9bc7
commit
a580d1320e
16 files changed
+383
-31
No files matched your search
@@ -219,6 +219,11 @@ The Chrome DevTools MCP server supports the following configuration option:
|
||||
- **Type:** boolean
|
||||
- **Default:** `true`
|
||||
|
||||
- **`--maxNavigationSaved`/ `--max-navigation-saved`**
|
||||
Number of navigations (including the current one) for which network requests and console messages are retained per page. Must be between 1 and 25. For earlier navigations, request URLs, status codes, and request and response headers are retained, which is sufficient to trace redirect hops, but response bodies are generally no longer available.
|
||||
- **Type:** number
|
||||
- **Default:** `3`
|
||||
|
||||
- **`--slim`**
|
||||
Exposes a "slim" set of 3 tools covering navigation, script execution and screenshots only. Useful for basic browser tasks.
|
||||
- **Type:** boolean
|
||||
|
||||
@@ -410,6 +410,7 @@ async function generateToolDocumentation(): Promise<void> {
|
||||
const tools = createTools({
|
||||
pageIdRouting: true,
|
||||
categoryExtensions: true,
|
||||
maxNavigationSaved: mcpOptions.maxNavigationSaved.default,
|
||||
} as ParsedArguments);
|
||||
|
||||
{
|
||||
|
||||
+20
-14
@@ -329,20 +329,26 @@ export class McpPage implements ContextPage {
|
||||
}
|
||||
this.#pptrPage = page;
|
||||
page.on('dialog', this.#dialogHandler);
|
||||
this.#networkCollector = new NetworkCollector(page);
|
||||
this.#consoleCollector = new ConsoleCollector(page, collect => {
|
||||
return {
|
||||
console: event => {
|
||||
collect(event);
|
||||
},
|
||||
uncaughtError: event => {
|
||||
collect(event);
|
||||
},
|
||||
devtoolsAggregatedIssue: event => {
|
||||
collect(event);
|
||||
},
|
||||
};
|
||||
this.#networkCollector = new NetworkCollector(page, {
|
||||
maxNavigationSaved: this.#config.resolved.maxNavigationSaved,
|
||||
});
|
||||
this.#consoleCollector = new ConsoleCollector(
|
||||
page,
|
||||
collect => {
|
||||
return {
|
||||
console: event => {
|
||||
collect(event);
|
||||
},
|
||||
uncaughtError: event => {
|
||||
collect(event);
|
||||
},
|
||||
devtoolsAggregatedIssue: event => {
|
||||
collect(event);
|
||||
},
|
||||
};
|
||||
},
|
||||
{maxNavigationSaved: this.#config.resolved.maxNavigationSaved},
|
||||
);
|
||||
}
|
||||
this.updateTimeouts();
|
||||
await this.#initDevToolsUniverseNoThrow();
|
||||
@@ -1436,7 +1442,7 @@ export class McpPage implements ContextPage {
|
||||
this.networkCollector.dispose();
|
||||
this.networkCollector = new NetworkCollector(
|
||||
this.pptrPage,
|
||||
undefined,
|
||||
{maxNavigationSaved: this.#config.resolved.maxNavigationSaved},
|
||||
collect => {
|
||||
return {
|
||||
request: req => {
|
||||
|
||||
@@ -24,6 +24,8 @@ import {
|
||||
} from '../utils/id.js';
|
||||
import {logger} from '../utils/logger.js';
|
||||
|
||||
import {DEFAULT_MAX_NAVIGATION_SAVED} from './constants.js';
|
||||
|
||||
export class UncaughtError {
|
||||
readonly details: Protocol.Runtime.ExceptionDetails;
|
||||
readonly targetId: string;
|
||||
@@ -57,12 +59,17 @@ export type ListenerMap<EventMap extends PageEvents = PageEvents> = {
|
||||
]?: (event: EventMap[K]) => void;
|
||||
};
|
||||
|
||||
export interface PageCollectorOptions {
|
||||
maxResourcesPerNavigation?: number;
|
||||
maxNavigationSaved?: number;
|
||||
}
|
||||
|
||||
export class PageCollector<T> {
|
||||
protected pptrPage: Page;
|
||||
#listeners?: ListenerMap<PageEvents>;
|
||||
#pendingSameDocumentNavigation = false;
|
||||
#frameNavigatedWithinDocumentTarget?: Frame;
|
||||
protected maxNavigationSaved = 3;
|
||||
protected readonly maxNavigationSaved: number;
|
||||
|
||||
/**
|
||||
* This maps a Page to a list of navigations with a sub-list
|
||||
@@ -74,9 +81,13 @@ export class PageCollector<T> {
|
||||
constructor(
|
||||
page: Page,
|
||||
listeners: (collector: (item: T) => void) => ListenerMap<PageEvents>,
|
||||
maxResourcesPerNavigation?: number,
|
||||
{
|
||||
maxResourcesPerNavigation,
|
||||
maxNavigationSaved = DEFAULT_MAX_NAVIGATION_SAVED,
|
||||
}: PageCollectorOptions = {},
|
||||
) {
|
||||
this.pptrPage = page;
|
||||
this.maxNavigationSaved = maxNavigationSaved;
|
||||
this.#attachFrameNavigatedWithinDocumentListener();
|
||||
|
||||
const idGenerator = createIdGenerator();
|
||||
@@ -219,9 +230,12 @@ export class ConsoleCollector extends PageCollector<
|
||||
item: ConsoleMessage | Error | DevTools.AggregatedIssue | UncaughtError,
|
||||
) => void,
|
||||
) => ListenerMap<PageEvents>,
|
||||
maxMessagesPerNavigation = ConsoleCollector.MAX_MESSAGES_PER_NAVIGATION,
|
||||
{
|
||||
maxResourcesPerNavigation = ConsoleCollector.MAX_MESSAGES_PER_NAVIGATION,
|
||||
maxNavigationSaved,
|
||||
}: PageCollectorOptions = {},
|
||||
) {
|
||||
super(page, listeners, maxMessagesPerNavigation);
|
||||
super(page, listeners, {maxResourcesPerNavigation, maxNavigationSaved});
|
||||
this.#subscriber = new PageEventSubscriber(this.pptrPage);
|
||||
this.#subscriber.subscribe();
|
||||
}
|
||||
@@ -392,7 +406,10 @@ export class NetworkCollector extends PageCollector<HTTPRequest> {
|
||||
|
||||
constructor(
|
||||
page: Page,
|
||||
maxRequestsPerNavigation = NetworkCollector.MAX_REQUESTS_PER_NAVIGATION,
|
||||
{
|
||||
maxResourcesPerNavigation = NetworkCollector.MAX_REQUESTS_PER_NAVIGATION,
|
||||
maxNavigationSaved,
|
||||
}: PageCollectorOptions = {},
|
||||
listeners: (
|
||||
collector: (item: HTTPRequest) => void,
|
||||
) => ListenerMap<PageEvents> = collect => {
|
||||
@@ -403,7 +420,7 @@ export class NetworkCollector extends PageCollector<HTTPRequest> {
|
||||
} as ListenerMap;
|
||||
},
|
||||
) {
|
||||
super(page, listeners, maxRequestsPerNavigation);
|
||||
super(page, listeners, {maxResourcesPerNavigation, maxNavigationSaved});
|
||||
}
|
||||
override splitAfterNavigation(): void {
|
||||
const requests = this.storage[0];
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
export const DEFAULT_MAX_NAVIGATION_SAVED = 3;
|
||||
@@ -4,6 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {DEFAULT_MAX_NAVIGATION_SAVED} from '../collectors/constants.js';
|
||||
import type {YargsOptions} from '../third_party/index.js';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
@@ -22,6 +23,9 @@ export interface McpOption extends YargsOptions {
|
||||
}
|
||||
|
||||
export const DEFAULT_FILESYSTEM_ROOT = [os.tmpdir()];
|
||||
// Each retained navigation can hold up to 1,000 network requests and 10,000
|
||||
// console messages, so the history is capped to avoid running out of memory.
|
||||
export const MAX_NAVIGATION_SAVED_LIMIT = 25;
|
||||
|
||||
export const mcpOptions = {
|
||||
...getCategoryOptions(),
|
||||
@@ -170,6 +174,26 @@ export const mcpOptions = {
|
||||
'Whether to enable source maps in DevTools. Use --no-source-maps to disable.',
|
||||
reloadable: true,
|
||||
},
|
||||
maxNavigationSaved: {
|
||||
type: 'number',
|
||||
default: DEFAULT_MAX_NAVIGATION_SAVED,
|
||||
describe: `Number of navigations (including the current one) for which network requests and console messages are retained per page. Must be between 1 and ${MAX_NAVIGATION_SAVED_LIMIT}. For earlier navigations, request URLs, status codes, and request and response headers are retained, which is sufficient to trace redirect hops, but response bodies are generally no longer available.`,
|
||||
coerce: (value: number | undefined) => {
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!Number.isInteger(value) ||
|
||||
value <= 0 ||
|
||||
value > MAX_NAVIGATION_SAVED_LIMIT
|
||||
) {
|
||||
throw new Error(
|
||||
`Invalid maxNavigationSaved ${value}. Expected an integer between 1 and ${MAX_NAVIGATION_SAVED_LIMIT}.`,
|
||||
);
|
||||
}
|
||||
return value;
|
||||
},
|
||||
},
|
||||
clearcutEndpoint: {
|
||||
type: 'string',
|
||||
hidden: true,
|
||||
|
||||
@@ -478,5 +478,9 @@
|
||||
{
|
||||
"name": "watch_config",
|
||||
"flagType": "boolean"
|
||||
},
|
||||
{
|
||||
"name": "max_navigation_saved_present",
|
||||
"flagType": "boolean"
|
||||
}
|
||||
]
|
||||
@@ -44,7 +44,7 @@ export const listConsoleMessages = definePageTool(
|
||||
(cliArgs: ParsedArguments) => {
|
||||
return {
|
||||
name: LIST_CONSOLE_MESSAGES_TOOL_NAME,
|
||||
description: `List all console messages for the target page since the last navigation.${cliArgs?.categoryExtensions ? ' This includes console messages originating from extensions content scripts.' : ''}`,
|
||||
description: `List all console messages for the target page since the last navigation.${cliArgs.categoryExtensions ? ' This includes console messages originating from extensions content scripts.' : ''}`,
|
||||
annotations: {
|
||||
category: ToolCategory.DEBUGGING,
|
||||
readOnlyHint: true,
|
||||
@@ -77,7 +77,7 @@ export const listConsoleMessages = definePageTool(
|
||||
.default(false)
|
||||
.optional()
|
||||
.describe(
|
||||
'Set to true to return the preserved messages over the last 3 navigations.',
|
||||
`Set to true to return the preserved messages over the last ${cliArgs.maxNavigationSaved} navigations.`,
|
||||
),
|
||||
includeStackTraces: zod
|
||||
.boolean()
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {ParsedArguments} from '../config/ConfigParser.js';
|
||||
import {zod} from '../third_party/index.js';
|
||||
import type {ResourceType} from '../third_party/index.js';
|
||||
|
||||
@@ -32,7 +33,7 @@ const FILTERABLE_RESOURCE_TYPES: readonly [ResourceType, ...ResourceType[]] = [
|
||||
'other',
|
||||
];
|
||||
|
||||
export const listNetworkRequests = definePageTool(() => ({
|
||||
export const listNetworkRequests = definePageTool((args: ParsedArguments) => ({
|
||||
name: 'list_network_requests',
|
||||
description: `Lists the most recent requests for the target page since the last navigation.`,
|
||||
annotations: {
|
||||
@@ -67,7 +68,7 @@ export const listNetworkRequests = definePageTool(() => ({
|
||||
.default(false)
|
||||
.optional()
|
||||
.describe(
|
||||
'Set to true to return the preserved requests over the last 3 navigations.',
|
||||
`Set to true to return the preserved requests over the last ${args.maxNavigationSaved} navigations.`,
|
||||
),
|
||||
},
|
||||
blockedByDialog: false,
|
||||
|
||||
@@ -9,6 +9,7 @@ import {describe, it} from 'node:test';
|
||||
|
||||
import sinon from 'sinon';
|
||||
|
||||
import {UncaughtError} from '../src/collectors/PageCollector.js';
|
||||
import type {ReadonlyServerConfig} from '../src/config/ServerConfig.js';
|
||||
import type {TargetUniverse} from '../src/devtools/DevtoolsUtils.js';
|
||||
import {McpPage, replaceHtmlElementsWithUids} from '../src/McpPage.js';
|
||||
@@ -314,6 +315,53 @@ describe('McpPage', () => {
|
||||
}
|
||||
|
||||
describe('lazy initialization', () => {
|
||||
for (const replaceNetworkCollector of [false, true]) {
|
||||
it(`forwards the navigation limit to collectors (replacement: ${replaceNetworkCollector})`, async () => {
|
||||
const pptrPage = createMockPuppeteerPage();
|
||||
const target = createMockPuppeteerTarget({page: pptrPage});
|
||||
const mcpPage = new McpPage(target, 1, {
|
||||
config: createMockServerConfig({maxNavigationSaved: 5}),
|
||||
locatorClass: Locator,
|
||||
});
|
||||
await mcpPage.init();
|
||||
if (replaceNetworkCollector) {
|
||||
await mcpPage.setUpNetworkCollectorForTesting();
|
||||
}
|
||||
const requests = [];
|
||||
const errors = [];
|
||||
for (let navigation = 0; navigation < 7; navigation++) {
|
||||
const request = getMockRequest({
|
||||
url: `http://example.com/nav${navigation}`,
|
||||
navigationRequest: true,
|
||||
frame: pptrPage.mainFrame(),
|
||||
});
|
||||
requests.push(request);
|
||||
pptrPage.emit('request', request);
|
||||
pptrPage.emit('framenavigated', pptrPage.mainFrame());
|
||||
const error = new UncaughtError(
|
||||
{
|
||||
exceptionId: navigation,
|
||||
text: `error-${navigation}`,
|
||||
lineNumber: 0,
|
||||
columnNumber: 0,
|
||||
},
|
||||
'<mock>',
|
||||
);
|
||||
errors.push(error);
|
||||
pptrPage.emit('uncaughtError', error);
|
||||
}
|
||||
assert.deepStrictEqual(
|
||||
mcpPage.networkCollector.getData(true),
|
||||
requests.slice(-5),
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
mcpPage.consoleCollector.getData(true),
|
||||
errors.slice(-5),
|
||||
);
|
||||
mcpPage.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
it('does not initialize Puppeteer Page in constructor and throws if pptrPage is accessed before init()', async () => {
|
||||
const pptrPage = createMockPuppeteerPage();
|
||||
pptrPage.emulateFocusedPage.resolves();
|
||||
|
||||
@@ -97,6 +97,24 @@ describe('McpServer', () => {
|
||||
return tools.map(tool => tool.name);
|
||||
}
|
||||
|
||||
it('passes the navigation limit to the lazily connected context', async () => {
|
||||
const context = createMockMcpContext();
|
||||
const createContext = sinon.stub(McpContext, 'create').returns(context);
|
||||
const {server, browserManager, config} = await createTestServer(
|
||||
['--maxNavigationSaved=5'],
|
||||
{mockContext: false},
|
||||
);
|
||||
|
||||
sinon.assert.calledOnceWithExactly(
|
||||
createContext,
|
||||
sinon.match.any,
|
||||
sinon.match({config: sinon.match.same(config)}),
|
||||
);
|
||||
assert.strictEqual(config.resolved.maxNavigationSaved, 5);
|
||||
sinon.assert.notCalled(browserManager.ensureBrowser);
|
||||
await server.close();
|
||||
});
|
||||
|
||||
describe('callTool', () => {
|
||||
it('returns an error for unknown tool names', async () => {
|
||||
const {server, browserManager} = await createTestServer();
|
||||
@@ -351,6 +369,18 @@ describe('McpServer', () => {
|
||||
});
|
||||
|
||||
describe('applyConfig', () => {
|
||||
it('keeps maxNavigationSaved unchanged when configuration is reloaded', async () => {
|
||||
const {server, config, browserManager} = await createTestServer([
|
||||
'--maxNavigationSaved=5',
|
||||
]);
|
||||
|
||||
await server.applyConfig(parseConfig(['--maxNavigationSaved=10']));
|
||||
|
||||
assert.strictEqual(config.resolved.maxNavigationSaved, 5);
|
||||
sinon.assert.notCalled(browserManager.ensureBrowser);
|
||||
await server.close();
|
||||
});
|
||||
|
||||
it('enables tools that were disabled', async () => {
|
||||
const {server} = await createTestServer();
|
||||
assert.ok(
|
||||
|
||||
@@ -10,12 +10,14 @@ import {describe, it} from 'node:test';
|
||||
|
||||
import sinon from 'sinon';
|
||||
|
||||
import {DEFAULT_MAX_NAVIGATION_SAVED} from '../src/collectors/constants.js';
|
||||
import {ConfigLocator} from '../src/config/ConfigLocator.js';
|
||||
import {ConfigParser} from '../src/config/ConfigParser.js';
|
||||
import {
|
||||
getCliOptions,
|
||||
mcpOptions,
|
||||
DEFAULT_FILESYSTEM_ROOT,
|
||||
MAX_NAVIGATION_SAVED_LIMIT,
|
||||
} from '../src/config/mcp-options.js';
|
||||
import {
|
||||
buildCommand,
|
||||
@@ -70,6 +72,7 @@ describe('cli args parsing', () => {
|
||||
experimentalStructuredContent: false,
|
||||
pageIdRouting: true,
|
||||
sourceMaps: true,
|
||||
maxNavigationSaved: 3,
|
||||
clearcutIncludePidHeader: false,
|
||||
screenshotFormat: 'png',
|
||||
slim: false,
|
||||
@@ -701,6 +704,52 @@ describe('cli args parsing', () => {
|
||||
assert.strictEqual(explicitTrueArgs.sourceMaps, true);
|
||||
});
|
||||
|
||||
it('parses maxNavigationSaved flag', async () => {
|
||||
const defaultParsed = parseConfig(['main.js']);
|
||||
assert.strictEqual(defaultParsed.maxNavigationSaved, 3);
|
||||
assert.strictEqual(
|
||||
defaultParsed.maxNavigationSaved,
|
||||
DEFAULT_MAX_NAVIGATION_SAVED,
|
||||
);
|
||||
|
||||
const camelCaseArgs = parseConfig(['--maxNavigationSaved', '5']);
|
||||
assert.strictEqual(camelCaseArgs.maxNavigationSaved, 5);
|
||||
|
||||
const kebabCaseArgs = parseConfig(['--max-navigation-saved=1']);
|
||||
assert.strictEqual(kebabCaseArgs.maxNavigationSaved, 1);
|
||||
|
||||
assert.throws(
|
||||
() => parseConfig(['--maxNavigationSaved', '26']),
|
||||
/Invalid maxNavigationSaved 26/,
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects invalid maxNavigationSaved values', async () => {
|
||||
const coerce = mcpOptions.maxNavigationSaved.coerce;
|
||||
|
||||
assert.strictEqual(coerce(undefined), undefined);
|
||||
assert.strictEqual(coerce(1), 1);
|
||||
assert.strictEqual(
|
||||
coerce(MAX_NAVIGATION_SAVED_LIMIT),
|
||||
MAX_NAVIGATION_SAVED_LIMIT,
|
||||
);
|
||||
|
||||
for (const value of [
|
||||
0,
|
||||
-1,
|
||||
2.5,
|
||||
Number.NaN,
|
||||
MAX_NAVIGATION_SAVED_LIMIT + 1,
|
||||
Number.MAX_SAFE_INTEGER,
|
||||
Number.POSITIVE_INFINITY,
|
||||
]) {
|
||||
assert.throws(
|
||||
() => coerce(value),
|
||||
/Invalid maxNavigationSaved .* Expected an integer between 1 and 25\./,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('parses config option', async () => {
|
||||
using testConfig = createTempFile(
|
||||
JSON.stringify({
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
} from '../../src/collectors/PageCollector.js';
|
||||
import {DevTools, FrameEvent} from '../../src/third_party/index.js';
|
||||
|
||||
import {createMockPuppeteerPage} from '../mocks.js';
|
||||
import {getMockRequest, getMockBrowser} from '../utils.js';
|
||||
|
||||
function createMockConsoleMessage(text: string): ConsoleMessage {
|
||||
@@ -34,6 +35,35 @@ function createMockConsoleMessage(text: string): ConsoleMessage {
|
||||
}
|
||||
|
||||
describe('PageCollector', () => {
|
||||
it('applies resource and navigation limits independently', () => {
|
||||
const pptrPage = createMockPuppeteerPage();
|
||||
const collector = new PageCollector<Error>(
|
||||
pptrPage,
|
||||
collect => ({
|
||||
pageerror: error => {
|
||||
if (error instanceof Error) {
|
||||
collect(error);
|
||||
}
|
||||
},
|
||||
}),
|
||||
{maxResourcesPerNavigation: 2, maxNavigationSaved: 4},
|
||||
);
|
||||
const errors: Error[] = [];
|
||||
for (let navigation = 0; navigation < 6; navigation++) {
|
||||
pptrPage.emit('framenavigated', pptrPage.mainFrame());
|
||||
for (let resource = 0; resource < 3; resource++) {
|
||||
const error = new Error(`${navigation}-${resource}`);
|
||||
pptrPage.emit('pageerror', error);
|
||||
if (resource > 0) {
|
||||
errors.push(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.deepStrictEqual(collector.getData(), errors.slice(-2));
|
||||
assert.deepStrictEqual(collector.getData(true), errors.slice(-8));
|
||||
collector.dispose();
|
||||
});
|
||||
|
||||
it('works', async () => {
|
||||
const browser = getMockBrowser();
|
||||
const page = (await browser.pages())[0];
|
||||
@@ -189,6 +219,40 @@ describe('PageCollector', () => {
|
||||
});
|
||||
|
||||
describe('NetworkCollector', () => {
|
||||
it('applies both limits while retaining navigation requests', () => {
|
||||
const pptrPage = createMockPuppeteerPage();
|
||||
const collector = new NetworkCollector(pptrPage, {
|
||||
maxResourcesPerNavigation: 2,
|
||||
maxNavigationSaved: 4,
|
||||
});
|
||||
const requests: HTTPRequest[] = [];
|
||||
for (let navigation = 0; navigation < 6; navigation++) {
|
||||
const request = getMockRequest({
|
||||
navigationRequest: true,
|
||||
frame: pptrPage.mainFrame(),
|
||||
});
|
||||
pptrPage.emit('request', request);
|
||||
pptrPage.emit('framenavigated', pptrPage.mainFrame());
|
||||
for (let resource = 0; resource < 3; resource++) {
|
||||
const request = getMockRequest();
|
||||
pptrPage.emit('request', request);
|
||||
if (resource > 0) {
|
||||
requests.push(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.deepStrictEqual(collector.getData(), requests.slice(-2));
|
||||
// The next navigation request arrives before framenavigated, evicting
|
||||
// the oldest resource in each completed navigation.
|
||||
assert.deepStrictEqual(collector.getData(true), [
|
||||
requests[5],
|
||||
requests[7],
|
||||
requests[9],
|
||||
...requests.slice(-2),
|
||||
]);
|
||||
collector.dispose();
|
||||
});
|
||||
|
||||
it('retains only the newest requests without navigation', async () => {
|
||||
const browser = getMockBrowser();
|
||||
const page = (await browser.pages())[0];
|
||||
@@ -337,6 +401,25 @@ describe('NetworkCollector', () => {
|
||||
assert.equal(collector.getData(true).length, 3);
|
||||
});
|
||||
|
||||
it('respects a custom maxNavigationSaved', async () => {
|
||||
const browser = getMockBrowser();
|
||||
const page = (await browser.pages())[0];
|
||||
const mainFrame = page.mainFrame();
|
||||
const collector = new NetworkCollector(page, {maxNavigationSaved: 5});
|
||||
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const req = getMockRequest({
|
||||
url: `http://example.com/nav${i}`,
|
||||
navigationRequest: true,
|
||||
frame: mainFrame,
|
||||
});
|
||||
page.emit('request', req);
|
||||
page.emit('framenavigated', mainFrame);
|
||||
}
|
||||
|
||||
assert.equal(collector.getData(true).length, 5);
|
||||
});
|
||||
|
||||
it('bounds retained navigation buckets with redirects and subframes', async () => {
|
||||
class TestNetworkCollector extends NetworkCollector {
|
||||
get navigationSizes(): number[] {
|
||||
@@ -347,7 +430,9 @@ describe('NetworkCollector', () => {
|
||||
const browser = getMockBrowser();
|
||||
const page = (await browser.pages())[0];
|
||||
const mainFrame = page.mainFrame();
|
||||
const collector = new TestNetworkCollector(page, 3);
|
||||
const collector = new TestNetworkCollector(page, {
|
||||
maxResourcesPerNavigation: 3,
|
||||
});
|
||||
|
||||
const firstNavigation = getMockRequest({
|
||||
url: 'http://example.com/first',
|
||||
@@ -431,6 +516,44 @@ describe('ConsoleCollector', () => {
|
||||
};
|
||||
});
|
||||
|
||||
for (const maxResourcesPerNavigation of [undefined, 2]) {
|
||||
it(`retains a custom navigation history with resource limit ${maxResourcesPerNavigation}`, () => {
|
||||
const pptrPage = createMockPuppeteerPage();
|
||||
const collector = new ConsoleCollector(
|
||||
pptrPage,
|
||||
collect => ({
|
||||
pageerror: error => {
|
||||
if (error instanceof Error) {
|
||||
collect(error);
|
||||
}
|
||||
},
|
||||
}),
|
||||
{maxResourcesPerNavigation, maxNavigationSaved: 4},
|
||||
);
|
||||
const errors: Error[] = [];
|
||||
for (let navigation = 0; navigation < 6; navigation++) {
|
||||
pptrPage.emit('framenavigated', pptrPage.mainFrame());
|
||||
for (let resource = 0; resource < 3; resource++) {
|
||||
const error = new Error(`${navigation}-${resource}`);
|
||||
pptrPage.emit('pageerror', error);
|
||||
if (maxResourcesPerNavigation === undefined || resource > 0) {
|
||||
errors.push(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
const retainedPerNavigation = maxResourcesPerNavigation ?? 3;
|
||||
assert.deepStrictEqual(
|
||||
collector.getData(),
|
||||
errors.slice(-retainedPerNavigation),
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
collector.getData(true),
|
||||
errors.slice(-4 * retainedPerNavigation),
|
||||
);
|
||||
collector.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
it('collects issues', async () => {
|
||||
const browser = getMockBrowser();
|
||||
const page = (await browser.pages())[0];
|
||||
@@ -554,7 +677,7 @@ describe('ConsoleCollector', () => {
|
||||
},
|
||||
} as ListenerMap;
|
||||
},
|
||||
cap,
|
||||
{maxResourcesPerNavigation: cap},
|
||||
);
|
||||
|
||||
const messages = Array.from({length: cap + 3}, (_, i) =>
|
||||
|
||||
+3
-3
@@ -268,9 +268,8 @@ export function createMockCDPSession(): sinon.SinonStubbedInstance<MockCDPSessio
|
||||
}
|
||||
|
||||
export function createMockPuppeteerPage(): MockPuppeteerPage {
|
||||
const page = sinon.createStubInstance(
|
||||
CdpPage,
|
||||
) as unknown as sinon.SinonStubbedInstance<Page>;
|
||||
const pptrPage = sinon.createStubInstance(CdpPage);
|
||||
const page = pptrPage as unknown as sinon.SinonStubbedInstance<Page>;
|
||||
const pageListener = mockListener();
|
||||
page.listenerCount.callsFake(pageListener.listenerCount);
|
||||
page.on.callsFake((eventName, handler) => {
|
||||
@@ -292,6 +291,7 @@ export function createMockPuppeteerPage(): MockPuppeteerPage {
|
||||
// PageCollector can subscribe to FrameNavigatedWithinDocument and tests
|
||||
// can trigger it.
|
||||
const mainFrameStub = sinon.createStubInstance(CdpFrame);
|
||||
mainFrameStub.page.returns(pptrPage);
|
||||
const mainFrameListener = mockListener();
|
||||
mainFrameStub.on.callsFake((eventName, handler) => {
|
||||
mainFrameListener.on(eventName, handler);
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
listConsoleMessages,
|
||||
} from '../../src/tools/console.js';
|
||||
import {installExtension} from '../../src/tools/extensions.js';
|
||||
import {createMockServerConfig} from '../mocks.js';
|
||||
import {createMockParsedArguments, createMockServerConfig} from '../mocks.js';
|
||||
import {serverHooks} from '../server.js';
|
||||
import {
|
||||
getTextContent,
|
||||
@@ -143,6 +143,24 @@ describe('console', () => {
|
||||
});
|
||||
|
||||
describe('list_console_messages', () => {
|
||||
it('describes the default preserved navigation history', () => {
|
||||
const tool = listConsoleMessages(createMockParsedArguments());
|
||||
assert.strictEqual(
|
||||
tool.schema.includePreservedMessages.description,
|
||||
'Set to true to return the preserved messages over the last 3 navigations.',
|
||||
);
|
||||
});
|
||||
|
||||
it('describes preserved messages using maxNavigationSaved', () => {
|
||||
const tool = listConsoleMessages(
|
||||
createMockParsedArguments({maxNavigationSaved: 5}),
|
||||
);
|
||||
assert.strictEqual(
|
||||
tool.schema.includePreservedMessages.description,
|
||||
'Set to true to return the preserved messages over the last 5 navigations.',
|
||||
);
|
||||
});
|
||||
|
||||
it('list messages', async () => {
|
||||
await withMcpContext(async (response, context, args) => {
|
||||
await listConsoleMessages(args).handler(
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import assert from 'node:assert';
|
||||
import {describe, it} from 'node:test';
|
||||
|
||||
import sinon from 'sinon';
|
||||
@@ -12,10 +13,28 @@ import {
|
||||
getNetworkRequest,
|
||||
listNetworkRequests,
|
||||
} from '../../src/tools/network.js';
|
||||
import {createHandlerMocks} from '../mocks.js';
|
||||
import {createHandlerMocks, createMockParsedArguments} from '../mocks.js';
|
||||
|
||||
describe('network', () => {
|
||||
describe('list_network_requests', () => {
|
||||
it('describes the default preserved navigation history', () => {
|
||||
const tool = listNetworkRequests(createMockParsedArguments());
|
||||
assert.strictEqual(
|
||||
tool.schema.includePreservedRequests.description,
|
||||
'Set to true to return the preserved requests over the last 3 navigations.',
|
||||
);
|
||||
});
|
||||
|
||||
it('describes preserved requests using maxNavigationSaved', () => {
|
||||
const tool = listNetworkRequests(
|
||||
createMockParsedArguments({maxNavigationSaved: 5}),
|
||||
);
|
||||
assert.strictEqual(
|
||||
tool.schema.includePreservedRequests.description,
|
||||
'Set to true to return the preserved requests over the last 5 navigations.',
|
||||
);
|
||||
});
|
||||
|
||||
it('handles default parameters', async () => {
|
||||
const {page, context, response, args} = createHandlerMocks();
|
||||
page.getDevToolsData.resolves({});
|
||||
|
||||
Reference in new issue
Block a user