From 59bb303fde69e66dcc1bf0c8ffa1846f2a3a2f31 Mon Sep 17 00:00:00 2001 From: Alex Rudenko Date: Fri, 27 Feb 2026 09:48:15 +0100 Subject: [PATCH] refactor: consistently use McpPage in tools --- src/McpContext.ts | 38 +++++++---- src/McpPage.ts | 53 +++++++++++++-- src/McpResponse.ts | 6 +- src/server.ts | 2 +- src/tools/ToolDefinition.ts | 17 +++-- src/tools/emulation.ts | 2 +- src/tools/input.ts | 28 ++++---- src/tools/lighthouse.ts | 4 +- src/tools/memory.ts | 2 +- src/tools/pages.ts | 34 +++++----- src/tools/performance.ts | 12 ++-- src/tools/screencast.ts | 2 +- src/tools/screenshot.ts | 4 +- src/tools/script.ts | 4 +- src/tools/snapshot.ts | 2 +- tests/McpContext.test.ts | 77 +++++++++++---------- tests/tools/console.test.ts | 86 ++++++++++++----------- tests/tools/emulation.test.ts | 54 +++++++-------- tests/tools/input.test.ts | 48 ++++++------- tests/tools/lighthouse.test.ts | 8 +-- tests/tools/memory.test.ts | 2 +- tests/tools/network.test.ts | 14 ++-- tests/tools/pageFocus.test.ts | 54 ++++++++------- tests/tools/pages.test.ts | 117 +++++++++++++++++++------------- tests/tools/performance.test.ts | 24 +++---- tests/tools/screencast.test.ts | 17 +++-- tests/tools/screenshot.test.ts | 22 +++--- tests/tools/script.test.ts | 18 ++--- tests/tools/snapshot.test.ts | 14 ++-- 29 files changed, 439 insertions(+), 326 deletions(-) diff --git a/src/McpContext.ts b/src/McpContext.ts index 09a339db0..65c6b0671 100644 --- a/src/McpContext.ts +++ b/src/McpContext.ts @@ -37,7 +37,11 @@ import {PredefinedNetworkConditions} from './third_party/index.js'; import {listPages} from './tools/pages.js'; import {takeSnapshot} from './tools/snapshot.js'; import {CLOSE_PAGE_ERROR} from './tools/ToolDefinition.js'; -import type {Context, DevToolsData} from './tools/ToolDefinition.js'; +import type { + Context, + DevToolsData, + ContextPage, +} from './tools/ToolDefinition.js'; import type {TraceResult} from './trace-processing/parse.js'; import type { EmulationSettings, @@ -117,7 +121,7 @@ export class McpContext implements Context { null; #focusedPagePerContext = new Map(); - #requestPage?: Page; + #requestPage?: ContextPage; #nextPageId = 1; @@ -196,12 +200,12 @@ export class McpContext implements Context { // TODO: Refactor away mutable request state (e.g. per-request facade, // per-request context object, or another approach). Once resolved, the // global toolMutex could become per-BrowserContext for parallel execution. - setRequestPage(page?: Page): void { + setRequestPage(page?: ContextPage): void { this.#requestPage = page; } #resolveTargetPage(): Page { - return this.#requestPage ?? this.getSelectedPage(); + return this.#requestPage?.pptrPage ?? this.getSelectedPage(); } resolveCdpRequestId(cdpRequestId: string): number | undefined { @@ -283,7 +287,7 @@ export class McpContext implements Context { async newPage( background?: boolean, isolatedContextName?: string, - ): Promise { + ): Promise { let page: Page; if (isolatedContextName !== undefined) { let ctx = this.#isolatedContexts.get(isolatedContextName); @@ -299,7 +303,7 @@ export class McpContext implements Context { this.selectPage(page); this.#networkCollector.addPage(page); this.#consoleCollector.addPage(page); - return page; + return this.#getMcpPage(page); } async closePage(pageId: number): Promise { if (this.#pages.length === 1) { @@ -489,7 +493,8 @@ export class McpContext implements Context { } getDialog(page?: Page): Dialog | undefined { - const targetPage = page ?? this.#requestPage ?? this.#selectedPage; + const targetPage = + page ?? this.#requestPage?.pptrPage ?? this.#selectedPage; if (!targetPage) { return undefined; } @@ -516,11 +521,17 @@ export class McpContext implements Context { return page; } - resolvePageById(pageId?: number): Page { + getSelectedMcpPage(): McpPage { + const page = this.getSelectedPage(); + return this.#getMcpPage(page); + } + + resolvePageById(pageId?: number): McpPage { if (pageId === undefined) { - return this.getSelectedPage(); + return this.getSelectedMcpPage(); } - return this.getPageById(pageId); + const page = this.getPageById(pageId); + return this.#getMcpPage(page); } getPageById(pageId: number): Page { @@ -551,7 +562,8 @@ export class McpContext implements Context { return this.#selectedPage === page; } - assertPageIsFocused(page: Page): void { + assertPageIsFocused(pageToCheck: Page | ContextPage): void { + const page = 'pptrPage' in pageToCheck ? pageToCheck.pptrPage : pageToCheck; const ctx = page.browserContext(); const focused = this.#focusedPagePerContext.get(ctx); if (focused && focused !== page) { @@ -564,7 +576,9 @@ export class McpContext implements Context { } } - selectPage(newPage: Page): void { + selectPage(pageToSelect: Page | ContextPage): void { + const newPage = + 'pptrPage' in pageToSelect ? pageToSelect.pptrPage : pageToSelect; const ctx = newPage.browserContext(); const oldFocused = this.#focusedPagePerContext.get(ctx); if (oldFocused && oldFocused !== newPage && !oldFocused.isClosed()) { diff --git a/src/McpPage.ts b/src/McpPage.ts index 51a30448f..fb23a5f48 100644 --- a/src/McpPage.ts +++ b/src/McpPage.ts @@ -4,11 +4,19 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type {Dialog, Page, Viewport} from './third_party/index.js'; +import type { + Dialog, + ElementHandle, + Page, + Viewport, +} from './third_party/index.js'; +import {takeSnapshot} from './tools/snapshot.js'; +import type {ContextPage} from './tools/ToolDefinition.js'; import type { EmulationSettings, GeolocationOptions, TextSnapshot, + TextSnapshotNode, } from './types.js'; /** @@ -19,8 +27,8 @@ import type { * read/write access. The dialog field is private because it requires an * event listener lifecycle managed by the constructor/dispose pair. */ -export class McpPage { - readonly page: Page; +export class McpPage implements ContextPage { + readonly pptrPage: Page; readonly id: number; // Snapshot @@ -39,7 +47,7 @@ export class McpPage { #dialogHandler: (dialog: Dialog) => void; constructor(page: Page, id: number) { - this.page = page; + this.pptrPage = page; this.id = id; this.#dialogHandler = (dialog: Dialog): void => { this.#dialog = dialog; @@ -80,6 +88,41 @@ export class McpPage { } dispose(): void { - this.page.off('dialog', this.#dialogHandler); + this.pptrPage.off('dialog', this.#dialogHandler); + } + + async getElementByUid(uid: string): Promise> { + if (!this.textSnapshot) { + throw new Error( + `No snapshot found for page ${this.id ?? '?'}. Use ${takeSnapshot.name} to capture one.`, + ); + } + const node = this.textSnapshot.idToNode.get(uid); + if (!node) { + throw new Error(`Element uid "${uid}" not found on page ${this.id}.`); + } + return this.#resolveElementHandle(node, uid); + } + + async #resolveElementHandle( + node: TextSnapshotNode, + uid: string, + ): Promise> { + const message = `Element with uid ${uid} no longer exists on the page.`; + try { + const handle = await node.elementHandle(); + if (!handle) { + throw new Error(message); + } + return handle; + } catch (error) { + throw new Error(message, { + cause: error, + }); + } + } + + getAXNodeByUid(uid: string) { + return this.textSnapshot?.idToNode.get(uid); } } diff --git a/src/McpResponse.ts b/src/McpResponse.ts index d53f4da25..5cbea123e 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -263,9 +263,11 @@ export class McpResponse implements Response { await context.createTextSnapshot( this.#snapshotParams.verbose, this.#devToolsData, - this.#snapshotParams.page, + this.#snapshotParams.page?.pptrPage, + ); + const textSnapshot = context.getTextSnapshot( + this.#snapshotParams.page?.pptrPage, ); - const textSnapshot = context.getTextSnapshot(this.#snapshotParams.page); if (textSnapshot) { const formatter = new SnapshotFormatter(textSnapshot); if (this.#snapshotParams.filePath) { diff --git a/src/server.ts b/src/server.ts index 83f03ad15..ec0df3706 100644 --- a/src/server.ts +++ b/src/server.ts @@ -181,7 +181,7 @@ export async function createMcpServer( const page = serverArgs.experimentalPageIdRouting && params.pageId ? context.resolvePageById(params.pageId) - : context.getSelectedPage(); + : context.getSelectedMcpPage(); context.setRequestPage(page); await tool.handler( { diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts index 3f441ee94..c6c1f2978 100644 --- a/src/tools/ToolDefinition.ts +++ b/src/tools/ToolDefinition.ts @@ -60,7 +60,7 @@ export interface ImageContentData { export interface SnapshotParams { verbose?: boolean; filePath?: string; - page?: Page; + page?: ContextPage; } export interface LighthouseData { @@ -141,7 +141,10 @@ export type Context = Readonly<{ getDialog(page?: Page): Dialog | undefined; clearDialog(page?: Page): void; getPageById(pageId: number): Page; - newPage(background?: boolean, isolatedContextName?: string): Promise; + newPage( + background?: boolean, + isolatedContextName?: string, + ): Promise; closePage(pageId: number): Promise; selectPage(page: Page): void; assertPageIsFocused(page: Page): void; @@ -191,6 +194,12 @@ export type Context = Readonly<{ getExtension(id: string): InstalledExtension | undefined; }>; +export type ContextPage = Readonly<{ + readonly pptrPage: Page; + getAXNodeByUid(uid: string): TextSnapshotNode | undefined; + getElementByUid(uid: string): Promise>; +}>; + export function defineTool( definition: ToolDefinition, ): ToolDefinition; @@ -223,7 +232,7 @@ interface PageToolDefinition< Schema extends zod.ZodRawShape = zod.ZodRawShape, > extends BaseToolDefinition { handler: ( - request: Request & {page: Page}, + request: Request & {page: ContextPage}, response: Response, context: Context, ) => Promise; @@ -233,7 +242,7 @@ export type DefinedPageTool = PageToolDefinition & { pageScoped: true; handler: ( - request: Request & {page: Page}, + request: Request & {page: ContextPage}, response: Response, context: Context, ) => Promise; diff --git a/src/tools/emulation.ts b/src/tools/emulation.ts index 0e1485c83..ed6ded49c 100644 --- a/src/tools/emulation.ts +++ b/src/tools/emulation.ts @@ -105,6 +105,6 @@ export const emulate = definePageTool({ }, handler: async (request, _response, context) => { const page = request.page; - await context.emulate(request.params, page); + await context.emulate(request.params, page.pptrPage); }, }); diff --git a/src/tools/input.ts b/src/tools/input.ts index b37839585..660ce5a8b 100644 --- a/src/tools/input.ts +++ b/src/tools/input.ts @@ -97,9 +97,9 @@ export const clickAt = definePageTool({ }, handler: async (request, response, context) => { const page = request.page; - context.assertPageIsFocused(page); + context.assertPageIsFocused(page.pptrPage); await context.waitForEventsAfterAction(async () => { - await page.mouse.click(request.params.x, request.params.y, { + await page.pptrPage.mouse.click(request.params.x, request.params.y, { clickCount: request.params.dblClick ? 2 : 1, }); }); @@ -239,7 +239,7 @@ export const fill = definePageTool({ request.params.uid, request.params.value, context as McpContext, - page, + page.pptrPage, ); }); response.appendResponseLine(`Successfully filled out the element`); @@ -262,11 +262,13 @@ export const typeText = definePageTool({ }, handler: async (request, response, context) => { const page = request.page; - context.assertPageIsFocused(page); + context.assertPageIsFocused(page.pptrPage); await context.waitForEventsAfterAction(async () => { - await page.keyboard.type(request.params.text); + await page.pptrPage.keyboard.type(request.params.text); if (request.params.submitKey) { - await page.keyboard.press(request.params.submitKey as KeyInput); + await page.pptrPage.keyboard.press( + request.params.submitKey as KeyInput, + ); } }); response.appendResponseLine( @@ -333,7 +335,7 @@ export const fillForm = definePageTool({ element.uid, element.value, context as McpContext, - page, + page.pptrPage, ); }); } @@ -364,7 +366,7 @@ export const uploadFile = definePageTool({ const {uid, filePath} = request.params; const handle = (await context.getElementByUid( uid, - request.page, + request.page.pptrPage, )) as ElementHandle; try { try { @@ -375,7 +377,7 @@ export const uploadFile = definePageTool({ // Page.waitForFileChooser() and upload the file this way. try { const [fileChooser] = await Promise.all([ - request.page.waitForFileChooser({timeout: 3000}), + request.page.pptrPage.waitForFileChooser({timeout: 3000}), handle.asLocator().click(), ]); await fileChooser.accept([filePath]); @@ -412,17 +414,17 @@ export const pressKey = definePageTool({ }, handler: async (request, response, context) => { const page = request.page; - context.assertPageIsFocused(page); + context.assertPageIsFocused(page.pptrPage); const tokens = parseKey(request.params.key); const [key, ...modifiers] = tokens; await context.waitForEventsAfterAction(async () => { for (const modifier of modifiers) { - await page.keyboard.down(modifier); + await page.pptrPage.keyboard.down(modifier); } - await page.keyboard.press(key); + await page.pptrPage.keyboard.press(key); for (const modifier of modifiers.toReversed()) { - await page.keyboard.up(modifier); + await page.pptrPage.keyboard.up(modifier); } }); diff --git a/src/tools/lighthouse.ts b/src/tools/lighthouse.ts index 580b2f5d8..5ed15fa96 100644 --- a/src/tools/lighthouse.ts +++ b/src/tools/lighthouse.ts @@ -82,11 +82,11 @@ export const lighthouseAudit = definePageTool({ let result: RunnerResult | undefined; try { if (mode === 'navigation') { - result = await navigation(page, page.url(), { + result = await navigation(page.pptrPage, page.pptrPage.url(), { flags, }); } else { - result = await snapshot(page, { + result = await snapshot(page.pptrPage, { flags, }); } diff --git a/src/tools/memory.ts b/src/tools/memory.ts index 0e00d9f8b..406354d28 100644 --- a/src/tools/memory.ts +++ b/src/tools/memory.ts @@ -24,7 +24,7 @@ export const takeMemorySnapshot = definePageTool({ handler: async (request, response, _context) => { const page = request.page; - await page.captureHeapSnapshot({ + await page.pptrPage.captureHeapSnapshot({ path: request.params.filePath, }); diff --git a/src/tools/pages.ts b/src/tools/pages.ts index e99124ee7..fa724ee4b 100644 --- a/src/tools/pages.ts +++ b/src/tools/pages.ts @@ -118,7 +118,7 @@ export const newPage = defineTool({ await context.waitForEventsAfterAction( async () => { - await page.goto(request.params.url, { + await page.pptrPage.goto(request.params.url, { timeout: request.params.timeout, }); }, @@ -187,19 +187,19 @@ export const navigatePage = definePageTool({ void dialog.dismiss(); } // We are not going to report the dialog like regular dialogs. - context.clearDialog(page); + context.clearDialog(page.pptrPage); } }; let initScriptId: string | undefined; if (request.params.initScript) { - const {identifier} = await page.evaluateOnNewDocument( + const {identifier} = await page.pptrPage.evaluateOnNewDocument( request.params.initScript, ); initScriptId = identifier; } - page.on('dialog', dialogHandler); + page.pptrPage.on('dialog', dialogHandler); try { await context.waitForEventsAfterAction( @@ -212,7 +212,7 @@ export const navigatePage = definePageTool({ ); } try { - await page.goto(request.params.url, options); + await page.pptrPage.goto(request.params.url, options); response.appendResponseLine( `Successfully navigated to ${request.params.url}.`, ); @@ -224,9 +224,9 @@ export const navigatePage = definePageTool({ break; case 'back': try { - await page.goBack(options); + await page.pptrPage.goBack(options); response.appendResponseLine( - `Successfully navigated back to ${page.url()}.`, + `Successfully navigated back to ${page.pptrPage.url()}.`, ); } catch (error) { response.appendResponseLine( @@ -236,9 +236,9 @@ export const navigatePage = definePageTool({ break; case 'forward': try { - await page.goForward(options); + await page.pptrPage.goForward(options); response.appendResponseLine( - `Successfully navigated forward to ${page.url()}.`, + `Successfully navigated forward to ${page.pptrPage.url()}.`, ); } catch (error) { response.appendResponseLine( @@ -248,7 +248,7 @@ export const navigatePage = definePageTool({ break; case 'reload': try { - await page.reload({ + await page.pptrPage.reload({ ...options, ignoreCache: request.params.ignoreCache, }); @@ -264,9 +264,9 @@ export const navigatePage = definePageTool({ {timeout: request.params.timeout}, ); } finally { - page.off('dialog', dialogHandler); + page.pptrPage.off('dialog', dialogHandler); if (initScriptId) { - await page + await page.pptrPage .removeScriptToEvaluateOnNewDocument(initScriptId) .catch(error => { logger(`Failed to remove init script`, error); @@ -293,8 +293,8 @@ export const resizePage = definePageTool({ const page = request.page; try { - const browser = page.browser(); - const windowId = await page.windowId(); + const browser = page.pptrPage.browser(); + const windowId = await page.pptrPage.windowId(); const bounds = await browser.getWindowBounds(windowId); @@ -308,7 +308,7 @@ export const resizePage = definePageTool({ } catch { // Window APIs are not supported on all platforms } - await page.resize({ + await page.pptrPage.resize({ contentWidth: request.params.width, contentHeight: request.params.height, }); @@ -335,7 +335,7 @@ export const handleDialog = definePageTool({ }, handler: async (request, response, context) => { const page = request.page; - const dialog = context.getDialog(page); + const dialog = context.getDialog(page.pptrPage); if (!dialog) { throw new Error('No open dialog found'); } @@ -363,7 +363,7 @@ export const handleDialog = definePageTool({ } } - context.clearDialog(page); + context.clearDialog(page.pptrPage); response.setIncludePages(true); }, }); diff --git a/src/tools/performance.ts b/src/tools/performance.ts index 1921d282f..594c46e79 100644 --- a/src/tools/performance.ts +++ b/src/tools/performance.ts @@ -56,11 +56,11 @@ export const startTrace = definePageTool({ context.setIsRunningPerformanceTrace(true); const page = request.page; - const pageUrlForTracing = page.url(); + const pageUrlForTracing = page.pptrPage.url(); if (request.params.reload) { // Before starting the recording, navigate to about:blank to clear out any state. - await page.goto('about:blank', { + await page.pptrPage.goto('about:blank', { waitUntil: ['networkidle0'], }); } @@ -86,12 +86,12 @@ export const startTrace = definePageTool({ 'v8.execute', 'v8', ]; - await page.tracing.start({ + await page.pptrPage.tracing.start({ categories, }); if (request.params.reload) { - await page.goto(pageUrlForTracing, { + await page.pptrPage.goto(pageUrlForTracing, { waitUntil: ['load'], }); } @@ -99,7 +99,7 @@ export const startTrace = definePageTool({ if (request.params.autoStop) { await new Promise(resolve => setTimeout(resolve, 5_000)); await stopTracingAndAppendOutput( - page, + page.pptrPage, response, context, request.params.filePath, @@ -129,7 +129,7 @@ export const stopTrace = definePageTool({ } const page = request.page; await stopTracingAndAppendOutput( - page, + page.pptrPage, response, context, request.params.filePath, diff --git a/src/tools/screencast.ts b/src/tools/screencast.ts index fb7f19d98..a5ab1c38e 100644 --- a/src/tools/screencast.ts +++ b/src/tools/screencast.ts @@ -52,7 +52,7 @@ export const startScreencast = definePageTool({ let recorder: ScreenRecorder; try { - recorder = await page.screencast({ + recorder = await page.pptrPage.screencast({ path: resolvedPath as `${string}.mp4`, format: 'mp4' as const, }); diff --git a/src/tools/screenshot.ts b/src/tools/screenshot.ts index 1c20a2379..ebe54357c 100644 --- a/src/tools/screenshot.ts +++ b/src/tools/screenshot.ts @@ -59,10 +59,10 @@ export const screenshot = definePageTool({ if (request.params.uid) { pageOrHandle = await context.getElementByUid( request.params.uid, - request.page, + request.page.pptrPage, ); } else { - pageOrHandle = request.page; + pageOrHandle = request.page.pptrPage; } const format = request.params.format; diff --git a/src/tools/script.ts b/src/tools/script.ts index 0e1c39285..d7352b790 100644 --- a/src/tools/script.ts +++ b/src/tools/script.ts @@ -49,7 +49,7 @@ Example with arguments: \`(el) => { try { const frames = new Set(); for (const el of request.params.args ?? []) { - const handle = await context.getElementByUid(el.uid, request.page); + const handle = await request.page.getElementByUid(el.uid); frames.add(handle.frame); args.push(handle); } @@ -60,7 +60,7 @@ Example with arguments: \`(el) => { "Elements from different frames can't be evaluated together.", ); } else { - pageOrFrame = [...frames.values()][0] ?? request.page; + pageOrFrame = [...frames.values()][0] ?? request.page.pptrPage; } const fn = await pageOrFrame.evaluateHandle( `(${request.params.function})`, diff --git a/src/tools/snapshot.ts b/src/tools/snapshot.ts index a0300eded..10f882b4b 100644 --- a/src/tools/snapshot.ts +++ b/src/tools/snapshot.ts @@ -63,7 +63,7 @@ export const waitFor = definePageTool({ await context.waitForTextOnPage( request.params.text, request.params.timeout, - page, + page.pptrPage, ); response.appendResponseLine( diff --git a/tests/McpContext.test.ts b/tests/McpContext.test.ts index a0dd00917..f1499032b 100644 --- a/tests/McpContext.test.ts +++ b/tests/McpContext.test.ts @@ -50,9 +50,9 @@ describe('McpContext', () => { it('should update default timeout when cpu throttling changes', async () => { await withMcpContext(async (_response, context) => { const page = await context.newPage(); - const timeoutBefore = page.getDefaultTimeout(); + const timeoutBefore = page.pptrPage.getDefaultTimeout(); await context.emulate({cpuThrottlingRate: 2}); - const timeoutAfter = page.getDefaultTimeout(); + const timeoutAfter = page.pptrPage.getDefaultTimeout(); assert(timeoutBefore < timeoutAfter, 'Timeout was less then expected'); }); }); @@ -60,9 +60,9 @@ describe('McpContext', () => { it('should update default timeout when network conditions changes', async () => { await withMcpContext(async (_response, context) => { const page = await context.newPage(); - const timeoutBefore = page.getDefaultNavigationTimeout(); + const timeoutBefore = page.pptrPage.getDefaultNavigationTimeout(); await context.emulate({networkConditions: 'Slow 3G'}); - const timeoutAfter = page.getDefaultNavigationTimeout(); + const timeoutAfter = page.pptrPage.getDefaultNavigationTimeout(); assert(timeoutBefore < timeoutAfter, 'Timeout was less then expected'); }); }); @@ -81,7 +81,7 @@ describe('McpContext', () => { // trigger the waiting only }); - sinon.assert.calledWithExactly(stub, page, 2, 10); + sinon.assert.calledWithExactly(stub, page.pptrPage, 2, 10); }); }); @@ -94,7 +94,7 @@ describe('McpContext', () => { // https://github.com/puppeteer/puppeteer/issues/14368 is there. await new Promise(resolve => setTimeout(resolve, 5000)); await context.createPagesSnapshot(); - assert.ok(context.getDevToolsPage(page)); + assert.ok(context.getDevToolsPage(page.pptrPage)); }, { autoOpenDevTools: true, @@ -116,8 +116,8 @@ describe('McpContext', () => { // Page 2: new page, set content, snapshot const page2 = await context.newPage(); context.selectPage(page2); - await page2.setContent(html``); - await context.createTextSnapshot(false, undefined, page2); + await page2.pptrPage.setContent(html``); + await context.createTextSnapshot(false, undefined, page2.pptrPage); // Page 2 is now selected. Page 1's uid should still resolve. const node = context.getAXNodeByUid(page1Uid); @@ -134,8 +134,8 @@ describe('McpContext', () => { it('resolves for the focused page in an isolated context', async () => { await withMcpContext(async (_response, context) => { const page = await context.newPage(false, 'agent-a'); - await page.setContent(html``); - await context.createTextSnapshot(false, undefined, page); + await page.pptrPage.setContent(html``); + await context.createTextSnapshot(false, undefined, page.pptrPage); // page is focused for agent-a context; should resolve. const handle = await context.getElementByUid('1_1'); @@ -146,14 +146,14 @@ describe('McpContext', () => { it('throws for a non-focused page in the same context', async () => { await withMcpContext(async (_response, context) => { const pageA1 = await context.newPage(false, 'agent-a'); - await pageA1.setContent(html``); - await context.createTextSnapshot(false, undefined, pageA1); + await pageA1.pptrPage.setContent(html``); + await context.createTextSnapshot(false, undefined, pageA1.pptrPage); const a1Uid = '1_1'; // button on pageA1 // Open a second page in the same context (becomes focused). const pageA2 = await context.newPage(false, 'agent-a'); - await pageA2.setContent(html``); - await context.createTextSnapshot(false, undefined, pageA2); + await pageA2.pptrPage.setContent(html``); + await context.createTextSnapshot(false, undefined, pageA2.pptrPage); // pageA2 is now focused for agent-a; clicking pageA1's uid should throw. await assert.rejects( @@ -171,13 +171,13 @@ describe('McpContext', () => { await withMcpContext(async (_response, context) => { // Set up two pages in separate isolated contexts. const pageA = await context.newPage(false, 'agent-a'); - await pageA.setContent(html``); - await context.createTextSnapshot(false, undefined, pageA); + await pageA.pptrPage.setContent(html``); + await context.createTextSnapshot(false, undefined, pageA.pptrPage); const uidA = '1_1'; const pageB = await context.newPage(false, 'agent-b'); - await pageB.setContent(html``); - await context.createTextSnapshot(false, undefined, pageB); + await pageB.pptrPage.setContent(html``); + await context.createTextSnapshot(false, undefined, pageB.pptrPage); const uidB = '2_1'; // Simulate race: agent-a selects its page, then agent-b overwrites global. @@ -197,29 +197,29 @@ describe('McpContext', () => { it('aligns global selectedPage after resolution', async () => { await withMcpContext(async (_response, context) => { const pageA = await context.newPage(false, 'agent-a'); - await pageA.setContent(html``); - await context.createTextSnapshot(false, undefined, pageA); + await pageA.pptrPage.setContent(html``); + await context.createTextSnapshot(false, undefined, pageA.pptrPage); const uidA = '1_1'; const pageB = await context.newPage(false, 'agent-b'); - await pageB.setContent(html``); - await context.createTextSnapshot(false, undefined, pageB); + await pageB.pptrPage.setContent(html``); + await context.createTextSnapshot(false, undefined, pageB.pptrPage); // Global is on pageB after newPage. - assert.strictEqual(context.getSelectedPage(), pageB); + assert.strictEqual(context.getSelectedMcpPage(), pageB); // Resolve uid from pageA; should pass and align global. const handle = await context.getElementByUid(uidA); void handle.dispose(); - assert.strictEqual(context.getSelectedPage(), pageA); + assert.strictEqual(context.getSelectedMcpPage(), pageA); }); }); it('throws for nonexistent uid', async () => { await withMcpContext(async (_response, context) => { const page = await context.newPage(false, 'agent-a'); - await page.setContent(html``); - await context.createTextSnapshot(false, undefined, page); + await page.pptrPage.setContent(html``); + await context.createTextSnapshot(false, undefined, page.pptrPage); await assert.rejects(() => context.getElementByUid('nonexistent_99'), { message: 'No such element found in any snapshot.', @@ -237,8 +237,10 @@ describe('McpContext', () => { // Isolated context page. const isoPage = await context.newPage(false, 'agent-a'); - await isoPage.setContent(html``); - await context.createTextSnapshot(false, undefined, isoPage); + await isoPage.pptrPage.setContent( + html``, + ); + await context.createTextSnapshot(false, undefined, isoPage.pptrPage); const isoUid = '2_1'; // Global is now isoPage. Default context focus is still defaultPage. @@ -253,21 +255,24 @@ describe('McpContext', () => { it('scopes search to target page when page is provided', async () => { await withMcpContext(async (_response, context) => { const pageA = await context.newPage(false, 'agent-a'); - await pageA.setContent(html``); - await context.createTextSnapshot(false, undefined, pageA); + await pageA.pptrPage.setContent(html``); + await context.createTextSnapshot(false, undefined, pageA.pptrPage); const uidA = '1_1'; const pageB = await context.newPage(false, 'agent-b'); - await pageB.setContent(html``); - await context.createTextSnapshot(false, undefined, pageB); + await pageB.pptrPage.setContent(html``); + await context.createTextSnapshot(false, undefined, pageB.pptrPage); // uidA belongs to pageA; searching with pageB should throw. - await assert.rejects(() => context.getElementByUid(uidA, pageB), { - message: /not found on page/, - }); + await assert.rejects( + () => context.getElementByUid(uidA, pageB.pptrPage), + { + message: /not found on page/, + }, + ); // Searching with the correct page should resolve. - const handle = await context.getElementByUid(uidA, pageA); + const handle = await context.getElementByUid(uidA, pageA.pptrPage); void handle.dispose(); }); }); diff --git a/tests/tools/console.test.ts b/tests/tools/console.test.ts index ce0e2d6d2..09754cadb 100644 --- a/tests/tools/console.test.ts +++ b/tests/tools/console.test.ts @@ -26,7 +26,7 @@ describe('console', () => { it('list messages', async () => { await withMcpContext(async (response, context) => { await listConsoleMessages.handler( - {params: {}, page: context.getSelectedPage()}, + {params: {}, page: context.getSelectedMcpPage()}, response, context, ); @@ -37,11 +37,11 @@ describe('console', () => { it('lists error messages', async () => { await withMcpContext(async (response, context) => { const page = await context.newPage(); - await page.setContent( + await page.pptrPage.setContent( '', ); await listConsoleMessages.handler( - {params: {}, page: context.getSelectedPage()}, + {params: {}, page: context.getSelectedMcpPage()}, response, context, ); @@ -54,11 +54,11 @@ describe('console', () => { it('lists error objects', async t => { await withMcpContext(async (response, context) => { const page = await context.newPage(); - await page.setContent( + await page.pptrPage.setContent( '', ); await listConsoleMessages.handler( - {params: {}, page: context.getSelectedPage()}, + {params: {}, page: context.getSelectedMcpPage()}, response, context, ); @@ -71,9 +71,9 @@ describe('console', () => { it('work with primitive unhandled errors', async () => { await withMcpContext(async (response, context) => { const page = await context.newPage(); - await page.setContent(''); + await page.pptrPage.setContent(''); await listConsoleMessages.handler( - {params: {}, page: context.getSelectedPage()}, + {params: {}, page: context.getSelectedMcpPage()}, response, context, ); @@ -88,14 +88,16 @@ describe('console', () => { await withMcpContext(async (response, context) => { const page = await context.newPage(); const issuePromise = new Promise(resolve => { - page.once('issue', () => { + page.pptrPage.once('issue', () => { resolve(); }); }); - await page.setContent(''); + await page.pptrPage.setContent( + '', + ); await issuePromise; await listConsoleMessages.handler( - {params: {}, page: context.getSelectedPage()}, + {params: {}, page: context.getSelectedMcpPage()}, response, context, ); @@ -113,15 +115,17 @@ describe('console', () => { await withMcpContext(async (response, context) => { const page = await context.newPage(); const issuePromise = new Promise(resolve => { - page.once('issue', () => { + page.pptrPage.once('issue', () => { resolve(); }); }); - await page.setContent(''); + await page.pptrPage.setContent( + '', + ); await issuePromise; await listConsoleMessages.handler( - {params: {}, page: context.getSelectedPage()}, + {params: {}, page: context.getSelectedMcpPage()}, response, context, ); @@ -136,12 +140,14 @@ describe('console', () => { } const anotherIssuePromise = new Promise(resolve => { - page.once('issue', () => { + page.pptrPage.once('issue', () => { resolve(); }); }); - await page.reload(); - await page.setContent(''); + await page.pptrPage.reload(); + await page.pptrPage.setContent( + '', + ); await anotherIssuePromise; { const formattedResponse = await response.handle('test', context); @@ -163,17 +169,17 @@ describe('console', () => { it('gets a specific console message', async () => { await withMcpContext(async (response, context) => { const page = await context.newPage(); - await page.setContent( + await page.pptrPage.setContent( '', ); // The list is needed to populate the console messages in the context. await listConsoleMessages.handler( - {params: {}, page: context.getSelectedPage()}, + {params: {}, page: context.getSelectedMcpPage()}, response, context, ); await getConsoleMessage.handler( - {params: {msgid: 1}, page: context.getSelectedPage()}, + {params: {msgid: 1}, page: context.getSelectedMcpPage()}, response, context, ); @@ -191,21 +197,23 @@ describe('console', () => { await withMcpContext(async (response, context) => { const page = await context.newPage(); const issuePromise = new Promise(resolve => { - page.once('issue', () => { + page.pptrPage.once('issue', () => { resolve(); }); }); - await page.setContent(''); + await page.pptrPage.setContent( + '', + ); await context.createTextSnapshot(); await issuePromise; await listConsoleMessages.handler( - {params: {}, page: context.getSelectedPage()}, + {params: {}, page: context.getSelectedMcpPage()}, response, context, ); const response2 = new McpResponse({} as ParsedArguments); await getConsoleMessage.handler( - {params: {msgid: 1}, page: context.getSelectedPage()}, + {params: {msgid: 1}, page: context.getSelectedMcpPage()}, response2, context, ); @@ -223,13 +231,13 @@ describe('console', () => { await withMcpContext(async (response, context) => { const page = await context.newPage(); const issuePromise = new Promise(resolve => { - page.once('issue', () => { + page.pptrPage.once('issue', () => { resolve(); }); }); const url = server.getRoute('/data.json'); - await page.setContent(` + await page.pptrPage.setContent(`