mirror of
https://github.com/ChromeDevTools/chrome-devtools-mcp.git
synced 2026-11-04 15:00:11 +00:00
fix: handle beforeunload dialogs in navigations (#788)
Closes https://github.com/ChromeDevTools/chrome-devtools-mcp/issues/713
This commit is contained in:
1 parent
7ba63a8777
commit
9b21f8b2e9
4 files changed
+146
-56
No files matched your search
@@ -148,6 +148,7 @@
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- **handleBeforeUnload** (enum: "accept", "decline") _(optional)_: Whether to auto accept or beforeunload dialogs triggered by this navigation. Default is accept.
|
||||
- **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.
|
||||
|
||||
@@ -53,6 +53,7 @@ const nodeArgs = [
|
||||
'spec',
|
||||
'--test-force-exit',
|
||||
'--test',
|
||||
'--test-timeout=30000',
|
||||
...flags,
|
||||
...files,
|
||||
];
|
||||
|
||||
+82
-55
@@ -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';
|
||||
@@ -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 or beforeunload dialogs triggered by this navigation. Default is accept.',
|
||||
),
|
||||
...timeoutSchema,
|
||||
},
|
||||
handler: async (request, response, context) => {
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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', () => {
|
||||
@@ -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(
|
||||
|
||||
Reference in new issue
Block a user