mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 04:18:05 +00:00
fix(browser): 修复 download 事件的 pageIndex 过滤
为 browser_wait_for_event 的 download 路径补齐按页面 frame tree 过滤,避免多页面或动态 iframe 下载时误命中或漏命中。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
f6ef59ef96
commit
40c718c584
@@ -4174,8 +4174,38 @@ async function waitForPageEvent({ page, timeoutMs, event }) {
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForBrowserDownloadEvent(timeoutMs, event) {
|
||||
const targets = await fetchJson(`${getHttpUrl()}/json/list`);
|
||||
async function getPageFrameIds(page) {
|
||||
const response = await sendCdpCommand(page, 'Page.getFrameTree', {});
|
||||
const frameTree = response?.frameTree;
|
||||
const frameIds = new Set();
|
||||
|
||||
function visitFrameTree(node) {
|
||||
if (!node || typeof node !== 'object') {
|
||||
return;
|
||||
}
|
||||
const frameId = node.frame?.id;
|
||||
if (typeof frameId === 'string' && frameId) {
|
||||
frameIds.add(frameId);
|
||||
}
|
||||
const childFrames = Array.isArray(node.childFrames) ? node.childFrames : [];
|
||||
for (const childFrame of childFrames) {
|
||||
visitFrameTree(childFrame);
|
||||
}
|
||||
}
|
||||
|
||||
visitFrameTree(frameTree);
|
||||
|
||||
if (frameIds.size === 0) {
|
||||
throw new Error('Browser MCP could not determine the selected page frame IDs.');
|
||||
}
|
||||
return frameIds;
|
||||
}
|
||||
|
||||
async function waitForBrowserDownloadEvent(page, timeoutMs, event) {
|
||||
const [targets, frameIds] = await Promise.all([
|
||||
fetchJson(`${getHttpUrl()}/json/list`),
|
||||
getPageFrameIds(page),
|
||||
]);
|
||||
const browserTarget = Array.isArray(targets)
|
||||
? targets.find((target) => target && typeof target === 'object' && target.type === 'browser')
|
||||
: null;
|
||||
@@ -4227,6 +4257,16 @@ async function waitForBrowserDownloadEvent(timeoutMs, event) {
|
||||
if (message?.method !== 'Browser.downloadWillBegin') {
|
||||
return;
|
||||
}
|
||||
const eventFrameId = message.params?.frameId;
|
||||
if (!frameIds.has(eventFrameId)) {
|
||||
const refreshedFrameIds = await getPageFrameIds(page);
|
||||
if (!refreshedFrameIds.has(eventFrameId)) {
|
||||
return;
|
||||
}
|
||||
for (const frameId of refreshedFrameIds) {
|
||||
frameIds.add(frameId);
|
||||
}
|
||||
}
|
||||
const observed = {
|
||||
url: message.params?.url || '',
|
||||
suggestedFilename: message.params?.suggestedFilename || '',
|
||||
@@ -4248,7 +4288,7 @@ async function waitForBrowserDownloadEvent(timeoutMs, event) {
|
||||
|
||||
async function waitForMatchingEvent({ page, timeoutMs, event }) {
|
||||
if (event.kind === 'download') {
|
||||
return await waitForBrowserDownloadEvent(timeoutMs, event);
|
||||
return await waitForBrowserDownloadEvent(page, timeoutMs, event);
|
||||
}
|
||||
return await waitForPageEvent({ page, timeoutMs, event });
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ type MockDownloadState = {
|
||||
guid?: string;
|
||||
url: string;
|
||||
suggestedFilename: string;
|
||||
frameId?: string;
|
||||
progress?: MockDownloadProgressState[];
|
||||
};
|
||||
|
||||
@@ -253,12 +254,19 @@ type MockInterceptState = {
|
||||
pauseDispatchDelayMs?: number;
|
||||
};
|
||||
|
||||
type MockFrameTree = {
|
||||
frame: { id: string };
|
||||
childFrames?: MockFrameTree[];
|
||||
};
|
||||
|
||||
type MockPageState = {
|
||||
id: string;
|
||||
title: string;
|
||||
currentUrl: string;
|
||||
fileInputs?: Record<string, MockFileInputPlan>;
|
||||
browser?: MockBrowserState;
|
||||
frameTree?: MockFrameTree;
|
||||
frameTreeSequence?: MockFrameTree[];
|
||||
readyStateSequence?: string[];
|
||||
visibleText?: string;
|
||||
domSnapshot?: string;
|
||||
@@ -781,6 +789,7 @@ function createMockBrowser(pagesInput: MockPageState[]) {
|
||||
browserState.canceledDownloadGuids = browserState.canceledDownloadGuids || [];
|
||||
browserState.canceledDownloadGuids.push(String(message.params?.guid || ''));
|
||||
reply({});
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -792,7 +801,7 @@ function createMockBrowser(pagesInput: MockPageState[]) {
|
||||
JSON.stringify({
|
||||
method: 'Browser.downloadWillBegin',
|
||||
params: {
|
||||
frameId: `frame-${page.id}`,
|
||||
frameId: download.frameId || `frame-${page.id}`,
|
||||
guid,
|
||||
url: download.url,
|
||||
suggestedFilename: download.suggestedFilename,
|
||||
@@ -865,6 +874,12 @@ function createMockBrowser(pagesInput: MockPageState[]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.method === 'Page.getFrameTree') {
|
||||
const nextFrameTree = page.frameTreeSequence?.shift();
|
||||
reply({ frameTree: nextFrameTree || page.frameTree || { frame: { id: `frame-${page.id}` } } });
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.method === 'Page.captureScreenshot') {
|
||||
if (!page.screenshot) {
|
||||
reply({ data: '' });
|
||||
@@ -8401,6 +8416,167 @@ describe('ccs-browser MCP server', () => {
|
||||
expect(text).not.toContain('embedded-checkout');
|
||||
});
|
||||
|
||||
it('filters download events by the selected page frame when pageIndex is provided', async () => {
|
||||
const responses = await runMcpRequests(
|
||||
[
|
||||
{
|
||||
id: 'page-1',
|
||||
title: 'First Page',
|
||||
currentUrl: 'https://example.com/one',
|
||||
events: {
|
||||
downloads: [
|
||||
{
|
||||
url: 'https://example.com/first-report.csv',
|
||||
suggestedFilename: 'first-report.csv',
|
||||
frameId: 'frame-page-1',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'page-2',
|
||||
title: 'Second Page',
|
||||
currentUrl: 'https://example.com/two',
|
||||
events: {
|
||||
downloads: [
|
||||
{
|
||||
url: 'https://example.com/second-report.csv',
|
||||
suggestedFilename: 'second-report.csv',
|
||||
frameId: 'frame-page-2',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
id: 59,
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'browser_wait_for_event',
|
||||
arguments: {
|
||||
pageIndex: 1,
|
||||
timeoutMs: 1000,
|
||||
event: { kind: 'download', suggestedFilenameIncludes: 'second-report' },
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
const text = getResponseText(responses.find((message) => message.id === 59));
|
||||
expect(text).toContain('status: observed');
|
||||
expect(text).toContain('"suggestedFilename":"second-report.csv"');
|
||||
expect(text).not.toContain('first-report.csv');
|
||||
});
|
||||
|
||||
it('matches download events from child frames within the selected page', async () => {
|
||||
const responses = await runMcpRequests(
|
||||
[
|
||||
{
|
||||
id: 'page-1',
|
||||
title: 'Framed Page',
|
||||
currentUrl: 'https://example.com/frame-host',
|
||||
frameTree: {
|
||||
frame: { id: 'frame-page-1' },
|
||||
childFrames: [{ frame: { id: 'frame-page-1-child' } }],
|
||||
},
|
||||
events: {
|
||||
downloads: [
|
||||
{
|
||||
url: 'https://example.com/embedded-report.csv',
|
||||
suggestedFilename: 'embedded-report.csv',
|
||||
frameId: 'frame-page-1-child',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'page-2',
|
||||
title: 'Other Page',
|
||||
currentUrl: 'https://example.com/other',
|
||||
events: {
|
||||
downloads: [
|
||||
{
|
||||
url: 'https://example.com/other-report.csv',
|
||||
suggestedFilename: 'other-report.csv',
|
||||
frameId: 'frame-page-2',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
id: 60,
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'browser_wait_for_event',
|
||||
arguments: {
|
||||
pageIndex: 0,
|
||||
timeoutMs: 1000,
|
||||
event: { kind: 'download', suggestedFilenameIncludes: 'embedded-report' },
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
const text = getResponseText(responses.find((message) => message.id === 60));
|
||||
expect(text).toContain('status: observed');
|
||||
expect(text).toContain('"suggestedFilename":"embedded-report.csv"');
|
||||
expect(text).not.toContain('other-report.csv');
|
||||
});
|
||||
|
||||
it('refreshes frame ids when a download comes from a newly attached child frame', async () => {
|
||||
const responses = await runMcpRequests(
|
||||
[
|
||||
{
|
||||
id: 'page-1',
|
||||
title: 'Dynamic Frame Page',
|
||||
currentUrl: 'https://example.com/dynamic',
|
||||
frameTreeSequence: [
|
||||
{ frame: { id: 'frame-page-1' } },
|
||||
{
|
||||
frame: { id: 'frame-page-1' },
|
||||
childFrames: [{ frame: { id: 'frame-page-1-late-child' } }],
|
||||
},
|
||||
],
|
||||
events: {
|
||||
downloads: [
|
||||
{
|
||||
url: 'https://example.com/late-report.csv',
|
||||
suggestedFilename: 'late-report.csv',
|
||||
frameId: 'frame-page-1-late-child',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
id: 61,
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'browser_wait_for_event',
|
||||
arguments: {
|
||||
pageIndex: 0,
|
||||
timeoutMs: 1000,
|
||||
event: { kind: 'download', suggestedFilenameIncludes: 'late-report' },
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
const text = getResponseText(responses.find((message) => message.id === 61));
|
||||
expect(text).toContain('status: observed');
|
||||
expect(text).toContain('"suggestedFilename":"late-report.csv"');
|
||||
});
|
||||
|
||||
it('captures element screenshots using the post-scroll visible clip and reports failures', async () => {
|
||||
const screenshotPlan: MockPageState['screenshot'] = {
|
||||
data: 'ZWxlbWVudC1zaG90',
|
||||
|
||||
Reference in New Issue
Block a user