mirror of
https://github.com/ChromeDevTools/chrome-devtools-mcp.git
synced 2026-09-14 19:45:30 +08:00
refactor: move remaining McpContext getters (#2342)
Moves various getters to McpPage.
This commit is contained in:
+7
-235
@@ -20,22 +20,15 @@ import type {
|
||||
DuplicateStringGroup,
|
||||
} from './HeapSnapshotManager.js';
|
||||
import {McpPage} from './McpPage.js';
|
||||
import {
|
||||
NetworkCollector,
|
||||
type ListenerMap,
|
||||
type UncaughtError,
|
||||
} from './PageCollector.js';
|
||||
import {type UncaughtError} from './PageCollector.js';
|
||||
import {ServiceWorkerConsoleCollector} from './ServiceWorkerCollector.js';
|
||||
import {
|
||||
Locator,
|
||||
PredefinedNetworkConditions,
|
||||
type Browser,
|
||||
type BrowserContext,
|
||||
type ConsoleMessage,
|
||||
type HTTPRequest,
|
||||
type Page,
|
||||
type ScreenRecorder,
|
||||
type Viewport,
|
||||
type Target,
|
||||
type Extension,
|
||||
type Root,
|
||||
@@ -46,15 +39,8 @@ import {CLOSE_PAGE_ERROR} from './tools/ToolDefinition.js';
|
||||
import type {Context, SupportedExtensions} from './tools/ToolDefinition.js';
|
||||
import type {TraceResult} from './trace-processing/parse.js';
|
||||
import type {Logger} from './types.js';
|
||||
import type {
|
||||
EmulationSettings,
|
||||
GeolocationOptions,
|
||||
ExtensionServiceWorker,
|
||||
} from './types.js';
|
||||
import type {ExtensionServiceWorker} from './types.js';
|
||||
import {getTempFilePath, resolveCanonicalPath} from './utils/files.js';
|
||||
import {type WithSymbolId, stableIdSymbol} from './utils/id.js';
|
||||
import {getNetworkMultiplierFromString} from './WaitForHelper.js';
|
||||
|
||||
interface McpContextOptions {
|
||||
// Whether the DevTools windows are exposed as pages for debugging of DevTools.
|
||||
experimentalDevToolsDebugging: boolean;
|
||||
@@ -72,9 +58,6 @@ interface McpContextOptions {
|
||||
allowUnrestrictedPaths?: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_TIMEOUT = 5_000;
|
||||
const NAVIGATION_TIMEOUT = 10_000;
|
||||
|
||||
export class McpContext implements Context {
|
||||
browser: Browser;
|
||||
logger: Logger;
|
||||
@@ -305,12 +288,6 @@ export class McpContext implements Context {
|
||||
return page.devtoolsUniverse ?? null;
|
||||
}
|
||||
|
||||
getConsoleMessageStableId(
|
||||
message: ConsoleMessage | Error | DevTools.AggregatedIssue | UncaughtError,
|
||||
): number {
|
||||
return (message as WithSymbolId<typeof message>)[stableIdSymbol] ?? -1;
|
||||
}
|
||||
|
||||
async newPage(
|
||||
background?: boolean,
|
||||
isolatedContextName?: string,
|
||||
@@ -343,136 +320,10 @@ export class McpContext implements Context {
|
||||
await page.pptrPage.close({runBeforeUnload: false});
|
||||
}
|
||||
|
||||
async restoreEmulation(page: McpPage) {
|
||||
const currentSetting = page.emulationSettings;
|
||||
await this.emulate(currentSetting, page);
|
||||
}
|
||||
|
||||
get #hasNetworkBlockOrAllowlist(): boolean {
|
||||
return !!(this.#options.allowList || this.#options.blocklist);
|
||||
}
|
||||
|
||||
async emulate(
|
||||
options: {
|
||||
networkConditions?: string;
|
||||
cpuThrottlingRate?: number;
|
||||
geolocation?: GeolocationOptions;
|
||||
userAgent?: string;
|
||||
colorScheme?: 'dark' | 'light' | 'auto';
|
||||
viewport?: Viewport;
|
||||
extraHttpHeaders?: Record<string, string> | undefined;
|
||||
},
|
||||
targetMcpPage?: McpPage,
|
||||
): Promise<void> {
|
||||
const mcpPage = targetMcpPage ?? this.getSelectedMcpPage();
|
||||
const page = mcpPage.pptrPage;
|
||||
const newSettings: EmulationSettings = {...mcpPage.emulationSettings};
|
||||
|
||||
// Skip network emulation if blocklist/allowlist is configured, as it conflicts with blocking rules in Puppeteer.
|
||||
if (this.#hasNetworkBlockOrAllowlist) {
|
||||
if (options.networkConditions !== undefined) {
|
||||
throw new Error(
|
||||
'Network throttling is not supported when network blocking (allowlist/blocklist) is configured.',
|
||||
);
|
||||
}
|
||||
} else if (!options.networkConditions) {
|
||||
await page.emulateNetworkConditions(null);
|
||||
delete newSettings.networkConditions;
|
||||
} else if (options.networkConditions === 'Offline') {
|
||||
await page.emulateNetworkConditions({
|
||||
offline: true,
|
||||
download: 0,
|
||||
upload: 0,
|
||||
latency: 0,
|
||||
});
|
||||
newSettings.networkConditions = 'Offline';
|
||||
} else if (options.networkConditions in PredefinedNetworkConditions) {
|
||||
const networkCondition =
|
||||
PredefinedNetworkConditions[
|
||||
options.networkConditions as keyof typeof PredefinedNetworkConditions
|
||||
];
|
||||
await page.emulateNetworkConditions(networkCondition);
|
||||
newSettings.networkConditions = options.networkConditions;
|
||||
}
|
||||
|
||||
const secondarySession = this.getDevToolsUniverse(mcpPage)?.session;
|
||||
if (!options.cpuThrottlingRate) {
|
||||
await page.emulateCPUThrottling(1);
|
||||
if (secondarySession) {
|
||||
await secondarySession.send('Emulation.setCPUThrottlingRate', {
|
||||
rate: 1,
|
||||
});
|
||||
}
|
||||
delete newSettings.cpuThrottlingRate;
|
||||
} else {
|
||||
await page.emulateCPUThrottling(options.cpuThrottlingRate);
|
||||
if (secondarySession) {
|
||||
await secondarySession.send('Emulation.setCPUThrottlingRate', {
|
||||
rate: options.cpuThrottlingRate,
|
||||
});
|
||||
}
|
||||
newSettings.cpuThrottlingRate = options.cpuThrottlingRate;
|
||||
}
|
||||
|
||||
if (!options.geolocation) {
|
||||
await page.setGeolocation({latitude: 0, longitude: 0});
|
||||
delete newSettings.geolocation;
|
||||
} else {
|
||||
await page.setGeolocation(options.geolocation);
|
||||
newSettings.geolocation = options.geolocation;
|
||||
}
|
||||
|
||||
if (!options.userAgent) {
|
||||
await page.setUserAgent({userAgent: undefined});
|
||||
delete newSettings.userAgent;
|
||||
} else {
|
||||
await page.setUserAgent({userAgent: options.userAgent});
|
||||
newSettings.userAgent = options.userAgent;
|
||||
}
|
||||
|
||||
if (!options.colorScheme || options.colorScheme === 'auto') {
|
||||
await page.emulateMediaFeatures([
|
||||
{name: 'prefers-color-scheme', value: ''},
|
||||
]);
|
||||
delete newSettings.colorScheme;
|
||||
} else {
|
||||
await page.emulateMediaFeatures([
|
||||
{name: 'prefers-color-scheme', value: options.colorScheme},
|
||||
]);
|
||||
newSettings.colorScheme = options.colorScheme;
|
||||
}
|
||||
|
||||
if (!options.viewport) {
|
||||
delete newSettings.viewport;
|
||||
} else {
|
||||
const defaults = {
|
||||
deviceScaleFactor: 1,
|
||||
isMobile: false,
|
||||
hasTouch: false,
|
||||
isLandscape: false,
|
||||
};
|
||||
newSettings.viewport = {...defaults, ...options.viewport};
|
||||
}
|
||||
|
||||
if (options.extraHttpHeaders !== undefined) {
|
||||
await page.setExtraHTTPHeaders(options.extraHttpHeaders);
|
||||
newSettings.extraHttpHeaders = options.extraHttpHeaders;
|
||||
if (Object.keys(options.extraHttpHeaders).length === 0) {
|
||||
delete newSettings.extraHttpHeaders;
|
||||
}
|
||||
}
|
||||
|
||||
mcpPage.emulationSettings = Object.keys(newSettings).length
|
||||
? newSettings
|
||||
: {};
|
||||
|
||||
this.#updateSelectedPageTimeouts();
|
||||
|
||||
// This should happen after updating the page timeouts.
|
||||
// Setting the viewport can trigger a reload which we don't want to timeout.
|
||||
await page.setViewport(newSettings.viewport ?? null);
|
||||
}
|
||||
|
||||
setIsRunningPerformanceTrace(x: boolean): void {
|
||||
this.#isRunningTrace = x;
|
||||
}
|
||||
@@ -526,7 +377,7 @@ export class McpContext implements Context {
|
||||
|
||||
selectPage(newPage: McpPage): void {
|
||||
this.#selectedPage = newPage;
|
||||
this.#updateSelectedPageTimeouts();
|
||||
newPage.updateTimeouts();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -539,36 +390,6 @@ export class McpContext implements Context {
|
||||
return this.#selectedPageFallback;
|
||||
}
|
||||
|
||||
#updateSelectedPageTimeouts() {
|
||||
const page = this.getSelectedMcpPage();
|
||||
// For waiters 5sec timeout should be sufficient.
|
||||
// Increased in case we throttle the CPU
|
||||
const cpuMultiplier = page.cpuThrottlingRate;
|
||||
page.pptrPage.setDefaultTimeout(DEFAULT_TIMEOUT * cpuMultiplier);
|
||||
// 10sec should be enough for the load event to be emitted during
|
||||
// navigations.
|
||||
// Increased in case we throttle the network requests or the CPU
|
||||
const networkMultiplier = getNetworkMultiplierFromString(
|
||||
page.networkConditions,
|
||||
);
|
||||
page.pptrPage.setDefaultNavigationTimeout(
|
||||
NAVIGATION_TIMEOUT * networkMultiplier * cpuMultiplier,
|
||||
);
|
||||
}
|
||||
|
||||
// Linear scan over per-page snapshots. The page count is small (typically
|
||||
// 2-10) so a reverse index isn't worthwhile given the uid-reuse lifecycle
|
||||
// complexity it would introduce.
|
||||
getAXNodeByUid(uid: string) {
|
||||
for (const mcpPage of this.#mcpPages.values()) {
|
||||
const node = mcpPage.textSnapshot?.idToNode.get(uid);
|
||||
if (node) {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a snapshot of the extension service workers.
|
||||
*/
|
||||
@@ -613,7 +434,10 @@ export class McpContext implements Context {
|
||||
#createMcpPage(page: Page): McpPage {
|
||||
let mcpPage = this.#mcpPages.get(page);
|
||||
if (!mcpPage) {
|
||||
mcpPage = new McpPage(page, this.#nextPageId++);
|
||||
mcpPage = new McpPage(page, this.#nextPageId++, {
|
||||
locatorClass: this.#locatorClass,
|
||||
hasNetworkBlockOrAllowlist: this.#hasNetworkBlockOrAllowlist,
|
||||
});
|
||||
this.#mcpPages.set(page, mcpPage);
|
||||
void mcpPage.init();
|
||||
}
|
||||
@@ -816,58 +640,6 @@ export class McpContext implements Context {
|
||||
return this.#traceResults;
|
||||
}
|
||||
|
||||
getNetworkRequestStableId(request: HTTPRequest): number {
|
||||
return (request as WithSymbolId<typeof request>)[stableIdSymbol] ?? -1;
|
||||
}
|
||||
|
||||
waitForTextOnPage(
|
||||
text: string[],
|
||||
timeout?: number,
|
||||
mcpPage?: McpPage,
|
||||
): Promise<Element> {
|
||||
const page = mcpPage
|
||||
? mcpPage.pptrPage
|
||||
: this.getSelectedMcpPage().pptrPage;
|
||||
const frames = page.frames();
|
||||
|
||||
let locator = this.#locatorClass.race(
|
||||
frames.flatMap(frame =>
|
||||
text.flatMap(value => [
|
||||
frame.locator(`aria/${value}`),
|
||||
frame.locator(`text/${value}`),
|
||||
]),
|
||||
),
|
||||
);
|
||||
|
||||
if (timeout) {
|
||||
locator = locator.setTimeout(timeout);
|
||||
}
|
||||
|
||||
return locator.wait();
|
||||
}
|
||||
|
||||
/**
|
||||
* We need to ignore favicon request as they make our test flaky
|
||||
*/
|
||||
async setUpNetworkCollectorForTesting() {
|
||||
for (const mcpPage of this.getPages()) {
|
||||
mcpPage.networkCollector.dispose();
|
||||
mcpPage.networkCollector = new NetworkCollector(
|
||||
mcpPage.pptrPage,
|
||||
collect => {
|
||||
return {
|
||||
request: req => {
|
||||
if (req.url().includes('favicon.ico')) {
|
||||
return;
|
||||
}
|
||||
collect(req);
|
||||
},
|
||||
} as ListenerMap;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async installExtension(extensionPath: string): Promise<string> {
|
||||
const id = await this.browser.installExtension(extensionPath);
|
||||
return id;
|
||||
|
||||
+210
-11
@@ -16,19 +16,23 @@ import {
|
||||
type UncaughtError,
|
||||
} from './PageCollector.js';
|
||||
import {TextSnapshot} from './TextSnapshot.js';
|
||||
import type {
|
||||
Dialog,
|
||||
ElementHandle,
|
||||
Viewport,
|
||||
WebMCPTool,
|
||||
Protocol,
|
||||
Page,
|
||||
ConsoleMessage,
|
||||
HTTPRequest,
|
||||
DevTools,
|
||||
import type {Locator} from './third_party/index.js';
|
||||
import {
|
||||
PredefinedNetworkConditions,
|
||||
type Dialog,
|
||||
type ElementHandle,
|
||||
type Viewport,
|
||||
type WebMCPTool,
|
||||
type Protocol,
|
||||
type Page,
|
||||
type ConsoleMessage,
|
||||
type HTTPRequest,
|
||||
type DevTools,
|
||||
} from './third_party/index.js';
|
||||
import {takeSnapshot} from './tools/snapshot.js';
|
||||
import type {ToolGroups} from './tools/thirdPartyDeveloper.js';
|
||||
const DEFAULT_TIMEOUT = 5_000;
|
||||
const NAVIGATION_TIMEOUT = 10_000;
|
||||
import type {
|
||||
ContextPage,
|
||||
DevToolsData,
|
||||
@@ -39,6 +43,7 @@ import type {
|
||||
GeolocationOptions,
|
||||
TextSnapshotNode,
|
||||
} from './types.js';
|
||||
import {type WithSymbolId, stableIdSymbol} from './utils/id.js';
|
||||
import {
|
||||
getNetworkMultiplierFromString,
|
||||
WaitForHelper,
|
||||
@@ -80,7 +85,19 @@ export class McpPage implements ContextPage {
|
||||
networkCollector: NetworkCollector;
|
||||
consoleCollector: ConsoleCollector;
|
||||
|
||||
constructor(page: Page, id: number) {
|
||||
#hasNetworkBlockOrAllowlist: boolean;
|
||||
#locatorClass: typeof Locator;
|
||||
|
||||
constructor(
|
||||
page: Page,
|
||||
id: number,
|
||||
options: {
|
||||
hasNetworkBlockOrAllowlist: boolean;
|
||||
locatorClass: typeof Locator;
|
||||
},
|
||||
) {
|
||||
this.#hasNetworkBlockOrAllowlist = options.hasNetworkBlockOrAllowlist;
|
||||
this.#locatorClass = options.locatorClass;
|
||||
this.pptrPage = page;
|
||||
this.id = id;
|
||||
this.#dialogHandler = (dialog: Dialog): void => {
|
||||
@@ -494,4 +511,186 @@ export class McpPage implements ContextPage {
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
getConsoleMessageStableId(
|
||||
message: ConsoleMessage | Error | DevTools.AggregatedIssue | UncaughtError,
|
||||
): number {
|
||||
return (message as WithSymbolId<typeof message>)[stableIdSymbol] ?? -1;
|
||||
}
|
||||
|
||||
getNetworkRequestStableId(request: HTTPRequest): number {
|
||||
return (request as WithSymbolId<typeof request>)[stableIdSymbol] ?? -1;
|
||||
}
|
||||
|
||||
async restoreEmulation() {
|
||||
const currentSetting = this.emulationSettings;
|
||||
await this.emulate(currentSetting);
|
||||
}
|
||||
|
||||
async emulate(options: {
|
||||
networkConditions?: string;
|
||||
cpuThrottlingRate?: number;
|
||||
geolocation?: GeolocationOptions;
|
||||
userAgent?: string;
|
||||
colorScheme?: 'dark' | 'light' | 'auto';
|
||||
viewport?: Viewport;
|
||||
extraHttpHeaders?: Record<string, string> | undefined;
|
||||
}): Promise<void> {
|
||||
const page = this.pptrPage;
|
||||
const newSettings: EmulationSettings = {...this.emulationSettings};
|
||||
|
||||
// Skip network emulation if blocklist/allowlist is configured, as it conflicts with blocking rules in Puppeteer.
|
||||
if (this.#hasNetworkBlockOrAllowlist) {
|
||||
if (options.networkConditions !== undefined) {
|
||||
throw new Error(
|
||||
'Network throttling is not supported when network blocking (allowlist/blocklist) is configured.',
|
||||
);
|
||||
}
|
||||
} else if (!options.networkConditions) {
|
||||
await page.emulateNetworkConditions(null);
|
||||
delete newSettings.networkConditions;
|
||||
} else if (options.networkConditions === 'Offline') {
|
||||
await page.emulateNetworkConditions({
|
||||
offline: true,
|
||||
download: 0,
|
||||
upload: 0,
|
||||
latency: 0,
|
||||
});
|
||||
newSettings.networkConditions = 'Offline';
|
||||
} else if (options.networkConditions in PredefinedNetworkConditions) {
|
||||
const networkCondition =
|
||||
PredefinedNetworkConditions[
|
||||
options.networkConditions as keyof typeof PredefinedNetworkConditions
|
||||
];
|
||||
await page.emulateNetworkConditions(networkCondition);
|
||||
newSettings.networkConditions = options.networkConditions;
|
||||
}
|
||||
|
||||
const secondarySession = this.devtoolsUniverse?.session;
|
||||
if (!options.cpuThrottlingRate) {
|
||||
await page.emulateCPUThrottling(1);
|
||||
if (secondarySession) {
|
||||
await secondarySession.send('Emulation.setCPUThrottlingRate', {
|
||||
rate: 1,
|
||||
});
|
||||
}
|
||||
delete newSettings.cpuThrottlingRate;
|
||||
} else {
|
||||
await page.emulateCPUThrottling(options.cpuThrottlingRate);
|
||||
if (secondarySession) {
|
||||
await secondarySession.send('Emulation.setCPUThrottlingRate', {
|
||||
rate: options.cpuThrottlingRate,
|
||||
});
|
||||
}
|
||||
newSettings.cpuThrottlingRate = options.cpuThrottlingRate;
|
||||
}
|
||||
|
||||
if (!options.geolocation) {
|
||||
await page.setGeolocation({latitude: 0, longitude: 0});
|
||||
delete newSettings.geolocation;
|
||||
} else {
|
||||
await page.setGeolocation(options.geolocation);
|
||||
newSettings.geolocation = options.geolocation;
|
||||
}
|
||||
|
||||
if (!options.userAgent) {
|
||||
await page.setUserAgent({userAgent: undefined});
|
||||
delete newSettings.userAgent;
|
||||
} else {
|
||||
await page.setUserAgent({userAgent: options.userAgent});
|
||||
newSettings.userAgent = options.userAgent;
|
||||
}
|
||||
|
||||
if (!options.colorScheme || options.colorScheme === 'auto') {
|
||||
await page.emulateMediaFeatures([
|
||||
{name: 'prefers-color-scheme', value: ''},
|
||||
]);
|
||||
delete newSettings.colorScheme;
|
||||
} else {
|
||||
await page.emulateMediaFeatures([
|
||||
{name: 'prefers-color-scheme', value: options.colorScheme},
|
||||
]);
|
||||
newSettings.colorScheme = options.colorScheme;
|
||||
}
|
||||
|
||||
if (!options.viewport) {
|
||||
delete newSettings.viewport;
|
||||
} else {
|
||||
const defaults = {
|
||||
deviceScaleFactor: 1,
|
||||
isMobile: false,
|
||||
hasTouch: false,
|
||||
isLandscape: false,
|
||||
};
|
||||
newSettings.viewport = {...defaults, ...options.viewport};
|
||||
}
|
||||
|
||||
if (options.extraHttpHeaders !== undefined) {
|
||||
await page.setExtraHTTPHeaders(options.extraHttpHeaders);
|
||||
newSettings.extraHttpHeaders = options.extraHttpHeaders;
|
||||
if (Object.keys(options.extraHttpHeaders).length === 0) {
|
||||
delete newSettings.extraHttpHeaders;
|
||||
}
|
||||
}
|
||||
|
||||
this.emulationSettings = Object.keys(newSettings).length ? newSettings : {};
|
||||
|
||||
this.updateTimeouts();
|
||||
|
||||
// This should happen after updating the page timeouts.
|
||||
// Setting the viewport can trigger a reload which we don't want to timeout.
|
||||
await page.setViewport(newSettings.viewport ?? null);
|
||||
}
|
||||
|
||||
updateTimeouts() {
|
||||
// For waiters 5sec timeout should be sufficient.
|
||||
// Increased in case we throttle the CPU
|
||||
const cpuMultiplier = this.cpuThrottlingRate;
|
||||
this.pptrPage.setDefaultTimeout(DEFAULT_TIMEOUT * cpuMultiplier);
|
||||
// 10sec should be enough for the load event to be emitted during
|
||||
// navigations.
|
||||
// Increased in case we throttle the network requests or the CPU
|
||||
const networkMultiplier = getNetworkMultiplierFromString(
|
||||
this.networkConditions,
|
||||
);
|
||||
this.pptrPage.setDefaultNavigationTimeout(
|
||||
NAVIGATION_TIMEOUT * networkMultiplier * cpuMultiplier,
|
||||
);
|
||||
}
|
||||
|
||||
waitForTextOnPage(text: string[], timeout?: number): Promise<Element> {
|
||||
const frames = this.pptrPage.frames();
|
||||
|
||||
let locator = this.#locatorClass.race(
|
||||
frames.flatMap(frame =>
|
||||
text.flatMap(value => [
|
||||
frame.locator(`aria/${value}`),
|
||||
frame.locator(`text/${value}`),
|
||||
]),
|
||||
),
|
||||
);
|
||||
|
||||
if (timeout) {
|
||||
locator = locator.setTimeout(timeout);
|
||||
}
|
||||
|
||||
return locator.wait();
|
||||
}
|
||||
|
||||
/**
|
||||
* We need to ignore favicon request as they make our test flaky
|
||||
*/
|
||||
async setUpNetworkCollectorForTesting() {
|
||||
this.networkCollector.dispose();
|
||||
this.networkCollector = new NetworkCollector(this.pptrPage, collect => {
|
||||
return {
|
||||
request: req => {
|
||||
if (req.url().includes('favicon.ico')) {
|
||||
return;
|
||||
}
|
||||
collect(req);
|
||||
},
|
||||
} as ListenerMap;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+11
-5
@@ -615,7 +615,8 @@ export class McpResponse implements Response {
|
||||
);
|
||||
const formatter = await NetworkFormatter.from(request, {
|
||||
requestId: this.#attachedNetworkRequestId,
|
||||
requestIdResolver: req => context.getNetworkRequestStableId(req),
|
||||
requestIdResolver: req =>
|
||||
context.getSelectedMcpPage().getNetworkRequestStableId(req),
|
||||
fetchData: true,
|
||||
requestFilePath: this.#attachedNetworkRequestOptions?.requestFilePath,
|
||||
responseFilePath: this.#attachedNetworkRequestOptions?.responseFilePath,
|
||||
@@ -724,8 +725,9 @@ export class McpResponse implements Response {
|
||||
await Promise.all(
|
||||
messages.map(
|
||||
async (item): Promise<ConsoleFormatter | IssueFormatter | null> => {
|
||||
const consoleMessageStableId =
|
||||
context.getConsoleMessageStableId(item);
|
||||
const consoleMessageStableId = context
|
||||
.getSelectedMcpPage()
|
||||
.getConsoleMessageStableId(item);
|
||||
if ('args' in item || item instanceof UncaughtError) {
|
||||
const consoleMessage = item as ConsoleMessage | UncaughtError;
|
||||
const devTools = page
|
||||
@@ -777,9 +779,13 @@ export class McpResponse implements Response {
|
||||
networkRequests = await Promise.all(
|
||||
requests.map(request =>
|
||||
NetworkFormatter.from(request, {
|
||||
requestId: context.getNetworkRequestStableId(request),
|
||||
requestId: context
|
||||
.getSelectedMcpPage()
|
||||
.getNetworkRequestStableId(request),
|
||||
selectedInDevToolsUI:
|
||||
context.getNetworkRequestStableId(request) ===
|
||||
context
|
||||
.getSelectedMcpPage()
|
||||
.getNetworkRequestStableId(request) ===
|
||||
this.#networkRequestsOptions?.networkRequestIdInDevToolsUI,
|
||||
fetchData: false,
|
||||
saveFile: (data, filename, extension) =>
|
||||
|
||||
+10
-17
@@ -206,18 +206,6 @@ export type Context = Readonly<{
|
||||
): Promise<ContextPage>;
|
||||
closePage(pageId: number): Promise<void>;
|
||||
selectPage(page: ContextPage): void;
|
||||
restoreEmulation(page: ContextPage): Promise<void>;
|
||||
emulate(
|
||||
options: {
|
||||
networkConditions?: string;
|
||||
cpuThrottlingRate?: number;
|
||||
geolocation?: GeolocationOptions;
|
||||
userAgent?: string;
|
||||
colorScheme?: 'dark' | 'light' | 'auto';
|
||||
viewport?: Viewport;
|
||||
},
|
||||
mcpPage?: ContextPage,
|
||||
): Promise<void>;
|
||||
saveTemporaryFile(
|
||||
data: Uint8Array<ArrayBufferLike>,
|
||||
filename: string,
|
||||
@@ -227,11 +215,6 @@ export type Context = Readonly<{
|
||||
clientProvidedFilePath: string,
|
||||
extension: SupportedExtensions,
|
||||
): Promise<{filename: string}>;
|
||||
waitForTextOnPage(
|
||||
text: string[],
|
||||
timeout?: number,
|
||||
mcpPage?: ContextPage,
|
||||
): Promise<Element>;
|
||||
|
||||
getScreenRecorder(): {recorder: ScreenRecorder; filePath: string} | null;
|
||||
setScreenRecorder(
|
||||
@@ -330,6 +313,16 @@ export type ContextPage = Readonly<{
|
||||
response: Response,
|
||||
): Promise<void>;
|
||||
getDevToolsData(): Promise<DevToolsData>;
|
||||
restoreEmulation(): Promise<void>;
|
||||
emulate(options: {
|
||||
networkConditions?: string;
|
||||
cpuThrottlingRate?: number;
|
||||
geolocation?: GeolocationOptions;
|
||||
userAgent?: string;
|
||||
colorScheme?: 'dark' | 'light' | 'auto';
|
||||
viewport?: Viewport;
|
||||
}): Promise<void>;
|
||||
waitForTextOnPage(text: string[], timeout?: number): Promise<Element>;
|
||||
}>;
|
||||
|
||||
export function defineTool<Schema extends zod.ZodRawShape>(
|
||||
|
||||
@@ -101,9 +101,9 @@ export const emulate = definePageTool({
|
||||
},
|
||||
blockedByDialog: true,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (request, response, context) => {
|
||||
handler: async (request, response) => {
|
||||
const page = request.page;
|
||||
await context.emulate(request.params, page);
|
||||
await page.emulate(request.params);
|
||||
response.appendResponseLine('Emulation configured successfully');
|
||||
},
|
||||
});
|
||||
|
||||
@@ -60,7 +60,7 @@ export const listExtensions = defineTool({
|
||||
schema: {},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (_request, response, _context) => {
|
||||
handler: async (_request, response) => {
|
||||
response.setListExtensions();
|
||||
},
|
||||
});
|
||||
|
||||
+2
-2
@@ -261,7 +261,7 @@ async function fillFormElement(
|
||||
) {
|
||||
const handle = await page.getElementByUid(uid);
|
||||
try {
|
||||
const aXNode = context.getAXNodeByUid(uid);
|
||||
const aXNode = page.getAXNodeByUid(uid);
|
||||
// We assume that combobox needs to be handled as select if it has
|
||||
// role='combobox' and option children.
|
||||
if (aXNode && aXNode.role === 'combobox' && hasOptionChildren(aXNode)) {
|
||||
@@ -469,7 +469,7 @@ export const uploadFile = definePageTool({
|
||||
},
|
||||
blockedByDialog: true,
|
||||
verifyFilesSchema: ['filePath'],
|
||||
handler: async (request, response, _context) => {
|
||||
handler: async (request, response) => {
|
||||
const {uid, filePath} = request.params;
|
||||
const handle = (await request.page.getElementByUid(
|
||||
uid,
|
||||
|
||||
@@ -103,7 +103,7 @@ export const lighthouseAudit = definePageTool({
|
||||
throw new Error('Lighthouse audit failed.');
|
||||
}
|
||||
} finally {
|
||||
await context.restoreEmulation(page);
|
||||
await page.restoreEmulation();
|
||||
}
|
||||
|
||||
const lhr = result.lhr;
|
||||
|
||||
+2
-2
@@ -307,7 +307,7 @@ export const resizePage = definePageTool({
|
||||
},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (request, response, _context) => {
|
||||
handler: async (request, response) => {
|
||||
const page = request.page;
|
||||
|
||||
try {
|
||||
@@ -353,7 +353,7 @@ export const handleDialog = definePageTool({
|
||||
},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (request, response, _context) => {
|
||||
handler: async (request, response) => {
|
||||
const page = request.page;
|
||||
const dialog = page.getDialog();
|
||||
if (!dialog) {
|
||||
|
||||
@@ -61,13 +61,9 @@ export const waitFor = definePageTool({
|
||||
},
|
||||
blockedByDialog: true,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (request, response, context) => {
|
||||
handler: async (request, response) => {
|
||||
const page = request.page;
|
||||
await context.waitForTextOnPage(
|
||||
request.params.text,
|
||||
request.params.timeout,
|
||||
page,
|
||||
);
|
||||
await page.waitForTextOnPage(request.params.text, request.params.timeout);
|
||||
|
||||
response.appendResponseLine(
|
||||
`Element matching one of ${JSON.stringify(request.params.text)} found.`,
|
||||
|
||||
@@ -58,7 +58,7 @@ third-party developer tools with additional functionality.`,
|
||||
schema: {},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (_request, response, _context) => {
|
||||
handler: async (_request, response) => {
|
||||
response.setListThirdPartyDeveloperTools();
|
||||
},
|
||||
});
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ export const listWebMcpTools = definePageTool({
|
||||
schema: {},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (_request, response, _context) => {
|
||||
handler: async (_request, response) => {
|
||||
response.setListWebMcpTools();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -56,7 +56,7 @@ describe('McpContext', () => {
|
||||
await withMcpContext(async (_response, context) => {
|
||||
const page = await context.newPage();
|
||||
const timeoutBefore = page.pptrPage.getDefaultTimeout();
|
||||
await context.emulate({cpuThrottlingRate: 2});
|
||||
await context.getSelectedMcpPage().emulate({cpuThrottlingRate: 2});
|
||||
const timeoutAfter = page.pptrPage.getDefaultTimeout();
|
||||
assert(timeoutBefore < timeoutAfter, 'Timeout was less then expected');
|
||||
});
|
||||
@@ -66,7 +66,9 @@ describe('McpContext', () => {
|
||||
await withMcpContext(async (_response, context) => {
|
||||
const page = await context.newPage();
|
||||
const timeoutBefore = page.pptrPage.getDefaultNavigationTimeout();
|
||||
await context.emulate({networkConditions: 'Slow 3G'});
|
||||
await context
|
||||
.getSelectedMcpPage()
|
||||
.emulate({networkConditions: 'Slow 3G'});
|
||||
const timeoutAfter = page.pptrPage.getDefaultNavigationTimeout();
|
||||
assert(timeoutBefore < timeoutAfter, 'Timeout was less then expected');
|
||||
});
|
||||
@@ -76,7 +78,7 @@ describe('McpContext', () => {
|
||||
await withMcpContext(async (_response, context) => {
|
||||
const page = await context.newPage();
|
||||
|
||||
await context.emulate({
|
||||
await context.getSelectedMcpPage().emulate({
|
||||
cpuThrottlingRate: 2,
|
||||
networkConditions: 'Slow 3G',
|
||||
});
|
||||
@@ -133,7 +135,7 @@ describe('McpContext', () => {
|
||||
|
||||
// Capture a uid from page1's snapshot (snapshotId=1, button is node 1)
|
||||
const page1Uid = '1_1';
|
||||
const page1Node = context.getAXNodeByUid(page1Uid);
|
||||
const page1Node = page1.getAXNodeByUid(page1Uid);
|
||||
assert.ok(page1Node, 'uid should resolve from page1 snapshot');
|
||||
|
||||
// Page 2: new page, set content, snapshot
|
||||
@@ -145,7 +147,7 @@ describe('McpContext', () => {
|
||||
});
|
||||
|
||||
// Page 2 is now selected. Page 1's uid should still resolve.
|
||||
const node = context.getAXNodeByUid(page1Uid);
|
||||
const node = page1.getAXNodeByUid(page1Uid);
|
||||
assert.ok(node, 'page1 uid should still resolve after page2 snapshot');
|
||||
assert.strictEqual(node?.name, 'Page1 Button');
|
||||
|
||||
@@ -230,7 +232,9 @@ describe('McpContext', () => {
|
||||
sinon
|
||||
.stub(context.getSelectedMcpPage(), 'getNetworkRequests')
|
||||
.returns([mockRequest]);
|
||||
sinon.stub(context, 'getNetworkRequestStableId').returns(123);
|
||||
sinon
|
||||
.stub(context.getSelectedMcpPage(), 'getNetworkRequestStableId')
|
||||
.returns(123);
|
||||
|
||||
response.setIncludeNetworkRequests(true);
|
||||
const result = await response.handle('test', context);
|
||||
@@ -248,7 +252,9 @@ describe('McpContext', () => {
|
||||
sinon
|
||||
.stub(context.getSelectedMcpPage(), 'getNetworkRequestById')
|
||||
.returns(mockRequest);
|
||||
sinon.stub(context, 'getNetworkRequestStableId').returns(456);
|
||||
sinon
|
||||
.stub(context.getSelectedMcpPage(), 'getNetworkRequestStableId')
|
||||
.returns(456);
|
||||
|
||||
response.attachNetworkRequest(456);
|
||||
const result = await response.handle('test', context);
|
||||
@@ -274,7 +280,9 @@ describe('McpContext', () => {
|
||||
sinon
|
||||
.stub(context.getSelectedMcpPage(), 'getNetworkRequestById')
|
||||
.returns(mockRequest);
|
||||
sinon.stub(context, 'getNetworkRequestStableId').returns(789);
|
||||
sinon
|
||||
.stub(context.getSelectedMcpPage(), 'getNetworkRequestStableId')
|
||||
.returns(789);
|
||||
|
||||
// Use os.tmpdir() so validatePath passes on all platforms (macOS tmpdir
|
||||
// is /var/folders/..., not /tmp, so hardcoded /tmp paths are rejected).
|
||||
|
||||
@@ -296,7 +296,9 @@ describe('McpResponse', () => {
|
||||
|
||||
it('adds throttling setting when it is not null', async t => {
|
||||
await withMcpContext(async (response, context) => {
|
||||
await context.emulate({networkConditions: 'Slow 3G'});
|
||||
await context
|
||||
.getSelectedMcpPage()
|
||||
.emulate({networkConditions: 'Slow 3G'});
|
||||
const {content, structuredContent} = await response.handle(
|
||||
'test',
|
||||
context,
|
||||
@@ -315,7 +317,7 @@ describe('McpResponse', () => {
|
||||
'test',
|
||||
context,
|
||||
);
|
||||
await context.emulate({});
|
||||
await context.getSelectedMcpPage().emulate({});
|
||||
t.assert.snapshot(getTextContent(content[0]));
|
||||
t.assert.snapshot(
|
||||
JSON.stringify(stabilizeStructuredContent(structuredContent), null, 2),
|
||||
@@ -341,7 +343,7 @@ describe('McpResponse', () => {
|
||||
|
||||
it('adds cpu throttling setting when it is over 1', async t => {
|
||||
await withMcpContext(async (response, context) => {
|
||||
await context.emulate({cpuThrottlingRate: 4});
|
||||
await context.getSelectedMcpPage().emulate({cpuThrottlingRate: 4});
|
||||
const {content, structuredContent} = await response.handle(
|
||||
'test',
|
||||
context,
|
||||
@@ -355,7 +357,7 @@ describe('McpResponse', () => {
|
||||
|
||||
it('does not include cpu throttling setting when it is 1', async t => {
|
||||
await withMcpContext(async (response, context) => {
|
||||
await context.emulate({cpuThrottlingRate: 1});
|
||||
await context.getSelectedMcpPage().emulate({cpuThrottlingRate: 1});
|
||||
const {content, structuredContent} = await response.handle(
|
||||
'test',
|
||||
context,
|
||||
@@ -369,7 +371,7 @@ describe('McpResponse', () => {
|
||||
|
||||
it('adds viewport emulation setting when it is set', async t => {
|
||||
await withMcpContext(async (response, context) => {
|
||||
await context.emulate({
|
||||
await context.getSelectedMcpPage().emulate({
|
||||
viewport: {width: 400, height: 400, deviceScaleFactor: 1},
|
||||
});
|
||||
const {content, structuredContent} = await response.handle(
|
||||
@@ -385,7 +387,7 @@ describe('McpResponse', () => {
|
||||
|
||||
it('adds userAgent emulation setting when it is set', async t => {
|
||||
await withMcpContext(async (response, context) => {
|
||||
await context.emulate({userAgent: 'MyUA'});
|
||||
await context.getSelectedMcpPage().emulate({userAgent: 'MyUA'});
|
||||
const {content, structuredContent} = await response.handle(
|
||||
'test',
|
||||
context,
|
||||
@@ -399,7 +401,7 @@ describe('McpResponse', () => {
|
||||
|
||||
it('adds color scheme emulation setting when it is set', async t => {
|
||||
await withMcpContext(async (response, context) => {
|
||||
await context.emulate({colorScheme: 'dark'});
|
||||
await context.getSelectedMcpPage().emulate({colorScheme: 'dark'});
|
||||
const {content, structuredContent} = await response.handle(
|
||||
'test',
|
||||
context,
|
||||
|
||||
@@ -412,7 +412,9 @@ describe('console', () => {
|
||||
}
|
||||
}
|
||||
assert.ok(issueMsg);
|
||||
const id = context.getConsoleMessageStableId(issueMsg);
|
||||
const id = context
|
||||
.getSelectedMcpPage()
|
||||
.getConsoleMessageStableId(issueMsg);
|
||||
assert.ok(id);
|
||||
await listConsoleMessages().handler(
|
||||
{params: {types: ['issue']}, page: context.getSelectedMcpPage()},
|
||||
|
||||
@@ -241,7 +241,7 @@ describe('emulation', () => {
|
||||
|
||||
it('disables cpu throttling', async () => {
|
||||
await withMcpContext(async (response, context) => {
|
||||
await context.emulate({
|
||||
await context.getSelectedMcpPage().emulate({
|
||||
cpuThrottlingRate: 4,
|
||||
});
|
||||
await emulate.handler(
|
||||
|
||||
@@ -58,7 +58,7 @@ describe('lighthouse', () => {
|
||||
await withMcpContext(async (response, context) => {
|
||||
const page = context.getSelectedMcpPage().pptrPage;
|
||||
await page.goto(server.getRoute('/test-mobile'));
|
||||
await context.emulate({
|
||||
await context.getSelectedMcpPage().emulate({
|
||||
viewport: {
|
||||
width: 400,
|
||||
height: 400,
|
||||
|
||||
@@ -40,7 +40,7 @@ describe('network', () => {
|
||||
server.addHtmlRoute('/three', html`<main>Third</main>`);
|
||||
|
||||
await withMcpContext(async (response, context) => {
|
||||
await context.setUpNetworkCollectorForTesting();
|
||||
await context.getSelectedMcpPage().setUpNetworkCollectorForTesting();
|
||||
const page = context.getSelectedMcpPage().pptrPage;
|
||||
await page.goto(server.getRoute('/one'));
|
||||
await page.goto(server.getRoute('/two'));
|
||||
@@ -67,7 +67,7 @@ describe('network', () => {
|
||||
server.addHtmlRoute('/three', html`<main>Third</main>`);
|
||||
|
||||
await withMcpContext(async (response, context) => {
|
||||
await context.setUpNetworkCollectorForTesting();
|
||||
await context.getSelectedMcpPage().setUpNetworkCollectorForTesting();
|
||||
const page = context.getSelectedMcpPage().pptrPage;
|
||||
await page.goto(server.getRoute('/one'));
|
||||
await page.goto(server.getRoute('/two'));
|
||||
@@ -110,7 +110,7 @@ describe('network', () => {
|
||||
);
|
||||
|
||||
await withMcpContext(async (response, context) => {
|
||||
await context.setUpNetworkCollectorForTesting();
|
||||
await context.getSelectedMcpPage().setUpNetworkCollectorForTesting();
|
||||
const page = context.getSelectedMcpPage().pptrPage;
|
||||
await page.goto(server.getRoute('/redirect'), {
|
||||
waitUntil: 'networkidle0',
|
||||
@@ -164,7 +164,7 @@ describe('network', () => {
|
||||
server.addHtmlRoute('/three', html`<main>Third</main>`);
|
||||
|
||||
await withMcpContext(async (response, context) => {
|
||||
await context.setUpNetworkCollectorForTesting();
|
||||
await context.getSelectedMcpPage().setUpNetworkCollectorForTesting();
|
||||
const page = context.getSelectedMcpPage().pptrPage;
|
||||
await page.goto(server.getRoute('/one'));
|
||||
await page.goto(server.getRoute('/two'));
|
||||
|
||||
@@ -171,7 +171,7 @@ describe('pages', () => {
|
||||
`chrome-extension://${extensionId}/sidepanel.html`,
|
||||
);
|
||||
|
||||
await context.waitForTextOnPage(['Side Panel']);
|
||||
await context.getSelectedMcpPage().waitForTextOnPage(['Side Panel']);
|
||||
|
||||
// Wait for service worker used in the snapshot.
|
||||
await context.browser.waitForTarget(
|
||||
|
||||
@@ -449,7 +449,7 @@ describe('performance', () => {
|
||||
sinon.stub(selectedPage.tracing, 'stop').resolves(modifiedData);
|
||||
|
||||
// Emulate mobile
|
||||
await context.emulate({
|
||||
await context.getSelectedMcpPage().emulate({
|
||||
viewport: {
|
||||
width: 375,
|
||||
height: 667,
|
||||
|
||||
Reference in New Issue
Block a user