diff --git a/src/McpContext.ts b/src/McpContext.ts index 645ecf69a..5975ed660 100644 --- a/src/McpContext.ts +++ b/src/McpContext.ts @@ -23,7 +23,6 @@ import type { BrowserContext, ConsoleMessage, Debugger, - ElementHandle, HTTPRequest, Page, ScreenRecorder, @@ -34,7 +33,6 @@ import type { import {Locator} from './third_party/index.js'; 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, @@ -109,7 +107,7 @@ export class McpContext implements Context { #extensionServiceWorkers: ExtensionServiceWorker[] = []; #mcpPages = new Map(); - #selectedPage?: Page; + #selectedPage?: McpPage; #networkCollector: NetworkCollector; #consoleCollector: ConsoleCollector; #devtoolsUniverseManager: UniverseManager; @@ -290,7 +288,7 @@ export class McpContext implements Context { page = await this.browser.newPage({background}); } await this.createPagesSnapshot(); - this.selectPage(page); + this.selectPage(this.#getMcpPage(page)); this.#networkCollector.addPage(page); this.#consoleCollector.addPage(page); return this.#getMcpPage(page); @@ -300,16 +298,15 @@ export class McpContext implements Context { throw new Error(CLOSE_PAGE_ERROR); } const page = this.getPageById(pageId); - const mcpPage = this.#mcpPages.get(page); - if (mcpPage) { - mcpPage.dispose(); - this.#mcpPages.delete(page); + if (page) { + page.dispose(); + this.#mcpPages.delete(page.pptrPage); } - const ctx = page.browserContext(); - if (this.#focusedPagePerContext.get(ctx) === page) { + const ctx = page.pptrPage.browserContext(); + if (this.#focusedPagePerContext.get(ctx) === page.pptrPage) { this.#focusedPagePerContext.delete(ctx); } - await page.close({runBeforeUnload: false}); + await page.pptrPage.close({runBeforeUnload: false}); } getNetworkRequestById(page: McpPage, reqid: number): HTTPRequest { @@ -461,12 +458,12 @@ export class McpContext implements Context { if (!page) { throw new Error('No page selected'); } - if (page.isClosed()) { + if (page.pptrPage.isClosed()) { throw new Error( `The selected page has been closed. Call ${listPages().name} to see open pages.`, ); } - return page; + return page.pptrPage; } getSelectedMcpPage(): McpPage { @@ -474,16 +471,8 @@ export class McpContext implements Context { return this.#getMcpPage(page); } - resolvePageById(pageId?: number): McpPage { - if (pageId === undefined) { - return this.getSelectedMcpPage(); - } - const page = this.getPageById(pageId); - return this.#getMcpPage(page); - } - - getPageById(pageId: number): Page { - const page = this.#pages.find(p => this.#mcpPages.get(p)?.id === pageId); + getPageById(pageId: number): McpPage { + const page = this.#mcpPages.values().find(mcpPage => mcpPage.id === pageId); if (!page) { throw new Error('No page found'); } @@ -507,7 +496,7 @@ export class McpContext implements Context { } isPageSelected(page: Page): boolean { - return this.#selectedPage === page; + return this.#selectedPage?.pptrPage === page; } assertPageIsFocused(pageToCheck: Page | ContextPage): void { @@ -524,20 +513,22 @@ export class McpContext implements Context { } } - selectPage(pageToSelect: Page | ContextPage): void { - const newPage = - 'pptrPage' in pageToSelect ? pageToSelect.pptrPage : pageToSelect; - const ctx = newPage.browserContext(); + selectPage(newPage: McpPage): void { + const ctx = newPage.pptrPage.browserContext(); const oldFocused = this.#focusedPagePerContext.get(ctx); - if (oldFocused && oldFocused !== newPage && !oldFocused.isClosed()) { + if ( + oldFocused && + oldFocused !== newPage.pptrPage && + !oldFocused.isClosed() + ) { void oldFocused.emulateFocusedPage(false).catch(error => { this.logger('Error turning off focused page emulation', error); }); } - this.#focusedPagePerContext.set(ctx, newPage); + this.#focusedPagePerContext.set(ctx, newPage.pptrPage); this.#selectedPage = newPage; this.#updateSelectedPageTimeouts(); - void newPage.emulateFocusedPage(true).catch(error => { + void newPage.pptrPage.emulateFocusedPage(true).catch(error => { this.logger('Error turning on focused page emulation', error); }); } @@ -572,63 +563,6 @@ export class McpContext implements Context { return undefined; } - async getElementByUid( - uid: string, - page?: Page, - ): Promise> { - if (page) { - // Scoped search: only look in the target page's snapshot. - const mcpPage = this.#mcpPages.get(page); - if (!mcpPage?.textSnapshot) { - throw new Error( - `No snapshot found for page ${mcpPage?.id ?? '?'}. Use ${takeSnapshot.name} to capture one.`, - ); - } - const node = mcpPage.textSnapshot.idToNode.get(uid); - if (!node) { - throw new Error( - `Element uid "${uid}" not found on page ${mcpPage.id}.`, - ); - } - return this.#resolveElementHandle(node, uid); - } - - // Cross-page search with context-focus validation. - let anySnapshot = false; - for (const [searchPage, mcpPage] of this.#mcpPages.entries()) { - if (!mcpPage.textSnapshot) { - continue; - } - anySnapshot = true; - const node = mcpPage.textSnapshot.idToNode.get(uid); - if (node) { - const ctx = searchPage.browserContext(); - const contextSelectedPage = this.#focusedPagePerContext.get(ctx); - if (contextSelectedPage !== searchPage) { - const targetId = mcpPage.id; - const selectedId = contextSelectedPage - ? this.#mcpPages.get(contextSelectedPage)?.id - : this.#getSelectedMcpPage().id; - throw new Error( - `Element uid "${uid}" belongs to page ${targetId}, but page ${selectedId} is currently selected. ` + - `Call select_page with pageId ${targetId} first.`, - ); - } - // Align global #selectedPage for waitForEventsAfterAction etc. - if (this.#selectedPage !== searchPage) { - this.#selectedPage = searchPage; - } - return this.#resolveElementHandle(node, uid); - } - } - if (!anySnapshot) { - throw new Error( - `No snapshot found. Use ${takeSnapshot.name} to capture one.`, - ); - } - throw new Error('No such element found in any snapshot.'); - } - /** * Creates a snapshot of the extension service workers. */ @@ -664,24 +598,6 @@ export class McpContext implements Context { return this.#extensionServiceWorkers; } - 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, - }); - } - } - async createPagesSnapshot(): Promise { const {pages: allPages, isolatedContextNames} = await this.#getAllPages(); @@ -717,10 +633,11 @@ export class McpContext implements Context { }); if ( - (!this.#selectedPage || this.#pages.indexOf(this.#selectedPage) === -1) && + (!this.#selectedPage || + this.#pages.indexOf(this.#selectedPage.pptrPage) === -1) && this.#pages[0] ) { - this.selectPage(this.#pages[0]); + this.selectPage(this.#getMcpPage(this.#pages[0])); } await this.detectOpenDevToolsWindows(); @@ -943,14 +860,6 @@ export class McpContext implements Context { } } - getTextSnapshot(targetPage?: Page): TextSnapshot | null { - const page = targetPage ?? this.#selectedPage; - if (!page) { - return null; - } - return this.#mcpPages.get(page)?.textSnapshot ?? null; - } - async saveTemporaryFile( data: Uint8Array, filename: string, diff --git a/src/McpResponse.ts b/src/McpResponse.ts index d1ee4b8aa..a4514767f 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -274,9 +274,7 @@ export class McpResponse implements Response { this.#snapshotParams.verbose, this.#devToolsData, ); - const textSnapshot = context.getTextSnapshot( - this.#snapshotParams.page?.pptrPage, - ); + const textSnapshot = this.#page.textSnapshot; if (textSnapshot) { const formatter = new SnapshotFormatter(textSnapshot); if (this.#snapshotParams.filePath) { diff --git a/src/server.ts b/src/server.ts index b36e3826a..747f60c4f 100644 --- a/src/server.ts +++ b/src/server.ts @@ -182,7 +182,7 @@ export async function createMcpServer( serverArgs.experimentalPageIdRouting && params.pageId && !serverArgs.slim - ? context.resolvePageById(params.pageId) + ? context.getPageById(params.pageId) : context.getSelectedMcpPage(); response.setPage(page); await tool.handler( diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts index 831512958..5fe2a9f3d 100644 --- a/src/tools/ToolDefinition.ts +++ b/src/tools/ToolDefinition.ts @@ -60,7 +60,6 @@ export interface ImageContentData { export interface SnapshotParams { verbose?: boolean; filePath?: string; - page?: ContextPage; } export interface LighthouseData { @@ -136,16 +135,14 @@ export type Context = Readonly<{ isCruxEnabled(): boolean; recordedTraces(): TraceResult[]; storeTraceRecording(result: TraceResult): void; - getPageById(pageId: number): Page; + getPageById(pageId: number): ContextPage; newPage( background?: boolean, isolatedContextName?: string, ): Promise; closePage(pageId: number): Promise; - selectPage(page: Page): void; + selectPage(page: ContextPage): void; assertPageIsFocused(page: Page): void; - getElementByUid(uid: string, page?: Page): Promise>; - getAXNodeByUid(uid: string): TextSnapshotNode | undefined; restoreEmulation(page: ContextPage): Promise; emulate( options: { diff --git a/src/tools/input.ts b/src/tools/input.ts index 660ce5a8b..2c9b46dce 100644 --- a/src/tools/input.ts +++ b/src/tools/input.ts @@ -7,10 +7,11 @@ import {logger} from '../logger.js'; import type {McpContext, TextSnapshotNode} from '../McpContext.js'; import {zod} from '../third_party/index.js'; -import type {ElementHandle, KeyInput, Page} from '../third_party/index.js'; +import type {ElementHandle, KeyInput} from '../third_party/index.js'; import {parseKey} from '../utils/keyboard.js'; import {ToolCategory} from './categories.js'; +import type {ContextPage} from './ToolDefinition.js'; import {definePageTool} from './ToolDefinition.js'; const dblClickSchema = zod @@ -58,7 +59,7 @@ export const click = definePageTool({ }, handler: async (request, response, context) => { const uid = request.params.uid; - const handle = await context.getElementByUid(uid); + const handle = await request.page.getElementByUid(uid); try { await context.waitForEventsAfterAction(async () => { await handle.asLocator().click({ @@ -109,7 +110,7 @@ export const clickAt = definePageTool({ : `Successfully clicked at the coordinates`, ); if (request.params.includeSnapshot) { - response.includeSnapshot({page}); + response.includeSnapshot(); } }, }); @@ -131,7 +132,7 @@ export const hover = definePageTool({ }, handler: async (request, response, context) => { const uid = request.params.uid; - const handle = await context.getElementByUid(uid); + const handle = await request.page.getElementByUid(uid); try { await context.waitForEventsAfterAction(async () => { await handle.asLocator().hover(); @@ -193,9 +194,9 @@ async function fillFormElement( uid: string, value: string, context: McpContext, - page: Page, + page: ContextPage, ) { - const handle = await context.getElementByUid(uid, page); + const handle = await page.getElementByUid(uid); try { const aXNode = context.getAXNodeByUid(uid); // We assume that combobox needs to be handled as select if it has @@ -206,7 +207,7 @@ async function fillFormElement( // Increase timeout for longer input values. const timeoutPerChar = 10; // ms const fillTimeout = - page.getDefaultTimeout() + value.length * timeoutPerChar; + page.pptrPage.getDefaultTimeout() + value.length * timeoutPerChar; await handle.asLocator().setTimeout(fillTimeout).fill(value); } } catch (error) { @@ -239,12 +240,12 @@ export const fill = definePageTool({ request.params.uid, request.params.value, context as McpContext, - page.pptrPage, + page, ); }); response.appendResponseLine(`Successfully filled out the element`); if (request.params.includeSnapshot) { - response.includeSnapshot({page}); + response.includeSnapshot(); } }, }); @@ -290,8 +291,10 @@ export const drag = definePageTool({ includeSnapshot: includeSnapshotSchema, }, handler: async (request, response, context) => { - const fromHandle = await context.getElementByUid(request.params.from_uid); - const toHandle = await context.getElementByUid(request.params.to_uid); + const fromHandle = await request.page.getElementByUid( + request.params.from_uid, + ); + const toHandle = await request.page.getElementByUid(request.params.to_uid); try { await context.waitForEventsAfterAction(async () => { await fromHandle.drag(toHandle); @@ -335,13 +338,13 @@ export const fillForm = definePageTool({ element.uid, element.value, context as McpContext, - page.pptrPage, + page, ); }); } response.appendResponseLine(`Successfully filled out the form`); if (request.params.includeSnapshot) { - response.includeSnapshot({page}); + response.includeSnapshot(); } }, }); @@ -362,11 +365,10 @@ export const uploadFile = definePageTool({ filePath: zod.string().describe('The local path of the file to upload'), includeSnapshot: includeSnapshotSchema, }, - handler: async (request, response, context) => { + handler: async (request, response) => { const {uid, filePath} = request.params; - const handle = (await context.getElementByUid( + const handle = (await request.page.getElementByUid( uid, - request.page.pptrPage, )) as ElementHandle; try { try { @@ -388,7 +390,7 @@ export const uploadFile = definePageTool({ } } if (request.params.includeSnapshot) { - response.includeSnapshot({page: request.page}); + response.includeSnapshot(); } response.appendResponseLine(`File uploaded from ${filePath}.`); } finally { @@ -432,7 +434,7 @@ export const pressKey = definePageTool({ `Successfully pressed key: ${request.params.key}`, ); if (request.params.includeSnapshot) { - response.includeSnapshot({page}); + response.includeSnapshot(); } }, }); diff --git a/src/tools/pages.ts b/src/tools/pages.ts index 9ec277712..f664080c8 100644 --- a/src/tools/pages.ts +++ b/src/tools/pages.ts @@ -54,7 +54,7 @@ export const selectPage = defineTool({ context.selectPage(page); response.setIncludePages(true); if (request.params.bringToFront) { - await page.bringToFront(); + await page.pptrPage.bringToFront(); } }, }); @@ -386,7 +386,7 @@ export const getTabId = definePageTool({ handler: async (request, response, context) => { const page = context.getPageById(request.params.pageId); // @ts-expect-error _tabId is internal. - const tabId = page._tabId; + const tabId = page.pptrPage._tabId; response.setTabId(tabId); }, }); diff --git a/src/tools/screenshot.ts b/src/tools/screenshot.ts index ebe54357c..764abde4b 100644 --- a/src/tools/screenshot.ts +++ b/src/tools/screenshot.ts @@ -57,10 +57,7 @@ export const screenshot = definePageTool({ let pageOrHandle: Page | ElementHandle; if (request.params.uid) { - pageOrHandle = await context.getElementByUid( - request.params.uid, - request.page.pptrPage, - ); + pageOrHandle = await request.page.getElementByUid(request.params.uid); } else { pageOrHandle = request.page.pptrPage; } diff --git a/src/tools/snapshot.ts b/src/tools/snapshot.ts index 10f882b4b..338bd6794 100644 --- a/src/tools/snapshot.ts +++ b/src/tools/snapshot.ts @@ -37,7 +37,6 @@ in the DevTools Elements panel (if any).`, response.includeSnapshot({ verbose: request.params.verbose ?? false, filePath: request.params.filePath, - page: request.page, }); }, }); @@ -70,6 +69,6 @@ export const waitFor = definePageTool({ `Element matching one of ${JSON.stringify(request.params.text)} found.`, ); - response.includeSnapshot({page}); + response.includeSnapshot(); }, }); diff --git a/tests/McpContext.test.ts b/tests/McpContext.test.ts index f5bad3011..075c94742 100644 --- a/tests/McpContext.test.ts +++ b/tests/McpContext.test.ts @@ -22,8 +22,8 @@ describe('McpContext', () => { it('list pages', async () => { await withMcpContext(async (_response, context) => { - const page = context.getSelectedPptrPage(); - await page.setContent( + const page = context.getSelectedMcpPage(); + await page.pptrPage.setContent( html` { />`, ); await context.createTextSnapshot(context.getSelectedMcpPage()); - assert.ok(await context.getElementByUid('1_1')); + assert.ok(await page.getElementByUid('1_1')); await context.createTextSnapshot(context.getSelectedMcpPage()); - await context.getElementByUid('1_1'); + await page.getElementByUid('1_1'); }); }); @@ -125,161 +125,11 @@ describe('McpContext', () => { assert.strictEqual(node?.name, 'Page1 Button'); // The element should also be retrievable when the target page is provided. - const element = await context.getElementByUid(page1Uid, page1.pptrPage); + const element = await page1.getElementByUid(page1Uid); assert.ok(element, 'should get element handle from page1 snapshot uid'); }); }); - describe('getElementByUid context-focus validation', () => { - 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.pptrPage.setContent(html``); - await context.createTextSnapshot(page, false, undefined); - - // page is focused for agent-a context; should resolve. - const handle = await context.getElementByUid('1_1'); - void handle.dispose(); - }); - }); - - 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.pptrPage.setContent(html``); - await context.createTextSnapshot(pageA1, false, undefined); - 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.pptrPage.setContent(html``); - await context.createTextSnapshot(pageA2, false, undefined); - - // pageA2 is now focused for agent-a; clicking pageA1's uid should throw. - await assert.rejects( - () => context.getElementByUid(a1Uid), - (err: Error) => { - assert.ok(err.message.includes('belongs to page')); - assert.ok(err.message.includes('currently selected')); - return true; - }, - ); - }); - }); - - it('resolves after cross-context select_page race', async () => { - await withMcpContext(async (_response, context) => { - // Set up two pages in separate isolated contexts. - const pageA = await context.newPage(false, 'agent-a'); - await pageA.pptrPage.setContent(html``); - await context.createTextSnapshot(pageA, false, undefined); - const uidA = '1_1'; - - const pageB = await context.newPage(false, 'agent-b'); - await pageB.pptrPage.setContent(html``); - await context.createTextSnapshot(pageB, false, undefined); - const uidB = '2_1'; - - // Simulate race: agent-a selects its page, then agent-b overwrites global. - context.selectPage(pageA); - context.selectPage(pageB); - // Global #selectedPage is now pageB. - - // Agent A's uid should still resolve (per-context focus for agent-a is pageA). - const handleA = await context.getElementByUid(uidA); - void handleA.dispose(); - // Agent B's uid should also resolve. - const handleB = await context.getElementByUid(uidB); - void handleB.dispose(); - }); - }); - - it('aligns global selectedPage after resolution', async () => { - await withMcpContext(async (_response, context) => { - const pageA = await context.newPage(false, 'agent-a'); - await pageA.pptrPage.setContent(html``); - await context.createTextSnapshot(pageA, false, undefined); - const uidA = '1_1'; - - const pageB = await context.newPage(false, 'agent-b'); - await pageB.pptrPage.setContent(html``); - await context.createTextSnapshot(pageB, false, undefined); - - // Global is on pageB after newPage. - 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.getSelectedMcpPage(), pageA); - }); - }); - - it('throws for nonexistent uid', async () => { - await withMcpContext(async (_response, context) => { - const page = await context.newPage(false, 'agent-a'); - await page.pptrPage.setContent(html``); - await context.createTextSnapshot(page, false, undefined); - - await assert.rejects(() => context.getElementByUid('nonexistent_99'), { - message: 'No such element found in any snapshot.', - }); - }); - }); - - it('resolves for default context page alongside isolated contexts', async () => { - await withMcpContext(async (_response, context) => { - // Default context page (already exists from withMcpContext setup). - const defaultPage = context.getSelectedMcpPage(); - await defaultPage.pptrPage.setContent( - html``, - ); - await context.createTextSnapshot(defaultPage, false, undefined); - const defaultUid = '1_1'; - - // Isolated context page. - const isoPage = await context.newPage(false, 'agent-a'); - await isoPage.pptrPage.setContent( - html``, - ); - await context.createTextSnapshot(isoPage, false, undefined); - const isoUid = '2_1'; - - // Global is now isoPage. Default context focus is still defaultPage. - // Both should resolve via per-context lookup. - const handleDefault = await context.getElementByUid(defaultUid); - void handleDefault.dispose(); - const handleIso = await context.getElementByUid(isoUid); - void handleIso.dispose(); - }); - }); - - 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.pptrPage.setContent(html``); - await context.createTextSnapshot(pageA, false, undefined); - const uidA = '1_1'; - - const pageB = await context.newPage(false, 'agent-b'); - await pageB.pptrPage.setContent(html``); - await context.createTextSnapshot(pageB, false, undefined); - - // uidA belongs to pageA; searching with pageB should throw. - 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.pptrPage); - void handle.dispose(); - }); - }); - }); - it('should include network requests in structured content', async t => { await withMcpContext(async (response, context) => { const mockRequest = getMockRequest({ diff --git a/tests/tools/pages.test.ts b/tests/tools/pages.test.ts index 3a4bdcd90..d2828be9a 100644 --- a/tests/tools/pages.test.ts +++ b/tests/tools/pages.test.ts @@ -107,7 +107,7 @@ describe('pages', () => { await withMcpContext(async (response, context) => { assert.strictEqual( context.getPageById(1), - context.getSelectedPptrPage(), + context.getSelectedMcpPage(), ); await newPage.handler( {params: {url: 'about:blank'}}, @@ -116,7 +116,7 @@ describe('pages', () => { ); assert.strictEqual( context.getPageById(2), - context.getSelectedPptrPage(), + context.getSelectedMcpPage(), ); assert.ok(response.includePages); }); @@ -124,11 +124,11 @@ describe('pages', () => { it('create a page in the background', async () => { await withMcpContext(async (response, context) => { const originalPage = context.getPageById(1); - assert.strictEqual(originalPage, context.getSelectedPptrPage()); + assert.strictEqual(originalPage, context.getSelectedMcpPage()); // Ensure original page has focus - await originalPage.bringToFront(); + await originalPage.pptrPage.bringToFront(); assert.strictEqual( - await originalPage.evaluate(() => document.hasFocus()), + await originalPage.pptrPage.evaluate(() => document.hasFocus()), true, ); await newPage.handler( @@ -139,10 +139,10 @@ describe('pages', () => { // New page should be selected but original should retain focus assert.strictEqual( context.getPageById(2), - context.getSelectedPptrPage(), + context.getSelectedMcpPage(), ); assert.strictEqual( - await originalPage.evaluate(() => document.hasFocus()), + await originalPage.pptrPage.evaluate(() => document.hasFocus()), true, ); assert.ok(response.includePages); @@ -252,104 +252,47 @@ describe('pages', () => { }); }); - describe('resolvePageById', () => { - it('returns the correct page regardless of global selection', async () => { - await withMcpContext(async (response, context) => { - // Create two pages with different content. - await newPage.handler( - { - params: { - url: 'data:text/html,

Page A

', - isolatedContext: 'ctx-a', - }, + it('navigate_page targets the pageId page, not the global selection', async () => { + await withMcpContext(async (response, context) => { + await newPage.handler( + { + params: { + url: 'data:text/html,

Initial

', + isolatedContext: 'nav-ctx', }, - response, - context, - ); - const pageA = context.getSelectedMcpPage(); - const pageAId = context.getPageId(pageA.pptrPage)!; - - await newPage.handler( - { - params: { - url: 'data:text/html,

Page B

', - isolatedContext: 'ctx-b', - }, + }, + response, + context, + ); + const isolatedPage = context.getSelectedMcpPage(); + + // Switch global selection back to the default page. + await selectPage.handler({params: {pageId: 1}}, response, context); + assert.notStrictEqual(context.getSelectedMcpPage(), isolatedPage); + + // Navigate using page; should target the isolated page. + await navigatePage.handler( + { + params: { + url: 'data:text/html,

Navigated

', }, - response, - context, - ); - const pageB = context.getSelectedMcpPage(); - const pageBId = context.getPageId(pageB.pptrPage)!; - - // Global selection is now pageB (the last created page). - assert.strictEqual(context.getSelectedMcpPage(), pageB); - - // resolvePageById should return the correct page for each ID, - // regardless of which page is globally selected. - assert.strictEqual(context.resolvePageById(pageAId), pageA); - assert.strictEqual(context.resolvePageById(pageBId), pageB); - }); - }); - - it('falls back to getSelectedPage when no pageId is provided', async () => { - await withMcpContext(async (_response, context) => { - const selectedPage = context.getSelectedPptrPage(); - assert.strictEqual( - context.resolvePageById(undefined).pptrPage, - selectedPage, - ); - }); - }); - - it('throws for an unknown pageId', async () => { - await withMcpContext(async (_response, context) => { - assert.throws(() => context.resolvePageById(99999), /No page found/); - }); - }); - - it('navigate_page targets the pageId page, not the global selection', async () => { - await withMcpContext(async (response, context) => { - await newPage.handler( - { - params: { - url: 'data:text/html,

Initial

', - isolatedContext: 'nav-ctx', - }, - }, - response, - context, - ); - const isolatedPage = context.getSelectedMcpPage(); - - // Switch global selection back to the default page. - await selectPage.handler({params: {pageId: 1}}, response, context); - assert.notStrictEqual(context.getSelectedPptrPage(), isolatedPage); - - // Navigate using page; should target the isolated page. - await navigatePage.handler( - { - params: { - url: 'data:text/html,

Navigated

', - }, - page: isolatedPage, - }, - response, - context, - ); - - // Verify the isolated page was navigated. - const content = await isolatedPage.pptrPage.evaluate( - () => document.querySelector('h1')?.textContent, - ); - assert.strictEqual(content, 'Navigated'); - - // Verify the default page was NOT affected. - const defaultContent = await context - .getSelectedPptrPage() - .evaluate(() => document.querySelector('h1')?.textContent); - assert.notStrictEqual(defaultContent, 'Navigated'); - }); + page: isolatedPage, + }, + response, + context, + ); + + // Verify the isolated page was navigated. + const content = await isolatedPage.pptrPage.evaluate( + () => document.querySelector('h1')?.textContent, + ); + assert.strictEqual(content, 'Navigated'); + + // Verify the default page was NOT affected. + const defaultContent = await context + .getSelectedPptrPage() + .evaluate(() => document.querySelector('h1')?.textContent); + assert.notStrictEqual(defaultContent, 'Navigated'); }); }); @@ -359,9 +302,9 @@ describe('pages', () => { const page = await context.newPage(); assert.strictEqual( context.getPageById(2), - context.getSelectedPptrPage(), + context.getSelectedMcpPage(), ); - assert.strictEqual(context.getPageById(2), page.pptrPage); + assert.strictEqual(context.getPageById(2), page); await closePage.handler({params: {pageId: 2}}, response, context); assert.ok(page.pptrPage.isClosed()); assert.ok(response.includePages); @@ -386,12 +329,12 @@ describe('pages', () => { await context.newPage(); assert.strictEqual( context.getPageById(2), - context.getSelectedPptrPage(), + context.getSelectedMcpPage(), ); await selectPage.handler({params: {pageId: 1}}, response, context); assert.strictEqual( context.getPageById(1), - context.getSelectedPptrPage(), + context.getSelectedMcpPage(), ); assert.ok(response.includePages); }); @@ -401,19 +344,23 @@ describe('pages', () => { await context.newPage(); assert.strictEqual( context.getPageById(2), - context.getSelectedPptrPage(), + context.getSelectedMcpPage(), ); assert.strictEqual( - await context.getPageById(1).evaluate(() => document.hasFocus()), + await context + .getPageById(1) + .pptrPage.evaluate(() => document.hasFocus()), false, ); await selectPage.handler({params: {pageId: 1}}, response, context); assert.strictEqual( context.getPageById(1), - context.getSelectedPptrPage(), + context.getSelectedMcpPage(), ); assert.strictEqual( - await context.getPageById(1).evaluate(() => document.hasFocus()), + await context + .getPageById(1) + .pptrPage.evaluate(() => document.hasFocus()), true, ); assert.ok(response.includePages); @@ -604,9 +551,9 @@ describe('pages', () => { const page = await context.newPage(); assert.strictEqual( context.getPageById(2), - context.getSelectedPptrPage(), + context.getSelectedMcpPage(), ); - assert.strictEqual(context.getPageById(2), page.pptrPage); + assert.strictEqual(context.getPageById(2), page); await page.pptrPage.close();