diff --git a/docs/browser-automation.md b/docs/browser-automation.md index 3b1b81e7..2d205300 100644 --- a/docs/browser-automation.md +++ b/docs/browser-automation.md @@ -63,10 +63,17 @@ Phase 6 capability details: - Phase 6 supports minimal request matching by `urlIncludes` and `method` - Phase 6A actions are limited to `continue` and `fail` - Phase 6B adds `fulfill` mock responses on top of the existing session-local interception model -- fulfill rules can return a custom status code, optional headers, and a UTF-8 response body +- fulfill rules can return a custom status code, optional response headers, and a UTF-8 response body - `browser_list_requests` returns recent request summaries, not full bodies - response bodies are applied only to the matched request and are not persisted beyond the current MCP session +Phase 7 capability details: + +- `browser_add_intercept_rule` also accepts `resourceType`, `urlPattern`, `urlRegex`, `headerMatchers`, and `priority` +- richer matching rules remain page-bound and session-local; creating a rule still binds it to the concrete page selected at creation time +- higher `priority` rules win before lower-priority rules, while equal-priority rules continue to follow creation order +- `headerMatchers` are request-matching conditions; `responseHeaders` on `fulfill` rules remain response headers + Minimal multi-tab workflow examples: ```json @@ -91,12 +98,15 @@ Minimal multi-tab workflow examples: { "name": "browser_add_intercept_rule", "arguments": { - "urlIncludes": "/api/mock/users", - "method": "GET", + "resourceType": "XHR", + "headerMatchers": [ + { "name": "x-env", "valueIncludes": "staging" } + ], + "priority": 10, "action": "fulfill", "statusCode": 200, "contentType": "application/json", - "body": "{\"users\":[1]}" + "body": "{\"ok\":true}" } } ``` @@ -113,7 +123,7 @@ Scoped selector notes: - `browser_click`, `browser_hover`, `browser_query`, `browser_wait_for`, and `browser_take_element_screenshot` accept optional `frameSelector` for same-origin iframes whose `contentDocument` is accessible - the same selector-based tools accept optional `pierceShadow: true` for open shadow-root traversal -- closed shadow roots, frame-index routing, richer request matching, and download acceptance controls are still out of scope +- closed shadow roots, frame-index routing, cross-page shared rules, request body matching, advanced boolean matcher groups, and download acceptance controls are still out of scope Example event wait: diff --git a/tests/unit/hooks/ccs-browser-mcp-server.test.ts b/tests/unit/hooks/ccs-browser-mcp-server.test.ts index 77a6f9d7..03fe0e44 100644 --- a/tests/unit/hooks/ccs-browser-mcp-server.test.ts +++ b/tests/unit/hooks/ccs-browser-mcp-server.test.ts @@ -1810,6 +1810,53 @@ describe('ccs-browser MCP server', () => { 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/' }], @@ -2269,6 +2316,75 @@ describe('ccs-browser MCP server', () => { 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[] = [ { @@ -2763,6 +2879,54 @@ describe('ccs-browser MCP server', () => { 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( [