Skip to content

Commit 42fea42

Browse files
committed
chore: resolve dialog races
1 parent be8adb1 commit 42fea42

File tree

2 files changed

+67
-18
lines changed

2 files changed

+67
-18
lines changed

src/context.ts

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import { ManualPromise } from './manualPromise.js';
2222
import { Tab } from './tab.js';
2323
import { outputFile } from './config.js';
2424

25-
import type { ImageContent, TextContent } from '@modelcontextprotocol/sdk/types.js';
2625
import type { ModalState, Tool, ToolActionResult } from './tools/tool.js';
2726
import type { FullConfig } from './config.js';
2827
import type { BrowserContextFactory } from './browserContextFactory.js';
@@ -136,7 +135,6 @@ export class Context {
136135
// Tab management is done outside of the action() call.
137136
const toolResult = await tool.handle(this, tool.schema.inputSchema.parse(params || {}));
138137
const { code, action, waitForNetwork, captureSnapshot, resultOverride } = toolResult;
139-
const racingAction = action ? () => this._raceAgainstModalDialogs(action) : undefined;
140138

141139
if (resultOverride)
142140
return resultOverride;
@@ -152,16 +150,17 @@ export class Context {
152150

153151
const tab = this.currentTabOrDie();
154152
// TODO: race against modal dialogs to resolve clicks.
155-
let actionResult: { content?: (ImageContent | TextContent)[] } | undefined;
156-
try {
157-
if (waitForNetwork)
158-
actionResult = await waitForCompletion(this, tab, async () => racingAction?.()) ?? undefined;
159-
else
160-
actionResult = await racingAction?.() ?? undefined;
161-
} finally {
162-
if (captureSnapshot && !this._javaScriptBlocked())
163-
await tab.captureSnapshot();
164-
}
153+
const actionResult = await this._raceAgainstModalDialogs(async () => {
154+
try {
155+
if (waitForNetwork)
156+
return await waitForCompletion(this, tab, async () => action?.()) ?? undefined;
157+
else
158+
return await action?.() ?? undefined;
159+
} finally {
160+
if (captureSnapshot && !this._javaScriptBlocked())
161+
await tab.captureSnapshot();
162+
}
163+
});
165164

166165
const result: string[] = [];
167166
result.push(`- Ran Playwright code:

tests/dialogs.spec.ts

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,6 @@
1616

1717
import { test, expect } from './fixtures.js';
1818

19-
// https://github.com/microsoft/playwright/issues/35663
20-
test.skip(({ mcpBrowser, mcpHeadless }) => mcpBrowser === 'webkit' && mcpHeadless);
21-
2219
test('alert dialog', async ({ client, server }) => {
2320
server.setContent('/', `<button onclick="alert('Alert')">Button</button>`, 'text/html');
2421
expect(await client.callTool({
@@ -64,8 +61,6 @@ await page.getByRole('button', { name: 'Button' }).click();
6461
});
6562

6663
test('two alert dialogs', async ({ client, server }) => {
67-
test.fixme(true, 'Race between the dialog and ariaSnapshot');
68-
6964
server.setContent('/', `
7065
<title>Title</title>
7166
<body>
@@ -100,7 +95,18 @@ await page.getByRole('button', { name: 'Button' }).click();
10095
},
10196
});
10297

103-
expect(result).not.toContainTextContent('### Modal state');
98+
expect(result).toContainTextContent(`
99+
### Modal state
100+
- ["alert" dialog with message "Alert 2"]: can be handled by the "browser_handle_dialog" tool`);
101+
102+
const result2 = await client.callTool({
103+
name: 'browser_handle_dialog',
104+
arguments: {
105+
accept: true,
106+
},
107+
});
108+
109+
expect(result2).not.toContainTextContent('### Modal state');
104110
});
105111

106112
test('confirm dialog (true)', async ({ client, server }) => {
@@ -210,3 +216,47 @@ test('prompt dialog', async ({ client, server }) => {
210216
- generic [active] [ref=e1]: Answer
211217
\`\`\``);
212218
});
219+
220+
test('alert dialog w/ race', async ({ client, server }) => {
221+
server.setContent('/', `<button onclick="setTimeout(() => alert('Alert'), 100)">Button</button>`, 'text/html');
222+
expect(await client.callTool({
223+
name: 'browser_navigate',
224+
arguments: { url: server.PREFIX },
225+
})).toContainTextContent('- button "Button" [ref=e2]');
226+
227+
expect(await client.callTool({
228+
name: 'browser_click',
229+
arguments: {
230+
element: 'Button',
231+
ref: 'e2',
232+
},
233+
})).toHaveTextContent(`- Ran Playwright code:
234+
\`\`\`js
235+
// Click Button
236+
await page.getByRole('button', { name: 'Button' }).click();
237+
\`\`\`
238+
239+
### Modal state
240+
- ["alert" dialog with message "Alert"]: can be handled by the "browser_handle_dialog" tool`);
241+
242+
const result = await client.callTool({
243+
name: 'browser_handle_dialog',
244+
arguments: {
245+
accept: true,
246+
},
247+
});
248+
249+
expect(result).not.toContainTextContent('### Modal state');
250+
expect(result).toHaveTextContent(`- Ran Playwright code:
251+
\`\`\`js
252+
// <internal code to handle "alert" dialog>
253+
\`\`\`
254+
255+
- Page URL: ${server.PREFIX}
256+
- Page Title:
257+
- Page Snapshot
258+
\`\`\`yaml
259+
- button "Button" [active] [ref=e2]
260+
\`\`\`
261+
`);
262+
});

0 commit comments

Comments
 (0)