Skip to content

Commit 88445d3

Browse files
committed
fix: add proactive tool rejection when dialog is open
1 parent f1afe84 commit 88445d3

14 files changed

Lines changed: 194 additions & 0 deletions

src/McpPage.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,14 @@ export class McpPage implements ContextPage {
8181
this.#dialog = undefined;
8282
}
8383

84+
throwIfDialogOpen(): void {
85+
if (this.#dialog) {
86+
throw new Error(
87+
`A dialog is open (${this.#dialog.type()}: ${this.#dialog.message()}).`,
88+
);
89+
}
90+
}
91+
8492
getInPageTools(): ToolGroup<ToolDefinition> | undefined {
8593
return this.inPageTools;
8694
}

src/tools/ToolDefinition.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ export type ContextPage = Readonly<{
255255

256256
getDialog(): Dialog | undefined;
257257
clearDialog(): void;
258+
throwIfDialogOpen(): void;
258259
waitForEventsAfterAction(
259260
action: () => Promise<unknown>,
260261
options?: {timeout?: number; handleDialog?: 'accept' | 'dismiss' | string},

src/tools/emulation.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export const emulate = definePageTool({
6868
},
6969
handler: async (request, _response, context) => {
7070
const page = request.page;
71+
page.throwIfDialogOpen();
7172
await context.emulate(request.params, page.pptrPage);
7273
},
7374
});

src/tools/memory.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export const takeMemorySnapshot = definePageTool({
2525
handler: async (request, response, context) => {
2626
const page = request.page;
2727
context.validatePath(request.params.filePath);
28+
page.throwIfDialogOpen();
2829

2930
await page.pptrPage.captureHeapSnapshot({
3031
path: ensureExtension(request.params.filePath, '.heapsnapshot'),

src/tools/screenshot.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export const screenshot = definePageTool({
5252
},
5353
handler: async (request, response, context) => {
5454
context.validatePath(request.params.filePath);
55+
request.page.throwIfDialogOpen();
5556
if (request.params.uid && request.params.fullPage) {
5657
throw new Error('Providing both "uid" and "fullPage" is not allowed.');
5758
}

src/tools/snapshot.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ in the DevTools Elements panel (if any).`,
3535
},
3636
handler: async (request, response, context) => {
3737
context.validatePath(request.params.filePath);
38+
request.page.throwIfDialogOpen();
3839
response.includeSnapshot({
3940
verbose: request.params.verbose ?? false,
4041
filePath: request.params.filePath,
@@ -60,6 +61,7 @@ export const waitFor = definePageTool({
6061
},
6162
handler: async (request, response, context) => {
6263
const page = request.page;
64+
page.throwIfDialogOpen();
6365
await context.waitForTextOnPage(
6466
request.params.text,
6567
request.params.timeout,
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
exports[`emulation > when dialog is open 1`] = `
2+
{"content":[{"type":"text","text":"# Open dialog\\nalert: test dialog.\\nCall handle_dialog to handle it before continuing."}],"structuredContent":{"dialog":{"type":"alert","message":"test dialog","defaultValue":""}}}
3+
`;

tests/tools/emulation.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import assert from 'node:assert';
88
import {beforeEach, describe, it} from 'node:test';
99

10+
import type {Dialog} from 'puppeteer-core';
11+
1012
import {emulate} from '../../src/tools/emulation.js';
1113
import {
1214
geolocationTransform,
@@ -680,4 +682,35 @@ describe('emulation', () => {
680682
});
681683
});
682684
});
685+
686+
it('when dialog is open', async t => {
687+
await withMcpContext(async (response, context) => {
688+
const page = context.getSelectedPptrPage();
689+
await page.setContent('<h1>Test</h1>');
690+
const dialogPromise = new Promise<Dialog>(resolve => {
691+
page.on('dialog', dialog => resolve(dialog));
692+
});
693+
page.evaluate(() => {
694+
alert('test dialog');
695+
});
696+
const dialog = await dialogPromise;
697+
698+
await assert.rejects(
699+
emulate.handler(
700+
{
701+
params: {
702+
userAgent: 'Test Agent',
703+
},
704+
page: context.getSelectedMcpPage(),
705+
},
706+
response,
707+
context,
708+
),
709+
);
710+
const result = await response.handle('emulate', context);
711+
712+
t.assert.snapshot?.(JSON.stringify(result));
713+
await dialog.dismiss();
714+
});
715+
});
683716
});

tests/tools/memory.test.js.snapshot

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,3 +198,7 @@ Static Data: {
198198
"maxJSObjectId": 54005
199199
}
200200
`;
201+
202+
exports[`memory > take_memory_snapshot > when dialog is open 1`] = `
203+
{"content":[{"type":"text","text":"# Open dialog\\nalert: test dialog.\\nCall handle_dialog to handle it before continuing."}],"structuredContent":{"dialog":{"type":"alert","message":"test dialog","defaultValue":""}}}
204+
`;

tests/tools/memory.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import {tmpdir} from 'node:os';
1111
import {join} from 'node:path';
1212
import {describe, it} from 'node:test';
1313

14+
import type {Dialog} from 'puppeteer-core';
15+
1416
import {
1517
takeMemorySnapshot,
1618
exploreMemorySnapshot,
@@ -40,6 +42,34 @@ describe('memory', () => {
4042
}
4143
});
4244
});
45+
46+
it('when dialog is open', async t => {
47+
await withMcpContext(async (response, context) => {
48+
const page = context.getSelectedPptrPage();
49+
await page.setContent('<h1>Test</h1>');
50+
const dialogPromise = new Promise<Dialog>(resolve => {
51+
page.on('dialog', dialog => resolve(dialog));
52+
});
53+
page.evaluate(() => {
54+
alert('test dialog');
55+
});
56+
const dialog = await dialogPromise;
57+
const filePath = join(tmpdir(), 'test-dialog.heapsnapshot');
58+
59+
await assert.rejects(
60+
takeMemorySnapshot.handler(
61+
{params: {filePath}, page: context.getSelectedMcpPage()},
62+
response,
63+
context,
64+
),
65+
);
66+
await rm(filePath, {force: true});
67+
const result = await response.handle('take_memory_snapshot', context);
68+
69+
t.assert.snapshot?.(JSON.stringify(result));
70+
await dialog.dismiss();
71+
});
72+
});
4373
});
4474

4575
describe('load_memory_snapshot', () => {

0 commit comments

Comments
 (0)