mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-03 18:16:29 +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) {
|
async function getPageFrameIds(page) {
|
||||||
const targets = await fetchJson(`${getHttpUrl()}/json/list`);
|
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)
|
const browserTarget = Array.isArray(targets)
|
||||||
? targets.find((target) => target && typeof target === 'object' && target.type === 'browser')
|
? targets.find((target) => target && typeof target === 'object' && target.type === 'browser')
|
||||||
: null;
|
: null;
|
||||||
@@ -4227,6 +4257,16 @@ async function waitForBrowserDownloadEvent(timeoutMs, event) {
|
|||||||
if (message?.method !== 'Browser.downloadWillBegin') {
|
if (message?.method !== 'Browser.downloadWillBegin') {
|
||||||
return;
|
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 = {
|
const observed = {
|
||||||
url: message.params?.url || '',
|
url: message.params?.url || '',
|
||||||
suggestedFilename: message.params?.suggestedFilename || '',
|
suggestedFilename: message.params?.suggestedFilename || '',
|
||||||
@@ -4248,7 +4288,7 @@ async function waitForBrowserDownloadEvent(timeoutMs, event) {
|
|||||||
|
|
||||||
async function waitForMatchingEvent({ page, timeoutMs, event }) {
|
async function waitForMatchingEvent({ page, timeoutMs, event }) {
|
||||||
if (event.kind === 'download') {
|
if (event.kind === 'download') {
|
||||||
return await waitForBrowserDownloadEvent(timeoutMs, event);
|
return await waitForBrowserDownloadEvent(page, timeoutMs, event);
|
||||||
}
|
}
|
||||||
return await waitForPageEvent({ page, timeoutMs, event });
|
return await waitForPageEvent({ page, timeoutMs, event });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ type MockDownloadState = {
|
|||||||
guid?: string;
|
guid?: string;
|
||||||
url: string;
|
url: string;
|
||||||
suggestedFilename: string;
|
suggestedFilename: string;
|
||||||
|
frameId?: string;
|
||||||
progress?: MockDownloadProgressState[];
|
progress?: MockDownloadProgressState[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -253,12 +254,19 @@ type MockInterceptState = {
|
|||||||
pauseDispatchDelayMs?: number;
|
pauseDispatchDelayMs?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type MockFrameTree = {
|
||||||
|
frame: { id: string };
|
||||||
|
childFrames?: MockFrameTree[];
|
||||||
|
};
|
||||||
|
|
||||||
type MockPageState = {
|
type MockPageState = {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
currentUrl: string;
|
currentUrl: string;
|
||||||
fileInputs?: Record<string, MockFileInputPlan>;
|
fileInputs?: Record<string, MockFileInputPlan>;
|
||||||
browser?: MockBrowserState;
|
browser?: MockBrowserState;
|
||||||
|
frameTree?: MockFrameTree;
|
||||||
|
frameTreeSequence?: MockFrameTree[];
|
||||||
readyStateSequence?: string[];
|
readyStateSequence?: string[];
|
||||||
visibleText?: string;
|
visibleText?: string;
|
||||||
domSnapshot?: string;
|
domSnapshot?: string;
|
||||||
@@ -781,6 +789,7 @@ function createMockBrowser(pagesInput: MockPageState[]) {
|
|||||||
browserState.canceledDownloadGuids = browserState.canceledDownloadGuids || [];
|
browserState.canceledDownloadGuids = browserState.canceledDownloadGuids || [];
|
||||||
browserState.canceledDownloadGuids.push(String(message.params?.guid || ''));
|
browserState.canceledDownloadGuids.push(String(message.params?.guid || ''));
|
||||||
reply({});
|
reply({});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -792,7 +801,7 @@ function createMockBrowser(pagesInput: MockPageState[]) {
|
|||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
method: 'Browser.downloadWillBegin',
|
method: 'Browser.downloadWillBegin',
|
||||||
params: {
|
params: {
|
||||||
frameId: `frame-${page.id}`,
|
frameId: download.frameId || `frame-${page.id}`,
|
||||||
guid,
|
guid,
|
||||||
url: download.url,
|
url: download.url,
|
||||||
suggestedFilename: download.suggestedFilename,
|
suggestedFilename: download.suggestedFilename,
|
||||||
@@ -865,6 +874,12 @@ function createMockBrowser(pagesInput: MockPageState[]) {
|
|||||||
return;
|
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 (message.method === 'Page.captureScreenshot') {
|
||||||
if (!page.screenshot) {
|
if (!page.screenshot) {
|
||||||
reply({ data: '' });
|
reply({ data: '' });
|
||||||
@@ -8401,6 +8416,167 @@ describe('ccs-browser MCP server', () => {
|
|||||||
expect(text).not.toContain('embedded-checkout');
|
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 () => {
|
it('captures element screenshots using the post-scroll visible clip and reports failures', async () => {
|
||||||
const screenshotPlan: MockPageState['screenshot'] = {
|
const screenshotPlan: MockPageState['screenshot'] = {
|
||||||
data: 'ZWxlbWVudC1zaG90',
|
data: 'ZWxlbWVudC1zaG90',
|
||||||
|
|||||||
Reference in New Issue
Block a user