diff --git a/src/McpContext.ts b/src/McpContext.ts index e5a6c6bcf..ca583d732 100644 --- a/src/McpContext.ts +++ b/src/McpContext.ts @@ -48,7 +48,7 @@ import { type InstalledExtension, } from './utils/ExtensionRegistry.js'; import {saveTemporaryFile} from './utils/files.js'; -import {WaitForHelper} from './WaitForHelper.js'; +import {getNetworkMultiplierFromString} from './WaitForHelper.js'; interface McpContextOptions { // Whether the DevTools windows are exposed as pages for debugging of DevTools. @@ -62,23 +62,6 @@ interface McpContextOptions { const DEFAULT_TIMEOUT = 5_000; const NAVIGATION_TIMEOUT = 10_000; -function getNetworkMultiplierFromString(condition: string | null): number { - const puppeteerCondition = - condition as keyof typeof PredefinedNetworkConditions; - - switch (puppeteerCondition) { - case 'Fast 4G': - return 1; - case 'Slow 4G': - return 2.5; - case 'Fast 3G': - return 5; - case 'Slow 3G': - return 10; - } - return 1; -} - export class McpContext implements Context { browser: Browser; logger: Debugger; @@ -851,31 +834,6 @@ export class McpContext implements Context { return this.#traceResults; } - getWaitForHelper( - page: Page, - cpuMultiplier: number, - networkMultiplier: number, - ) { - return new WaitForHelper(page, cpuMultiplier, networkMultiplier); - } - - waitForEventsAfterAction( - action: () => Promise, - options?: {timeout?: number}, - ): Promise { - const page = this.#getSelectedMcpPage(); - const cpuMultiplier = page.cpuThrottlingRate; - const networkMultiplier = getNetworkMultiplierFromString( - page.networkConditions, - ); - const waitForHelper = this.getWaitForHelper( - page.pptrPage, - cpuMultiplier, - networkMultiplier, - ); - return waitForHelper.waitForEventsAfterAction(action, options); - } - getNetworkRequestStableId(request: HTTPRequest): number { return this.#networkCollector.getIdForResource(request); } diff --git a/src/McpPage.ts b/src/McpPage.ts index 73717464b..e65c78dfa 100644 --- a/src/McpPage.ts +++ b/src/McpPage.ts @@ -18,6 +18,10 @@ import type { TextSnapshot, TextSnapshotNode, } from './types.js'; +import { + getNetworkMultiplierFromString, + WaitForHelper, +} from './WaitForHelper.js'; /** * Per-page state wrapper. Consolidates dialog, snapshot, emulation, @@ -91,6 +95,25 @@ export class McpPage implements ContextPage { return this.emulationSettings.colorScheme ?? null; } + // Public for testability: tests spy on this method to verify throttle multipliers. + createWaitForHelper( + cpuMultiplier: number, + networkMultiplier: number, + ): WaitForHelper { + return new WaitForHelper(this.pptrPage, cpuMultiplier, networkMultiplier); + } + + waitForEventsAfterAction( + action: () => Promise, + options?: {timeout?: number}, + ): Promise { + const helper = this.createWaitForHelper( + this.cpuThrottlingRate, + getNetworkMultiplierFromString(this.networkConditions), + ); + return helper.waitForEventsAfterAction(action, options); + } + dispose(): void { this.pptrPage.off('dialog', this.#dialogHandler); } diff --git a/src/WaitForHelper.ts b/src/WaitForHelper.ts index 6c84daf12..2dd0f48c1 100644 --- a/src/WaitForHelper.ts +++ b/src/WaitForHelper.ts @@ -6,6 +6,7 @@ import {logger} from './logger.js'; import type {Page, Protocol, CdpPage} from './third_party/index.js'; +import type {PredefinedNetworkConditions} from './third_party/index.js'; export class WaitForHelper { #abortController = new AbortController(); @@ -160,3 +161,22 @@ export class WaitForHelper { } } } + +export function getNetworkMultiplierFromString( + condition: string | null, +): number { + const puppeteerCondition = + condition as keyof typeof PredefinedNetworkConditions; + + switch (puppeteerCondition) { + case 'Fast 4G': + return 1; + case 'Slow 4G': + return 2.5; + case 'Fast 3G': + return 5; + case 'Slow 3G': + return 10; + } + return 1; +} diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts index 503516ec0..463c515fb 100644 --- a/src/tools/ToolDefinition.ts +++ b/src/tools/ToolDefinition.ts @@ -172,10 +172,6 @@ export type Context = Readonly<{ data: Uint8Array, filename: string, ): Promise<{filename: string}>; - waitForEventsAfterAction( - action: () => Promise, - options?: {timeout?: number}, - ): Promise; waitForTextOnPage( text: string[], timeout?: number, @@ -213,6 +209,10 @@ export type ContextPage = Readonly<{ getDialog(): Dialog | undefined; clearDialog(): void; + waitForEventsAfterAction( + action: () => Promise, + options?: {timeout?: number}, + ): Promise; }>; export function defineTool( diff --git a/src/tools/inPage.ts b/src/tools/inPage.ts index f4a3d034b..34ede867e 100644 --- a/src/tools/inPage.ts +++ b/src/tools/inPage.ts @@ -71,7 +71,7 @@ export const executeInPageTool = definePageTool({ .describe('The JSON-stringified parameters to pass to the tool'), }, handler: async (request, response, context) => { - const page = context.getSelectedMcpPage(); + const page = request.page; const toolName = request.params.toolName; let params: Record = {}; if (request.params.params) { diff --git a/src/tools/input.ts b/src/tools/input.ts index 1ddbcacd9..59227ca67 100644 --- a/src/tools/input.ts +++ b/src/tools/input.ts @@ -58,11 +58,11 @@ export const click = definePageTool({ dblClick: dblClickSchema, includeSnapshot: includeSnapshotSchema, }, - handler: async (request, response, context) => { + handler: async (request, response) => { const uid = request.params.uid; const handle = await request.page.getElementByUid(uid); try { - await context.waitForEventsAfterAction(async () => { + await request.page.waitForEventsAfterAction(async () => { await handle.asLocator().click({ count: request.params.dblClick ? 2 : 1, }); @@ -97,9 +97,9 @@ export const clickAt = definePageTool({ dblClick: dblClickSchema, includeSnapshot: includeSnapshotSchema, }, - handler: async (request, response, context) => { + handler: async (request, response) => { const page = request.page; - await context.waitForEventsAfterAction(async () => { + await page.waitForEventsAfterAction(async () => { await page.pptrPage.mouse.click(request.params.x, request.params.y, { clickCount: request.params.dblClick ? 2 : 1, }); @@ -130,11 +130,11 @@ export const hover = definePageTool({ ), includeSnapshot: includeSnapshotSchema, }, - handler: async (request, response, context) => { + handler: async (request, response) => { const uid = request.params.uid; const handle = await request.page.getElementByUid(uid); try { - await context.waitForEventsAfterAction(async () => { + await request.page.waitForEventsAfterAction(async () => { await handle.asLocator().hover(); }); response.appendResponseLine(`Successfully hovered over the element`); @@ -217,7 +217,6 @@ async function fillFormElement( } } -// here export const fill = definePageTool({ name: 'fill', description: `Type text into a input, text area or select an option from a