豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/tool-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@

**Parameters:**

- **handleBeforeUnload** (enum: "accept", "decline") _(optional)_: Whether to auto accept, decline or ignore potential before unload dialogs triggered by this navigation.
- **ignoreCache** (boolean) _(optional)_: Whether to ignore cache on reload.
- **timeout** (integer) _(optional)_: Maximum wait time in milliseconds. If set to 0, the default timeout will be used.
- **type** (enum: "url", "back", "forward", "reload") _(optional)_: Navigate the page by URL, back or forward in history, or reload.
Expand Down
1 change: 1 addition & 0 deletions scripts/test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const nodeArgs = [
'spec',
'--test-force-exit',
'--test',
'--test-timeout=30000',
...flags,
...files,
];
Expand Down
137 changes: 82 additions & 55 deletions src/tools/pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

import {logger} from '../logger.js';
import type {Dialog} from '../third_party/index.js';
import {zod} from '../third_party/index.js';

import {ToolCategory} from './categories.js';
Expand Down Expand Up @@ -120,6 +121,12 @@ export const navigatePage = defineTool({
.boolean()
.optional()
.describe('Whether to ignore cache on reload.'),
handleBeforeUnload: zod
.enum(['accept', 'decline'])
.optional()
.describe(
'Whether to auto accept, decline or ignore potential before unload dialogs triggered by this navigation.',
Comment thread
OrKoN marked this conversation as resolved.
Outdated
),
...timeoutSchema,
},
handler: async (request, response, context) => {
Expand All @@ -136,62 +143,82 @@ export const navigatePage = defineTool({
request.params.type = 'url';
}

await context.waitForEventsAfterAction(async () => {
switch (request.params.type) {
case 'url':
if (!request.params.url) {
throw new Error('A URL is required for navigation of type=url.');
}
try {
await page.goto(request.params.url, options);
response.appendResponseLine(
`Successfully navigated to ${request.params.url}.`,
);
} catch (error) {
response.appendResponseLine(
`Unable to navigate in the selected page: ${error.message}.`,
);
}
break;
case 'back':
try {
await page.goBack(options);
response.appendResponseLine(
`Successfully navigated back to ${page.url()}.`,
);
} catch (error) {
response.appendResponseLine(
`Unable to navigate back in the selected page: ${error.message}.`,
);
}
break;
case 'forward':
try {
await page.goForward(options);
response.appendResponseLine(
`Successfully navigated forward to ${page.url()}.`,
);
} catch (error) {
response.appendResponseLine(
`Unable to navigate forward in the selected page: ${error.message}.`,
);
}
break;
case 'reload':
try {
await page.reload({
...options,
ignoreCache: request.params.ignoreCache,
});
response.appendResponseLine(`Successfully reloaded the page.`);
} catch (error) {
response.appendResponseLine(
`Unable to reload the selected page: ${error.message}.`,
);
}
break;
const handleBeforeUnload = request.params.handleBeforeUnload ?? 'accept';
const dialogHandler = (dialog: Dialog) => {
if (dialog.type() === 'beforeunload') {
if (handleBeforeUnload === 'accept') {
response.appendResponseLine(`Accepted a beforeunload dialog.`);
void dialog.accept();
} else {
response.appendResponseLine(`Declined a beforeunload dialog.`);
void dialog.dismiss();
}
// We are not going to report the dialog like regular dialogs.
context.clearDialog();
}
});
};
page.on('dialog', dialogHandler);

try {
await context.waitForEventsAfterAction(async () => {
switch (request.params.type) {
case 'url':
if (!request.params.url) {
throw new Error('A URL is required for navigation of type=url.');
}
try {
await page.goto(request.params.url, options);
response.appendResponseLine(
`Successfully navigated to ${request.params.url}.`,
);
} catch (error) {
response.appendResponseLine(
`Unable to navigate in the selected page: ${error.message}.`,
);
}
break;
case 'back':
try {
await page.goBack(options);
response.appendResponseLine(
`Successfully navigated back to ${page.url()}.`,
);
} catch (error) {
response.appendResponseLine(
`Unable to navigate back in the selected page: ${error.message}.`,
);
}
break;
case 'forward':
try {
await page.goForward(options);
response.appendResponseLine(
`Successfully navigated forward to ${page.url()}.`,
);
} catch (error) {
response.appendResponseLine(
`Unable to navigate forward in the selected page: ${error.message}.`,
);
}
break;
case 'reload':
try {
await page.reload({
...options,
ignoreCache: request.params.ignoreCache,
});
response.appendResponseLine(`Successfully reloaded the page.`);
} catch (error) {
response.appendResponseLine(
`Unable to reload the selected page: ${error.message}.`,
);
}
break;
}
});
} finally {
page.off('dialog', dialogHandler);
}

response.setIncludePages(true);
},
Expand Down
63 changes: 62 additions & 1 deletion tests/tools/pages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
handleDialog,
getTabId,
} from '../../src/tools/pages.js';
import {withMcpContext} from '../utils.js';
import {html, withMcpContext} from '../utils.js';

describe('pages', () => {
describe('list_pages', () => {
Expand Down Expand Up @@ -184,6 +184,67 @@ describe('pages', () => {
assert.ok(response.includePages);
});
});

it('reload with accpeting the beforeunload dialog', async () => {
await withMcpContext(async (response, context) => {
const page = context.getSelectedPage();
await page.setContent(
html` <script>
window.addEventListener('beforeunload', e => {
e.preventDefault();
e.returnValue = '';
});
</script>`,
);

await navigatePage.handler(
{params: {type: 'reload'}},
response,
context,
);

assert.strictEqual(context.getDialog(), undefined);
assert.ok(response.includePages);
assert.strictEqual(
response.responseLines.join('\n'),
'Accepted a beforeunload dialog.\nSuccessfully reloaded the page.',
);
});
});

it('reload with declining the beforeunload dialog', async () => {
await withMcpContext(async (response, context) => {
const page = context.getSelectedPage();
await page.setContent(
html` <script>
window.addEventListener('beforeunload', e => {
e.preventDefault();
e.returnValue = '';
});
</script>`,
);

await navigatePage.handler(
{
params: {
type: 'reload',
handleBeforeUnload: 'decline',
timeout: 500,
},
},
response,
context,
);

assert.strictEqual(context.getDialog(), undefined);
assert.ok(response.includePages);
assert.strictEqual(
response.responseLines.join('\n'),
'Declined a beforeunload dialog.\nUnable to reload the selected page: Navigation timeout of 500 ms exceeded.',
);
});
});

it('go forward with error', async () => {
await withMcpContext(async (response, context) => {
await navigatePage.handler(
Expand Down