refactor: clean up more of the context getters (#1062)

- moves more code to the McpPage
- deleted Context specific tests
This commit is contained in:
Alex Rudenko authored and GitHub committed 2026-02-27 12:53:20 +00:00
1 parent 4cb5a17b57
commit 9628dabcb4
10 files changed
+115 -416

No files matched your search

+25 -116
View File
@@ -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<Page, McpPage>();
#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<ElementHandle<Element>> {
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<ElementHandle<Element>> {
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<Page[]> {
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<ArrayBufferLike>,
filename: string,
+1 -3
View File
@@ -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) {
+1 -1
View File
@@ -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(
+2 -5
View File
@@ -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<ContextPage>;
closePage(pageId: number): Promise<void>;
selectPage(page: Page): void;
selectPage(page: ContextPage): void;
assertPageIsFocused(page: Page): void;
getElementByUid(uid: string, page?: Page): Promise<ElementHandle<Element>>;
getAXNodeByUid(uid: string): TextSnapshotNode | undefined;
restoreEmulation(page: ContextPage): Promise<void>;
emulate(
options: {
+20 -18
View File
@@ -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<HTMLInputElement>;
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();
}
},
});
+2 -2
View File
@@ -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);
},
});
+1 -4
View File
@@ -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;
}
+1 -2
View File
@@ -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();
},
});
+5 -155
View File
@@ -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`<button>Click me</button>
<input
type="text"
@@ -31,9 +31,9 @@ describe('McpContext', () => {
/>`,
);
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`<button>A1 Button</button>`);
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`<button>A1 Button</button>`);
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`<button>A2 Button</button>`);
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`<button>Agent A Button</button>`);
await context.createTextSnapshot(pageA, false, undefined);
const uidA = '1_1';
const pageB = await context.newPage(false, 'agent-b');
await pageB.pptrPage.setContent(html`<button>Agent B Button</button>`);
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`<button>Agent A Button</button>`);
await context.createTextSnapshot(pageA, false, undefined);
const uidA = '1_1';
const pageB = await context.newPage(false, 'agent-b');
await pageB.pptrPage.setContent(html`<button>Agent B Button</button>`);
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`<button>A Button</button>`);
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`<button>Default Button</button>`,
);
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`<button>Isolated Button</button>`,
);
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`<button>Agent A Button</button>`);
await context.createTextSnapshot(pageA, false, undefined);
const uidA = '1_1';
const pageB = await context.newPage(false, 'agent-b');
await pageB.pptrPage.setContent(html`<button>Agent B Button</button>`);
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({
+57 -110
View File
@@ -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,<h1>Page A</h1>',
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,<h1>Initial</h1>',
isolatedContext: 'nav-ctx',
},
response,
context,
);
const pageA = context.getSelectedMcpPage();
const pageAId = context.getPageId(pageA.pptrPage)!;
},
response,
context,
);
const isolatedPage = context.getSelectedMcpPage();
await newPage.handler(
{
params: {
url: 'data:text/html,<h1>Page B</h1>',
isolatedContext: 'ctx-b',
},
// 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,<h1>Navigated</h1>',
},
response,
context,
);
const pageB = context.getSelectedMcpPage();
const pageBId = context.getPageId(pageB.pptrPage)!;
page: isolatedPage,
},
response,
context,
);
// Global selection is now pageB (the last created page).
assert.strictEqual(context.getSelectedMcpPage(), pageB);
// Verify the isolated page was navigated.
const content = await isolatedPage.pptrPage.evaluate(
() => document.querySelector('h1')?.textContent,
);
assert.strictEqual(content, 'Navigated');
// 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,<h1>Initial</h1>',
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,<h1>Navigated</h1>',
},
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');
});
// 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();