refactor: move remaining McpContext getters (#2342)

Moves various getters to McpPage.
This commit is contained in:
Alex Rudenko
2026-07-10 14:46:01 +02:00
committed by GitHub
parent eb049513d2
commit 58ba174273
20 changed files with 286 additions and 308 deletions
+7 -235
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>(
+2 -2
View File
@@ -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');
},
});
+1 -1
View File
@@ -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
View File
@@ -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,
+1 -1
View File
@@ -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
View File
@@ -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) {
+2 -6
View File
@@ -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.`,
+1 -1
View File
@@ -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
View File
@@ -19,7 +19,7 @@ export const listWebMcpTools = definePageTool({
schema: {},
blockedByDialog: false,
verifyFilesSchema: [],
handler: async (_request, response, _context) => {
handler: async (_request, response) => {
response.setListWebMcpTools();
},
});
+16 -8
View File
@@ -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).
+9 -7
View File
@@ -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,
+3 -1
View File
@@ -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()},
+1 -1
View File
@@ -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(
+1 -1
View File
@@ -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,
+4 -4
View File
@@ -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'));
+1 -1
View File
@@ -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(
+1 -1
View File
@@ -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,