mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-18 22:16:52 +00:00
1818 lines
60 KiB
TypeScript
1818 lines
60 KiB
TypeScript
import { describe, expect, it } from 'bun:test';
|
|
import { runMcpRequests, getResponseText } from './browser-mcp-test-harness';
|
|
import type { MockPageState } from './browser-mcp-test-harness';
|
|
|
|
describe('ccs-browser MCP server - session and interception', () => {
|
|
it('lists browser tools including navigate, click, type, and screenshot', async () => {
|
|
const responses = await runMcpRequests(
|
|
[{ id: 'page-1', title: 'Example Page', currentUrl: 'https://example.com/' }],
|
|
[{ jsonrpc: '2.0', id: 2, method: 'tools/list' }]
|
|
);
|
|
|
|
const tools = (
|
|
responses.find((message) => message.id === 2)?.result as {
|
|
tools: Array<{
|
|
name: string;
|
|
description?: string;
|
|
inputSchema?: { properties?: Record<string, { type?: string; minimum?: number }> };
|
|
}>;
|
|
}
|
|
).tools;
|
|
|
|
expect(tools.map((tool) => tool.name)).toEqual([
|
|
'browser_get_session_info',
|
|
'browser_get_url_and_title',
|
|
'browser_get_visible_text',
|
|
'browser_get_dom_snapshot',
|
|
'browser_navigate',
|
|
'browser_click',
|
|
'browser_type',
|
|
'browser_press_key',
|
|
'browser_scroll',
|
|
'browser_select_page',
|
|
'browser_open_page',
|
|
'browser_close_page',
|
|
'browser_add_intercept_rule',
|
|
'browser_remove_intercept_rule',
|
|
'browser_list_intercept_rules',
|
|
'browser_list_requests',
|
|
'browser_set_download_behavior',
|
|
'browser_list_downloads',
|
|
'browser_cancel_download',
|
|
'browser_set_file_input',
|
|
'browser_drag_files',
|
|
'browser_drag_element',
|
|
'browser_pointer_action',
|
|
'browser_start_recording',
|
|
'browser_stop_recording',
|
|
'browser_get_recording',
|
|
'browser_clear_recording',
|
|
'browser_start_replay',
|
|
'browser_get_replay',
|
|
'browser_cancel_replay',
|
|
'browser_start_orchestration',
|
|
'browser_get_orchestration',
|
|
'browser_cancel_orchestration',
|
|
'browser_export_artifact',
|
|
'browser_import_artifact',
|
|
'browser_list_artifacts',
|
|
'browser_delete_artifact',
|
|
'browser_take_screenshot',
|
|
'browser_wait_for',
|
|
'browser_eval',
|
|
'browser_hover',
|
|
'browser_query',
|
|
'browser_take_element_screenshot',
|
|
'browser_wait_for_event',
|
|
]);
|
|
|
|
const clickTool = tools.find((tool) => tool.name === 'browser_click');
|
|
expect(clickTool?.description).toContain('mouse event chain');
|
|
expect(clickTool?.description).not.toContain('synthetic element.click()');
|
|
expect(clickTool?.inputSchema?.properties?.offsetX).toMatchObject({ type: 'number' });
|
|
expect(clickTool?.inputSchema?.properties?.offsetY).toMatchObject({ type: 'number' });
|
|
expect(clickTool?.inputSchema?.properties?.button).toMatchObject({ type: 'string' });
|
|
expect(clickTool?.inputSchema?.properties?.clickCount).toMatchObject({
|
|
type: 'integer',
|
|
minimum: 1,
|
|
});
|
|
|
|
const keyTool = tools.find((tool) => tool.name === 'browser_press_key');
|
|
expect(keyTool?.inputSchema?.properties?.key).toMatchObject({ type: 'string' });
|
|
expect(keyTool?.inputSchema?.properties?.modifiers).toMatchObject({ type: 'array' });
|
|
|
|
const scrollTool = tools.find((tool) => tool.name === 'browser_scroll');
|
|
expect(scrollTool?.inputSchema?.properties?.deltaX).toMatchObject({ type: 'number' });
|
|
expect(scrollTool?.inputSchema?.properties?.deltaY).toMatchObject({ type: 'number' });
|
|
|
|
const selectTool = tools.find((tool) => tool.name === 'browser_select_page');
|
|
expect(selectTool?.inputSchema?.properties?.pageIndex).toMatchObject({ type: 'integer' });
|
|
expect(selectTool?.inputSchema?.properties?.pageId).toMatchObject({ type: 'string' });
|
|
|
|
const openTool = tools.find((tool) => tool.name === 'browser_open_page');
|
|
expect(openTool?.inputSchema?.properties?.url).toMatchObject({ type: 'string' });
|
|
|
|
const closeTool = tools.find((tool) => tool.name === 'browser_close_page');
|
|
expect(closeTool?.inputSchema?.properties?.pageIndex).toMatchObject({ type: 'integer' });
|
|
expect(closeTool?.inputSchema?.properties?.pageId).toMatchObject({ type: 'string' });
|
|
|
|
const addRuleTool = tools.find((tool) => tool.name === 'browser_add_intercept_rule');
|
|
expect(addRuleTool?.inputSchema?.properties?.pageIndex).toMatchObject({ type: 'integer' });
|
|
expect(addRuleTool?.inputSchema?.properties?.pageId).toMatchObject({ type: 'string' });
|
|
expect(addRuleTool?.inputSchema?.properties?.urlIncludes).toMatchObject({ type: 'string' });
|
|
expect(addRuleTool?.inputSchema?.properties?.method).toMatchObject({ type: 'string' });
|
|
expect(addRuleTool?.inputSchema?.properties?.resourceType).toMatchObject({ type: 'string' });
|
|
expect(addRuleTool?.inputSchema?.properties?.urlPattern).toMatchObject({ type: 'string' });
|
|
expect(addRuleTool?.inputSchema?.properties?.urlRegex).toMatchObject({ type: 'string' });
|
|
expect(addRuleTool?.inputSchema?.properties?.headerMatchers).toMatchObject({ type: 'array' });
|
|
expect(addRuleTool?.inputSchema?.properties?.priority).toMatchObject({ type: 'integer' });
|
|
expect(addRuleTool?.inputSchema?.properties?.action).toMatchObject({ type: 'string' });
|
|
expect(addRuleTool?.inputSchema?.properties?.statusCode).toMatchObject({ type: 'integer' });
|
|
expect(addRuleTool?.inputSchema?.properties?.responseHeaders).toMatchObject({ type: 'array' });
|
|
expect(addRuleTool?.inputSchema?.properties?.headers).toBeUndefined();
|
|
expect(addRuleTool?.inputSchema?.properties?.body).toMatchObject({ type: 'string' });
|
|
expect(addRuleTool?.inputSchema?.properties?.contentType).toMatchObject({ type: 'string' });
|
|
|
|
const removeRuleTool = tools.find((tool) => tool.name === 'browser_remove_intercept_rule');
|
|
expect(removeRuleTool?.inputSchema?.properties?.ruleId).toMatchObject({ type: 'string' });
|
|
|
|
const listRulesTool = tools.find((tool) => tool.name === 'browser_list_intercept_rules');
|
|
expect(listRulesTool?.inputSchema).toMatchObject({ type: 'object' });
|
|
|
|
const listRequestsTool = tools.find((tool) => tool.name === 'browser_list_requests');
|
|
expect(listRequestsTool?.inputSchema?.properties?.pageIndex).toMatchObject({ type: 'integer' });
|
|
expect(listRequestsTool?.inputSchema?.properties?.pageId).toMatchObject({ type: 'string' });
|
|
expect(listRequestsTool?.inputSchema?.properties?.limit).toMatchObject({ type: 'integer' });
|
|
|
|
const setDownloadTool = tools.find((tool) => tool.name === 'browser_set_download_behavior');
|
|
expect(setDownloadTool?.inputSchema?.properties?.behavior).toMatchObject({ type: 'string' });
|
|
expect(setDownloadTool?.inputSchema?.properties?.downloadPath).toMatchObject({ type: 'string' });
|
|
expect(setDownloadTool?.inputSchema?.properties?.eventsEnabled).toMatchObject({ type: 'boolean' });
|
|
|
|
const listDownloadsTool = tools.find((tool) => tool.name === 'browser_list_downloads');
|
|
expect(listDownloadsTool?.inputSchema?.properties?.limit).toMatchObject({ type: 'integer' });
|
|
expect(listDownloadsTool?.inputSchema?.properties?.pageId).toBeUndefined();
|
|
|
|
const cancelDownloadTool = tools.find((tool) => tool.name === 'browser_cancel_download');
|
|
expect(cancelDownloadTool?.inputSchema?.properties?.downloadId).toMatchObject({ type: 'string' });
|
|
expect(cancelDownloadTool?.inputSchema?.properties?.guid).toMatchObject({ type: 'string' });
|
|
|
|
const uploadTool = tools.find((tool) => tool.name === 'browser_set_file_input');
|
|
expect(uploadTool?.inputSchema?.properties?.selector).toMatchObject({ type: 'string' });
|
|
expect(uploadTool?.inputSchema?.properties?.files).toMatchObject({ type: 'array' });
|
|
expect(uploadTool?.inputSchema?.properties?.pageIndex).toMatchObject({ type: 'integer' });
|
|
expect(uploadTool?.inputSchema?.properties?.pageId).toMatchObject({ type: 'string' });
|
|
expect(uploadTool?.inputSchema?.properties?.nth).toMatchObject({ type: 'integer' });
|
|
expect(uploadTool?.inputSchema?.properties?.frameSelector).toMatchObject({ type: 'string' });
|
|
expect(uploadTool?.inputSchema?.properties?.pierceShadow).toMatchObject({ type: 'boolean' });
|
|
|
|
const dragFilesTool = tools.find((tool) => tool.name === 'browser_drag_files');
|
|
expect(dragFilesTool?.inputSchema?.properties?.selector).toMatchObject({ type: 'string' });
|
|
expect(dragFilesTool?.inputSchema?.properties?.files).toMatchObject({ type: 'array' });
|
|
expect(dragFilesTool?.inputSchema?.properties?.pageIndex).toMatchObject({ type: 'integer' });
|
|
expect(dragFilesTool?.inputSchema?.properties?.pageId).toMatchObject({ type: 'string' });
|
|
expect(dragFilesTool?.inputSchema?.properties?.nth).toMatchObject({ type: 'integer' });
|
|
expect(dragFilesTool?.inputSchema?.properties?.frameSelector).toMatchObject({ type: 'string' });
|
|
expect(dragFilesTool?.inputSchema?.properties?.pierceShadow).toMatchObject({ type: 'boolean' });
|
|
|
|
const dragElementTool = tools.find((tool) => tool.name === 'browser_drag_element');
|
|
expect(dragElementTool?.inputSchema?.properties?.selector).toMatchObject({ type: 'string' });
|
|
expect(dragElementTool?.inputSchema?.properties?.targetSelector).toMatchObject({ type: 'string' });
|
|
expect(dragElementTool?.inputSchema?.properties?.targetX).toMatchObject({ type: 'number' });
|
|
expect(dragElementTool?.inputSchema?.properties?.targetY).toMatchObject({ type: 'number' });
|
|
expect(dragElementTool?.inputSchema?.properties?.steps).toMatchObject({ type: 'integer' });
|
|
|
|
const pointerTool = tools.find((tool) => tool.name === 'browser_pointer_action');
|
|
expect(pointerTool?.inputSchema?.properties?.actions).toMatchObject({ type: 'array' });
|
|
|
|
const startRecordingTool = tools.find((tool) => tool.name === 'browser_start_recording');
|
|
expect(startRecordingTool?.inputSchema?.properties?.pageIndex).toMatchObject({ type: 'integer' });
|
|
expect(startRecordingTool?.inputSchema?.properties?.pageId).toMatchObject({ type: 'string' });
|
|
|
|
const stopRecordingTool = tools.find((tool) => tool.name === 'browser_stop_recording');
|
|
expect(stopRecordingTool?.inputSchema).toMatchObject({ type: 'object' });
|
|
|
|
const getRecordingTool = tools.find((tool) => tool.name === 'browser_get_recording');
|
|
expect(getRecordingTool?.inputSchema).toMatchObject({ type: 'object' });
|
|
|
|
const clearRecordingTool = tools.find((tool) => tool.name === 'browser_clear_recording');
|
|
expect(clearRecordingTool?.inputSchema).toMatchObject({ type: 'object' });
|
|
|
|
const startReplayTool = tools.find((tool) => tool.name === 'browser_start_replay');
|
|
expect(startReplayTool?.inputSchema?.properties?.steps).toMatchObject({ type: 'array' });
|
|
expect(startReplayTool?.inputSchema?.properties?.pageIndex).toMatchObject({ type: 'integer' });
|
|
expect(startReplayTool?.inputSchema?.properties?.pageId).toMatchObject({ type: 'string' });
|
|
|
|
const getReplayTool = tools.find((tool) => tool.name === 'browser_get_replay');
|
|
expect(getReplayTool?.inputSchema).toMatchObject({ type: 'object' });
|
|
|
|
const cancelReplayTool = tools.find((tool) => tool.name === 'browser_cancel_replay');
|
|
expect(cancelReplayTool?.inputSchema).toMatchObject({ type: 'object' });
|
|
|
|
const startOrchestrationTool = tools.find((tool) => tool.name === 'browser_start_orchestration');
|
|
expect(startOrchestrationTool?.inputSchema?.properties?.blocks).toMatchObject({ type: 'array' });
|
|
expect(startOrchestrationTool?.inputSchema?.properties?.pageIndex).toMatchObject({ type: 'integer' });
|
|
expect(startOrchestrationTool?.inputSchema?.properties?.pageId).toMatchObject({ type: 'string' });
|
|
|
|
const getOrchestrationTool = tools.find((tool) => tool.name === 'browser_get_orchestration');
|
|
expect(getOrchestrationTool?.inputSchema).toMatchObject({ type: 'object' });
|
|
|
|
const cancelOrchestrationTool = tools.find((tool) => tool.name === 'browser_cancel_orchestration');
|
|
expect(cancelOrchestrationTool?.inputSchema).toMatchObject({ type: 'object' });
|
|
|
|
const exportArtifactTool = tools.find((tool) => tool.name === 'browser_export_artifact');
|
|
expect(exportArtifactTool?.inputSchema?.properties?.kind).toMatchObject({ type: 'string' });
|
|
expect(exportArtifactTool?.inputSchema?.properties?.name).toMatchObject({ type: 'string' });
|
|
|
|
const importArtifactTool = tools.find((tool) => tool.name === 'browser_import_artifact');
|
|
expect(importArtifactTool?.inputSchema?.properties?.path).toMatchObject({ type: 'string' });
|
|
|
|
const listArtifactsTool = tools.find((tool) => tool.name === 'browser_list_artifacts');
|
|
expect(listArtifactsTool?.inputSchema).toMatchObject({ type: 'object' });
|
|
|
|
const deleteArtifactTool = tools.find((tool) => tool.name === 'browser_delete_artifact');
|
|
expect(deleteArtifactTool?.inputSchema?.properties?.name).toMatchObject({ type: 'string' });
|
|
|
|
const queryTool = tools.find((tool) => tool.name === 'browser_query');
|
|
expect(queryTool?.inputSchema?.properties?.fields).toMatchObject({
|
|
type: 'array',
|
|
});
|
|
|
|
for (const tool of tools.filter((candidate) => candidate.inputSchema?.properties?.pageIndex)) {
|
|
expect(tool.inputSchema?.properties?.pageIndex).toMatchObject({
|
|
type: 'integer',
|
|
minimum: 0,
|
|
});
|
|
}
|
|
});
|
|
|
|
it('marks the selected page in browser_get_session_info', async () => {
|
|
const responses = await runMcpRequests(
|
|
[
|
|
{ id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' },
|
|
{ id: 'page-2', title: 'Docs', currentUrl: 'https://example.com/docs' },
|
|
],
|
|
[
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 801,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_select_page', arguments: { pageIndex: 1 } },
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 802,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_get_session_info', arguments: {} },
|
|
},
|
|
]
|
|
);
|
|
|
|
const text = getResponseText(responses.find((message) => message.id === 802));
|
|
expect(text).toContain('selected: true');
|
|
expect(text).toContain('1. Docs');
|
|
});
|
|
|
|
it('uses the selected page when pageIndex is omitted', async () => {
|
|
const responses = await runMcpRequests(
|
|
[
|
|
{
|
|
id: 'page-1',
|
|
title: 'Home',
|
|
currentUrl: 'https://example.com/',
|
|
visibleText: 'Home text',
|
|
},
|
|
{
|
|
id: 'page-2',
|
|
title: 'Docs',
|
|
currentUrl: 'https://example.com/docs',
|
|
visibleText: 'Docs text',
|
|
},
|
|
],
|
|
[
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 811,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_select_page', arguments: { pageIndex: 1 } },
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 812,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_get_visible_text', arguments: {} },
|
|
},
|
|
]
|
|
);
|
|
|
|
const text = getResponseText(responses.find((message) => message.id === 812));
|
|
expect(text).toContain('Docs text');
|
|
});
|
|
|
|
it('opens a page and makes it selected', async () => {
|
|
const responses = await runMcpRequests(
|
|
[{ id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' }],
|
|
[
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 821,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'browser_open_page',
|
|
arguments: { url: 'https://example.com/new' },
|
|
},
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 822,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_get_session_info', arguments: {} },
|
|
},
|
|
]
|
|
);
|
|
|
|
const openText = getResponseText(responses.find((message) => message.id === 821));
|
|
expect(openText).toContain('status: opened');
|
|
expect(openText).toContain('url: https://example.com/new');
|
|
|
|
const listText = getResponseText(responses.find((message) => message.id === 822));
|
|
expect(listText).toContain('https://example.com/new');
|
|
expect(listText).toContain('selected: true');
|
|
});
|
|
|
|
it('closes the selected page and falls back deterministically', async () => {
|
|
const responses = await runMcpRequests(
|
|
[
|
|
{ id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' },
|
|
{ id: 'page-2', title: 'Docs', currentUrl: 'https://example.com/docs' },
|
|
],
|
|
[
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 831,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_select_page', arguments: { pageIndex: 1 } },
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 832,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_close_page', arguments: {} },
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 833,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_get_session_info', arguments: {} },
|
|
},
|
|
]
|
|
);
|
|
|
|
const closeText = getResponseText(responses.find((message) => message.id === 832));
|
|
expect(closeText).toContain('status: closed');
|
|
|
|
const listText = getResponseText(responses.find((message) => message.id === 833));
|
|
expect(listText).toContain('0. Home');
|
|
expect(listText).toContain('selected: true');
|
|
expect(listText).not.toContain('Docs');
|
|
});
|
|
|
|
it('keeps the selected page when closing a different page', async () => {
|
|
const responses = await runMcpRequests(
|
|
[
|
|
{
|
|
id: 'page-1',
|
|
title: 'Home',
|
|
currentUrl: 'https://example.com/',
|
|
visibleText: 'Home text',
|
|
},
|
|
{
|
|
id: 'page-2',
|
|
title: 'Docs',
|
|
currentUrl: 'https://example.com/docs',
|
|
visibleText: 'Docs text',
|
|
},
|
|
{
|
|
id: 'page-3',
|
|
title: 'Pricing',
|
|
currentUrl: 'https://example.com/pricing',
|
|
visibleText: 'Pricing text',
|
|
},
|
|
],
|
|
[
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 834,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_select_page', arguments: { pageIndex: 0 } },
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 835,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_close_page', arguments: { pageId: 'page-3' } },
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 836,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_get_visible_text', arguments: {} },
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 837,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_get_session_info', arguments: {} },
|
|
},
|
|
]
|
|
);
|
|
|
|
const visibleText = getResponseText(responses.find((message) => message.id === 836));
|
|
expect(visibleText).toContain('Home text');
|
|
|
|
const listText = getResponseText(responses.find((message) => message.id === 837));
|
|
expect(listText).toContain('0. Home');
|
|
expect(listText).toContain('selected: true');
|
|
expect(listText).not.toContain('Pricing');
|
|
});
|
|
|
|
it('reconciles a stale selected page when browser_get_session_info is called', async () => {
|
|
const responses = await runMcpRequests(
|
|
[{ id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' }],
|
|
[
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 841,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_open_page', arguments: { url: 'https://example.com/new' } },
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 842,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_close_page', arguments: { pageId: 'page-2' } },
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 843,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_get_session_info', arguments: {} },
|
|
},
|
|
]
|
|
);
|
|
|
|
const listText = getResponseText(responses.find((message) => message.id === 843));
|
|
expect(listText).toContain('0. Home');
|
|
expect(listText).toContain('selected: true');
|
|
});
|
|
|
|
it('closes a page by pageId', async () => {
|
|
const responses = await runMcpRequests(
|
|
[{ id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' }],
|
|
[
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 851,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_open_page', arguments: { url: 'https://example.com/new' } },
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 852,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_close_page', arguments: { pageId: 'page-2' } },
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 853,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_get_session_info', arguments: {} },
|
|
},
|
|
]
|
|
);
|
|
|
|
const closeText = getResponseText(responses.find((message) => message.id === 852));
|
|
expect(closeText).toContain('pageId: page-2');
|
|
expect(closeText).toContain('status: closed');
|
|
|
|
const listText = getResponseText(responses.find((message) => message.id === 853));
|
|
expect(listText).toContain('0. Home');
|
|
expect(listText).not.toContain('https://example.com/new');
|
|
});
|
|
|
|
it('adds an interception rule and lists it by bound pageId', async () => {
|
|
const responses = await runMcpRequests(
|
|
[
|
|
{ id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' },
|
|
{ id: 'page-2', title: 'Docs', currentUrl: 'https://example.com/docs' },
|
|
],
|
|
[
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 901,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'browser_select_page',
|
|
arguments: { pageIndex: 1 },
|
|
},
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 902,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'browser_add_intercept_rule',
|
|
arguments: { urlIncludes: '/api', method: 'GET', action: 'continue' },
|
|
},
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 903,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'browser_list_intercept_rules',
|
|
arguments: {},
|
|
},
|
|
},
|
|
]
|
|
);
|
|
|
|
const listText = getResponseText(responses.find((message) => message.id === 903));
|
|
expect(listText).toContain('pageId: page-2');
|
|
expect(listText).toContain('urlIncludes: /api');
|
|
expect(listText).toContain('method: GET');
|
|
expect(listText).toContain('action: continue');
|
|
});
|
|
|
|
it('keeps an existing rule bound to the original page after selected page changes', async () => {
|
|
const responses = await runMcpRequests(
|
|
[
|
|
{ id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' },
|
|
{ id: 'page-2', title: 'Docs', currentUrl: 'https://example.com/docs' },
|
|
],
|
|
[
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 911,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_select_page', arguments: { pageIndex: 0 } },
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 912,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'browser_add_intercept_rule',
|
|
arguments: { urlIncludes: '/api', method: 'POST', action: 'fail' },
|
|
},
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 913,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_select_page', arguments: { pageIndex: 1 } },
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 914,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_list_intercept_rules', arguments: {} },
|
|
},
|
|
]
|
|
);
|
|
|
|
const listText = getResponseText(responses.find((message) => message.id === 914));
|
|
expect(listText).toContain('pageId: page-1');
|
|
expect(listText).toContain('method: POST');
|
|
expect(listText).toContain('action: fail');
|
|
});
|
|
|
|
it('keeps richer matching rules bound to the original page after selected page changes', async () => {
|
|
const responses = await runMcpRequests(
|
|
[
|
|
{ id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' },
|
|
{ id: 'page-2', title: 'Docs', currentUrl: 'https://example.com/docs' },
|
|
],
|
|
[
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 1003,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_select_page', arguments: { pageIndex: 1 } },
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 1004,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'browser_add_intercept_rule',
|
|
arguments: {
|
|
resourceType: 'XHR',
|
|
priority: 7,
|
|
action: 'continue',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 1005,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_select_page', arguments: { pageIndex: 0 } },
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 1006,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_list_intercept_rules', arguments: {} },
|
|
},
|
|
]
|
|
);
|
|
|
|
const listText = getResponseText(responses.find((message) => message.id === 1006));
|
|
expect(listText).toContain('pageId: page-2');
|
|
expect(listText).toContain('resourceType: XHR');
|
|
expect(listText).toContain('priority: 7');
|
|
});
|
|
|
|
it('removes an interception rule by ruleId', async () => {
|
|
const responses = await runMcpRequests(
|
|
[{ id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' }],
|
|
[
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 921,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'browser_add_intercept_rule',
|
|
arguments: { urlIncludes: '/api', method: 'GET', action: 'continue' },
|
|
},
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 922,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'browser_remove_intercept_rule',
|
|
arguments: { ruleId: 'rule-1' },
|
|
},
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 923,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_list_intercept_rules', arguments: {} },
|
|
},
|
|
]
|
|
);
|
|
|
|
const removeText = getResponseText(responses.find((message) => message.id === 922));
|
|
expect(removeText).toContain('ruleId: rule-1');
|
|
expect(removeText).toContain('status: removed');
|
|
|
|
const listText = getResponseText(responses.find((message) => message.id === 923));
|
|
expect(listText).toBe('status: empty');
|
|
});
|
|
|
|
it('records recent requests with matched rule action summaries', async () => {
|
|
const responses = await runMcpRequests(
|
|
[
|
|
{
|
|
id: 'page-1',
|
|
title: 'Home',
|
|
currentUrl: 'https://example.com/',
|
|
intercept: {
|
|
pausedRequests: [
|
|
{
|
|
requestId: 'req-1',
|
|
url: 'https://example.com/api/users',
|
|
method: 'GET',
|
|
resourceType: 'XHR',
|
|
},
|
|
{
|
|
requestId: 'req-2',
|
|
url: 'https://example.com/assets/app.js',
|
|
method: 'GET',
|
|
resourceType: 'Script',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
[
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 931,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'browser_add_intercept_rule',
|
|
arguments: { urlIncludes: '/api', method: 'GET', action: 'fail' },
|
|
},
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 932,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_list_requests', arguments: {} },
|
|
},
|
|
],
|
|
{
|
|
responseTimeoutMs: 12000,
|
|
}
|
|
);
|
|
|
|
const listText = getResponseText(responses.find((message) => message.id === 932));
|
|
expect(listText).toContain('requestId: req-1');
|
|
expect(listText).toContain('matchedRuleId: rule-1');
|
|
expect(listText).toContain('action: fail');
|
|
expect(listText).toContain('requestId: req-2');
|
|
expect(listText).toContain('action: continue');
|
|
});
|
|
|
|
it('removes rules and recent requests bound to a page after that page is closed', async () => {
|
|
const responses = await runMcpRequests(
|
|
[
|
|
{ id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' },
|
|
{
|
|
id: 'page-2',
|
|
title: 'Docs',
|
|
currentUrl: 'https://example.com/docs',
|
|
intercept: {
|
|
pausedRequests: [{ requestId: 'req-closed', url: 'https://example.com/api/docs', method: 'GET' }],
|
|
},
|
|
},
|
|
],
|
|
[
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 941,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_select_page', arguments: { pageIndex: 1 } },
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 942,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'browser_add_intercept_rule',
|
|
arguments: { urlIncludes: '/api', method: 'GET', action: 'continue' },
|
|
},
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 943,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_list_requests', arguments: {} },
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 944,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_close_page', arguments: { pageId: 'page-2' } },
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 945,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_list_intercept_rules', arguments: {} },
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 946,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_list_requests', arguments: {} },
|
|
},
|
|
],
|
|
{
|
|
responseTimeoutMs: 12000,
|
|
}
|
|
);
|
|
|
|
const preCloseRequestsText = getResponseText(responses.find((message) => message.id === 943));
|
|
expect(preCloseRequestsText).toContain('requestId: req-closed');
|
|
|
|
const listRulesText = getResponseText(responses.find((message) => message.id === 945));
|
|
expect(listRulesText).not.toContain('pageId: page-2');
|
|
|
|
const postCloseRequestsText = getResponseText(responses.find((message) => message.id === 946));
|
|
expect(postCloseRequestsText).not.toContain('requestId: req-closed');
|
|
expect(postCloseRequestsText).not.toContain('pageId: page-2');
|
|
});
|
|
|
|
it('rejects browser_add_intercept_rule when pageIndex and pageId are both provided', async () => {
|
|
const responses = await runMcpRequests(
|
|
[{ id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' }],
|
|
[
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 951,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'browser_add_intercept_rule',
|
|
arguments: {
|
|
pageIndex: 0,
|
|
pageId: 'page-1',
|
|
urlIncludes: '/api',
|
|
action: 'continue',
|
|
},
|
|
},
|
|
},
|
|
]
|
|
);
|
|
|
|
const response = responses.find((message) => message.id === 951);
|
|
expect((response?.result as { isError?: boolean }).isError).toBe(true);
|
|
expect(getResponseText(response)).toContain('Browser MCP failed: pageIndex and pageId cannot be used together');
|
|
});
|
|
|
|
it('rejects browser_add_intercept_rule when action is invalid', async () => {
|
|
const responses = await runMcpRequests(
|
|
[{ id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' }],
|
|
[
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 952,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'browser_add_intercept_rule',
|
|
arguments: {
|
|
urlIncludes: '/api',
|
|
action: 'mock',
|
|
},
|
|
},
|
|
},
|
|
]
|
|
);
|
|
|
|
const response = responses.find((message) => message.id === 952);
|
|
expect((response?.result as { isError?: boolean }).isError).toBe(true);
|
|
expect(getResponseText(response)).toContain('Browser MCP failed: action must be one of: continue, fail');
|
|
});
|
|
|
|
it('rejects intercept rules when urlPattern and urlRegex are both provided', async () => {
|
|
const responses = await runMcpRequests(
|
|
[{ id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' }],
|
|
[
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 981,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'browser_add_intercept_rule',
|
|
arguments: {
|
|
urlPattern: 'https://example.com/api/*',
|
|
urlRegex: '^https://example\\.com/api/',
|
|
action: 'continue',
|
|
},
|
|
},
|
|
},
|
|
]
|
|
);
|
|
|
|
const response = responses.find((message) => message.id === 981);
|
|
expect((response?.result as { isError?: boolean }).isError).toBe(true);
|
|
expect(getResponseText(response)).toContain('Browser MCP failed: urlPattern and urlRegex cannot be used together');
|
|
});
|
|
|
|
it('rejects intercept rules when priority is not an integer', async () => {
|
|
const responses = await runMcpRequests(
|
|
[{ id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' }],
|
|
[
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 982,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'browser_add_intercept_rule',
|
|
arguments: {
|
|
urlIncludes: '/api',
|
|
priority: 1.5,
|
|
action: 'continue',
|
|
},
|
|
},
|
|
},
|
|
]
|
|
);
|
|
|
|
const response = responses.find((message) => message.id === 982);
|
|
expect((response?.result as { isError?: boolean }).isError).toBe(true);
|
|
expect(getResponseText(response)).toContain('Browser MCP failed: priority must be an integer');
|
|
});
|
|
|
|
it('rejects intercept rules when headerMatchers is not an array', async () => {
|
|
const responses = await runMcpRequests(
|
|
[{ id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' }],
|
|
[
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 983,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'browser_add_intercept_rule',
|
|
arguments: {
|
|
headerMatchers: 'x',
|
|
action: 'continue',
|
|
},
|
|
},
|
|
},
|
|
]
|
|
);
|
|
|
|
const response = responses.find((message) => message.id === 983);
|
|
expect((response?.result as { isError?: boolean }).isError).toBe(true);
|
|
expect(getResponseText(response)).toContain('Browser MCP failed: headerMatchers must be an array');
|
|
});
|
|
|
|
it('rejects intercept rules when a header matcher is missing name', async () => {
|
|
const responses = await runMcpRequests(
|
|
[{ id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' }],
|
|
[
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 984,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'browser_add_intercept_rule',
|
|
arguments: {
|
|
headerMatchers: [{ valueIncludes: 'staging' }],
|
|
action: 'continue',
|
|
},
|
|
},
|
|
},
|
|
]
|
|
);
|
|
|
|
const response = responses.find((message) => message.id === 984);
|
|
expect((response?.result as { isError?: boolean }).isError).toBe(true);
|
|
expect(getResponseText(response)).toContain('Browser MCP failed: headerMatchers.name is required');
|
|
});
|
|
|
|
it('rejects intercept rules when a header matcher has no value matcher', async () => {
|
|
const responses = await runMcpRequests(
|
|
[{ id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' }],
|
|
[
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 985,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'browser_add_intercept_rule',
|
|
arguments: {
|
|
headerMatchers: [{ name: 'x-env' }],
|
|
action: 'continue',
|
|
},
|
|
},
|
|
},
|
|
]
|
|
);
|
|
|
|
const response = responses.find((message) => message.id === 985);
|
|
expect((response?.result as { isError?: boolean }).isError).toBe(true);
|
|
expect(getResponseText(response)).toContain('Browser MCP failed: headerMatchers entry must include valueIncludes or valueRegex');
|
|
});
|
|
|
|
it('rejects intercept rules when no matching condition is provided', async () => {
|
|
const responses = await runMcpRequests(
|
|
[{ id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' }],
|
|
[
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 986,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'browser_add_intercept_rule',
|
|
arguments: {
|
|
priority: 10,
|
|
action: 'continue',
|
|
},
|
|
},
|
|
},
|
|
]
|
|
);
|
|
|
|
const response = responses.find((message) => message.id === 986);
|
|
expect((response?.result as { isError?: boolean }).isError).toBe(true);
|
|
expect(getResponseText(response)).toContain('Browser MCP failed: at least one matching condition is required');
|
|
});
|
|
|
|
it('adds a resourceType interception rule and lists its richer matching summary', async () => {
|
|
const responses = await runMcpRequests(
|
|
[{ id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' }],
|
|
[
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 987,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'browser_add_intercept_rule',
|
|
arguments: {
|
|
resourceType: 'XHR',
|
|
priority: 10,
|
|
action: 'continue',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 988,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_list_intercept_rules', arguments: {} },
|
|
},
|
|
]
|
|
);
|
|
|
|
const listText = getResponseText(responses.find((message) => message.id === 988));
|
|
expect(listText).toContain('resourceType: XHR');
|
|
expect(listText).toContain('priority: 10');
|
|
});
|
|
|
|
it('prefers the higher-priority matched rule over an earlier lower-priority rule', async () => {
|
|
const pages: MockPageState[] = [
|
|
{
|
|
id: 'page-1',
|
|
title: 'Home',
|
|
currentUrl: 'https://example.com/',
|
|
intercept: {
|
|
pausedRequests: [
|
|
{
|
|
requestId: 'req-priority',
|
|
url: 'https://example.com/api/orders',
|
|
method: 'GET',
|
|
resourceType: 'XHR',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
];
|
|
const responses = await runMcpRequests(
|
|
pages,
|
|
[
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 989,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'browser_add_intercept_rule',
|
|
arguments: {
|
|
urlIncludes: '/api',
|
|
action: 'fulfill',
|
|
statusCode: 201,
|
|
body: 'low',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 990,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'browser_add_intercept_rule',
|
|
arguments: {
|
|
resourceType: 'XHR',
|
|
priority: 10,
|
|
action: 'fulfill',
|
|
statusCode: 202,
|
|
body: 'high',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 991,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_list_requests', arguments: {} },
|
|
},
|
|
],
|
|
{
|
|
responseTimeoutMs: 12000,
|
|
}
|
|
);
|
|
|
|
const listText = getResponseText(responses.find((message) => message.id === 991));
|
|
expect(listText).toContain('requestId: req-priority');
|
|
expect(listText).toContain('matchedRuleId: rule-2');
|
|
expect(pages[0]?.intercept?.fulfilledRequests?.[0]?.responseCode).toBe(202);
|
|
});
|
|
|
|
it('keeps creation order when matched rules have the same priority', async () => {
|
|
const pages: MockPageState[] = [
|
|
{
|
|
id: 'page-1',
|
|
title: 'Home',
|
|
currentUrl: 'https://example.com/',
|
|
intercept: {
|
|
pausedRequests: [
|
|
{
|
|
requestId: 'req-same-priority',
|
|
url: 'https://example.com/api/orders',
|
|
method: 'GET',
|
|
resourceType: 'XHR',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
];
|
|
const responses = await runMcpRequests(
|
|
pages,
|
|
[
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 9911,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'browser_add_intercept_rule',
|
|
arguments: {
|
|
urlIncludes: '/api',
|
|
priority: 5,
|
|
action: 'fulfill',
|
|
statusCode: 201,
|
|
body: 'first',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 9912,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'browser_add_intercept_rule',
|
|
arguments: {
|
|
resourceType: 'XHR',
|
|
priority: 5,
|
|
action: 'fulfill',
|
|
statusCode: 202,
|
|
body: 'second',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 9913,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_list_requests', arguments: {} },
|
|
},
|
|
],
|
|
{
|
|
responseTimeoutMs: 12000,
|
|
}
|
|
);
|
|
|
|
const listText = getResponseText(responses.find((message) => message.id === 9913));
|
|
expect(listText).toContain('requestId: req-same-priority');
|
|
expect(listText).toContain('matchedRuleId: rule-1');
|
|
expect(pages[0]?.intercept?.fulfilledRequests?.[0]?.responseCode).toBe(201);
|
|
});
|
|
|
|
it('matches urlPattern rules with wildcard syntax', async () => {
|
|
const pages: MockPageState[] = [
|
|
{
|
|
id: 'page-1',
|
|
title: 'Home',
|
|
currentUrl: 'https://example.com/',
|
|
intercept: {
|
|
pausedRequests: [
|
|
{
|
|
requestId: 'req-pattern',
|
|
url: 'https://example.com/api/v1/users',
|
|
method: 'GET',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
];
|
|
const responses = await runMcpRequests(
|
|
pages,
|
|
[
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 992,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'browser_add_intercept_rule',
|
|
arguments: {
|
|
urlPattern: 'https://example.com/api/*',
|
|
action: 'fail',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 993,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_list_requests', arguments: {} },
|
|
},
|
|
],
|
|
{
|
|
responseTimeoutMs: 12000,
|
|
}
|
|
);
|
|
|
|
const listText = getResponseText(responses.find((message) => message.id === 993));
|
|
expect(listText).toContain('requestId: req-pattern');
|
|
expect(listText).toContain('action: fail');
|
|
expect(pages[0]?.intercept?.failedRequests?.[0]?.requestId).toBe('req-pattern');
|
|
});
|
|
|
|
it('matches urlRegex rules', async () => {
|
|
const pages: MockPageState[] = [
|
|
{
|
|
id: 'page-1',
|
|
title: 'Home',
|
|
currentUrl: 'https://example.com/',
|
|
intercept: {
|
|
pausedRequests: [
|
|
{
|
|
requestId: 'req-regex',
|
|
url: 'https://example.com/api/users',
|
|
method: 'GET',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
];
|
|
const responses = await runMcpRequests(
|
|
pages,
|
|
[
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 994,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'browser_add_intercept_rule',
|
|
arguments: {
|
|
urlRegex: '^https://example\\.com/api/(users|teams)$',
|
|
action: 'continue',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 995,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_list_requests', arguments: {} },
|
|
},
|
|
],
|
|
{
|
|
responseTimeoutMs: 12000,
|
|
}
|
|
);
|
|
|
|
const listText = getResponseText(responses.find((message) => message.id === 995));
|
|
expect(listText).toContain('requestId: req-regex');
|
|
expect(listText).toContain('matchedRuleId: rule-1');
|
|
expect(pages[0]?.intercept?.continuedRequestIds).toContain('req-regex');
|
|
});
|
|
|
|
it('matches headerMatchers using valueIncludes and case-insensitive header names', async () => {
|
|
const pages: MockPageState[] = [
|
|
{
|
|
id: 'page-1',
|
|
title: 'Home',
|
|
currentUrl: 'https://example.com/',
|
|
intercept: {
|
|
pausedRequests: [
|
|
{
|
|
requestId: 'req-header-includes',
|
|
url: 'https://example.com/api/header-includes',
|
|
method: 'GET',
|
|
requestHeaders: {
|
|
'X-Env': 'staging-us',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
];
|
|
const responses = await runMcpRequests(
|
|
pages,
|
|
[
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 996,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'browser_add_intercept_rule',
|
|
arguments: {
|
|
headerMatchers: [{ name: 'x-env', valueIncludes: 'staging' }],
|
|
action: 'fulfill',
|
|
statusCode: 207,
|
|
body: 'matched-includes',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 997,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_list_requests', arguments: {} },
|
|
},
|
|
],
|
|
{
|
|
responseTimeoutMs: 12000,
|
|
}
|
|
);
|
|
|
|
const listText = getResponseText(responses.find((message) => message.id === 997));
|
|
expect(listText).toContain('requestId: req-header-includes');
|
|
expect(listText).toContain('matchedRuleId: rule-1');
|
|
expect(pages[0]?.intercept?.fulfilledRequests?.[0]?.responseCode).toBe(207);
|
|
});
|
|
|
|
it('matches headerMatchers using valueRegex', async () => {
|
|
const pages: MockPageState[] = [
|
|
{
|
|
id: 'page-1',
|
|
title: 'Home',
|
|
currentUrl: 'https://example.com/',
|
|
intercept: {
|
|
pausedRequests: [
|
|
{
|
|
requestId: 'req-header-regex',
|
|
url: 'https://example.com/api/header-regex',
|
|
method: 'GET',
|
|
requestHeaders: {
|
|
'x-tenant': 'acme-prod',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
];
|
|
const responses = await runMcpRequests(
|
|
pages,
|
|
[
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 998,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'browser_add_intercept_rule',
|
|
arguments: {
|
|
headerMatchers: [{ name: 'X-Tenant', valueRegex: '^acme-' }],
|
|
action: 'continue',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 999,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_list_requests', arguments: {} },
|
|
},
|
|
],
|
|
{
|
|
responseTimeoutMs: 12000,
|
|
}
|
|
);
|
|
|
|
const listText = getResponseText(responses.find((message) => message.id === 999));
|
|
expect(listText).toContain('requestId: req-header-regex');
|
|
expect(listText).toContain('matchedRuleId: rule-1');
|
|
expect(pages[0]?.intercept?.continuedRequestIds).toContain('req-header-regex');
|
|
});
|
|
|
|
it('adds a fulfill interception rule and lists its response summary', async () => {
|
|
const responses = await runMcpRequests(
|
|
[{ id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' }],
|
|
[
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 961,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'browser_add_intercept_rule',
|
|
arguments: {
|
|
urlIncludes: '/api/mock',
|
|
method: 'GET',
|
|
action: 'fulfill',
|
|
statusCode: 202,
|
|
contentType: 'application/json',
|
|
body: '{"ok":true}',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 962,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_list_intercept_rules', arguments: {} },
|
|
},
|
|
]
|
|
);
|
|
|
|
const listText = getResponseText(responses.find((message) => message.id === 962));
|
|
expect(listText).toContain('action: fulfill');
|
|
expect(listText).toContain('statusCode: 202');
|
|
expect(listText).toContain('contentType: application/json');
|
|
});
|
|
|
|
it('fulfills a paused request with the configured mock response', async () => {
|
|
const pages: MockPageState[] = [
|
|
{
|
|
id: 'page-1',
|
|
title: 'Home',
|
|
currentUrl: 'https://example.com/',
|
|
intercept: {
|
|
pausedRequests: [
|
|
{
|
|
requestId: 'req-fulfill-1',
|
|
url: 'https://example.com/api/mock/users',
|
|
method: 'GET',
|
|
resourceType: 'XHR',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
];
|
|
const responses = await runMcpRequests(
|
|
pages,
|
|
[
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 963,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'browser_add_intercept_rule',
|
|
arguments: {
|
|
urlIncludes: '/api/mock',
|
|
method: 'GET',
|
|
action: 'fulfill',
|
|
statusCode: 200,
|
|
contentType: 'application/json',
|
|
body: '{"users":[1]}',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 964,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_list_requests', arguments: {} },
|
|
},
|
|
],
|
|
{
|
|
responseTimeoutMs: 12000,
|
|
}
|
|
);
|
|
|
|
const listText = getResponseText(responses.find((message) => message.id === 964));
|
|
expect(listText).toContain('requestId: req-fulfill-1');
|
|
expect(listText).toContain('matchedRuleId: rule-1');
|
|
expect(listText).toContain('action: fulfill');
|
|
expect(pages[0]?.intercept?.fulfilledRequests).toEqual([
|
|
expect.objectContaining({
|
|
requestId: 'req-fulfill-1',
|
|
responseCode: 200,
|
|
}),
|
|
]);
|
|
});
|
|
|
|
it('passes custom response headers to Fetch.fulfillRequest', async () => {
|
|
const pages: MockPageState[] = [
|
|
{
|
|
id: 'page-1',
|
|
title: 'Home',
|
|
currentUrl: 'https://example.com/',
|
|
intercept: {
|
|
pausedRequests: [
|
|
{
|
|
requestId: 'req-fulfill-2',
|
|
url: 'https://example.com/api/mock/headers',
|
|
method: 'GET',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
];
|
|
const responses = await runMcpRequests(
|
|
pages,
|
|
[
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 965,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'browser_add_intercept_rule',
|
|
arguments: {
|
|
urlIncludes: '/api/mock/headers',
|
|
action: 'fulfill',
|
|
statusCode: 201,
|
|
responseHeaders: [
|
|
{ name: 'Cache-Control', value: 'no-store' },
|
|
{ name: 'X-Mocked-By', value: 'ccs-browser' },
|
|
],
|
|
body: 'ok',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 966,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_list_requests', arguments: {} },
|
|
},
|
|
],
|
|
{
|
|
responseTimeoutMs: 12000,
|
|
}
|
|
);
|
|
|
|
const listText = getResponseText(responses.find((message) => message.id === 966));
|
|
expect(listText).toContain('action: fulfill');
|
|
expect(pages[0]?.intercept?.fulfilledRequests?.[0]?.responseHeaders).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ name: 'Cache-Control', value: 'no-store' }),
|
|
expect.objectContaining({ name: 'X-Mocked-By', value: 'ccs-browser' }),
|
|
])
|
|
);
|
|
});
|
|
|
|
it('allows fulfill rules with an empty response body', async () => {
|
|
const responses = await runMcpRequests(
|
|
[{ id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' }],
|
|
[
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 967,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'browser_add_intercept_rule',
|
|
arguments: {
|
|
urlIncludes: '/api/empty',
|
|
action: 'fulfill',
|
|
statusCode: 204,
|
|
contentType: 'text/plain',
|
|
body: '',
|
|
},
|
|
},
|
|
},
|
|
]
|
|
);
|
|
|
|
const addText = getResponseText(responses.find((message) => message.id === 967));
|
|
expect(addText).toContain('action: fulfill');
|
|
expect(addText).toContain('statusCode: 204');
|
|
});
|
|
|
|
it('rejects fulfill rules when statusCode is out of range', async () => {
|
|
const responses = await runMcpRequests(
|
|
[{ id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' }],
|
|
[
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 968,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'browser_add_intercept_rule',
|
|
arguments: {
|
|
urlIncludes: '/api/mock',
|
|
action: 'fulfill',
|
|
statusCode: 99,
|
|
body: 'x',
|
|
},
|
|
},
|
|
},
|
|
]
|
|
);
|
|
|
|
const response = responses.find((message) => message.id === 968);
|
|
expect((response?.result as { isError?: boolean }).isError).toBe(true);
|
|
expect(getResponseText(response)).toContain('Browser MCP failed: statusCode must be an integer between 100 and 599');
|
|
});
|
|
|
|
it('rejects fulfill rules when responseHeaders is not an array', async () => {
|
|
const responses = await runMcpRequests(
|
|
[{ id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' }],
|
|
[
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 969,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'browser_add_intercept_rule',
|
|
arguments: {
|
|
urlIncludes: '/api/mock',
|
|
action: 'fulfill',
|
|
responseHeaders: 'x',
|
|
body: 'x',
|
|
},
|
|
},
|
|
},
|
|
]
|
|
);
|
|
|
|
const response = responses.find((message) => message.id === 969);
|
|
expect((response?.result as { isError?: boolean }).isError).toBe(true);
|
|
expect(getResponseText(response)).toContain('Browser MCP failed: responseHeaders must be an array');
|
|
});
|
|
|
|
it('rejects fulfill rules when a responseHeaders entry is missing name', async () => {
|
|
const responses = await runMcpRequests(
|
|
[{ id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' }],
|
|
[
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 970,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'browser_add_intercept_rule',
|
|
arguments: {
|
|
urlIncludes: '/api/mock',
|
|
action: 'fulfill',
|
|
responseHeaders: [{ value: 'x' }],
|
|
body: 'x',
|
|
},
|
|
},
|
|
},
|
|
]
|
|
);
|
|
|
|
const response = responses.find((message) => message.id === 970);
|
|
expect((response?.result as { isError?: boolean }).isError).toBe(true);
|
|
expect(getResponseText(response)).toContain('Browser MCP failed: responseHeaders.name is required');
|
|
});
|
|
|
|
it('rejects fulfill rules when body is not a string', async () => {
|
|
const responses = await runMcpRequests(
|
|
[{ id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' }],
|
|
[
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 971,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'browser_add_intercept_rule',
|
|
arguments: {
|
|
urlIncludes: '/api/mock',
|
|
action: 'fulfill',
|
|
body: 123,
|
|
},
|
|
},
|
|
},
|
|
]
|
|
);
|
|
|
|
const response = responses.find((message) => message.id === 971);
|
|
expect((response?.result as { isError?: boolean }).isError).toBe(true);
|
|
expect(getResponseText(response)).toContain('Browser MCP failed: body must be a string');
|
|
});
|
|
|
|
it('rejects intercept rules when urlRegex is invalid', async () => {
|
|
const responses = await runMcpRequests(
|
|
[{ id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' }],
|
|
[
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 1001,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'browser_add_intercept_rule',
|
|
arguments: {
|
|
urlRegex: '[',
|
|
action: 'continue',
|
|
},
|
|
},
|
|
},
|
|
]
|
|
);
|
|
|
|
const response = responses.find((message) => message.id === 1001);
|
|
expect((response?.result as { isError?: boolean }).isError).toBe(true);
|
|
expect(getResponseText(response)).toContain('Browser MCP failed: urlRegex must be a valid regular expression');
|
|
});
|
|
|
|
it('rejects intercept rules when headerMatchers.valueRegex is invalid', async () => {
|
|
const responses = await runMcpRequests(
|
|
[{ id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' }],
|
|
[
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 1002,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'browser_add_intercept_rule',
|
|
arguments: {
|
|
headerMatchers: [{ name: 'x-env', valueRegex: '[' }],
|
|
action: 'continue',
|
|
},
|
|
},
|
|
},
|
|
]
|
|
);
|
|
|
|
const response = responses.find((message) => message.id === 1002);
|
|
expect((response?.result as { isError?: boolean }).isError).toBe(true);
|
|
expect(getResponseText(response)).toContain('Browser MCP failed: headerMatchers.valueRegex must be a valid regular expression');
|
|
});
|
|
|
|
it('removes fulfill rules and request summaries after the bound page is closed', async () => {
|
|
const responses = await runMcpRequests(
|
|
[
|
|
{ id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' },
|
|
{
|
|
id: 'page-2',
|
|
title: 'Mocked',
|
|
currentUrl: 'https://example.com/mocked',
|
|
intercept: {
|
|
pausedRequests: [
|
|
{ requestId: 'req-fulfill-close', url: 'https://example.com/api/mock/close', method: 'GET' },
|
|
],
|
|
},
|
|
},
|
|
],
|
|
[
|
|
{ jsonrpc: '2.0', id: 972, method: 'tools/call', params: { name: 'browser_select_page', arguments: { pageIndex: 1 } } },
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 973,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'browser_add_intercept_rule',
|
|
arguments: {
|
|
urlIncludes: '/api/mock/close',
|
|
action: 'fulfill',
|
|
statusCode: 200,
|
|
body: 'done',
|
|
},
|
|
},
|
|
},
|
|
{ jsonrpc: '2.0', id: 974, method: 'tools/call', params: { name: 'browser_list_requests', arguments: {} } },
|
|
{ jsonrpc: '2.0', id: 975, method: 'tools/call', params: { name: 'browser_close_page', arguments: { pageId: 'page-2' } } },
|
|
{ jsonrpc: '2.0', id: 976, method: 'tools/call', params: { name: 'browser_list_intercept_rules', arguments: {} } },
|
|
{ jsonrpc: '2.0', id: 977, method: 'tools/call', params: { name: 'browser_list_requests', arguments: {} } },
|
|
],
|
|
{
|
|
responseTimeoutMs: 12000,
|
|
}
|
|
);
|
|
|
|
expect(getResponseText(responses.find((message) => message.id === 974))).toContain('requestId: req-fulfill-close');
|
|
expect(getResponseText(responses.find((message) => message.id === 976))).not.toContain('pageId: page-2');
|
|
expect(getResponseText(responses.find((message) => message.id === 977))).not.toContain('requestId: req-fulfill-close');
|
|
});
|
|
|
|
it('returns only the requested number of recent requests', async () => {
|
|
const responses = await runMcpRequests(
|
|
[
|
|
{
|
|
id: 'page-1',
|
|
title: 'Home',
|
|
currentUrl: 'https://example.com/',
|
|
intercept: {
|
|
pausedRequests: [
|
|
{ requestId: 'req-1', url: 'https://example.com/api/one', method: 'GET', resourceType: 'XHR' },
|
|
{ requestId: 'req-2', url: 'https://example.com/api/two', method: 'GET', resourceType: 'XHR' },
|
|
],
|
|
},
|
|
},
|
|
],
|
|
[
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 953,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'browser_add_intercept_rule',
|
|
arguments: { urlIncludes: '/api', method: 'GET', action: 'continue' },
|
|
},
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 954,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_list_requests', arguments: { limit: 1 } },
|
|
},
|
|
],
|
|
{
|
|
responseTimeoutMs: 12000,
|
|
}
|
|
);
|
|
|
|
const listText = getResponseText(responses.find((message) => message.id === 954));
|
|
expect(listText).toContain('requestId: req-2');
|
|
expect(listText).not.toContain('requestId: req-1');
|
|
});
|
|
|
|
it('fails browser_add_intercept_rule when Fetch.enable fails', async () => {
|
|
const responses = await runMcpRequests(
|
|
[
|
|
{
|
|
id: 'page-1',
|
|
title: 'Home',
|
|
currentUrl: 'https://example.com/',
|
|
intercept: {
|
|
enableError: 'Fetch.enable blocked',
|
|
},
|
|
},
|
|
],
|
|
[
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 955,
|
|
method: 'tools/call',
|
|
params: {
|
|
name: 'browser_add_intercept_rule',
|
|
arguments: { urlIncludes: '/api', action: 'continue' },
|
|
},
|
|
},
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: 956,
|
|
method: 'tools/call',
|
|
params: { name: 'browser_list_intercept_rules', arguments: {} },
|
|
},
|
|
],
|
|
{
|
|
responseTimeoutMs: 12000,
|
|
}
|
|
);
|
|
|
|
const addResponse = responses.find((message) => message.id === 955);
|
|
expect((addResponse?.result as { isError?: boolean }).isError).toBe(true);
|
|
expect(getResponseText(addResponse)).toContain('Browser MCP failed: Fetch.enable blocked');
|
|
|
|
const listText = getResponseText(responses.find((message) => message.id === 956));
|
|
expect(listText).toBe('status: empty');
|
|
});
|
|
|
|
});
|