mirror of
https://github.com/microsoft/playwright-mcp.git
synced 2025-07-26 08:32:26 +08:00
chore: resolve dialog races (#673)
Fixes https://github.com/microsoft/playwright-mcp/issues/595
This commit is contained in:
parent
da818d113a
commit
3061d9aa56
@ -22,7 +22,6 @@ import { ManualPromise } from './manualPromise.js';
|
|||||||
import { Tab } from './tab.js';
|
import { Tab } from './tab.js';
|
||||||
import { outputFile } from './config.js';
|
import { outputFile } from './config.js';
|
||||||
|
|
||||||
import type { ImageContent, TextContent } from '@modelcontextprotocol/sdk/types.js';
|
|
||||||
import type { ModalState, Tool, ToolActionResult } from './tools/tool.js';
|
import type { ModalState, Tool, ToolActionResult } from './tools/tool.js';
|
||||||
import type { FullConfig } from './config.js';
|
import type { FullConfig } from './config.js';
|
||||||
import type { BrowserContextFactory } from './browserContextFactory.js';
|
import type { BrowserContextFactory } from './browserContextFactory.js';
|
||||||
@ -136,7 +135,6 @@ export class Context {
|
|||||||
// Tab management is done outside of the action() call.
|
// Tab management is done outside of the action() call.
|
||||||
const toolResult = await tool.handle(this, tool.schema.inputSchema.parse(params || {}));
|
const toolResult = await tool.handle(this, tool.schema.inputSchema.parse(params || {}));
|
||||||
const { code, action, waitForNetwork, captureSnapshot, resultOverride } = toolResult;
|
const { code, action, waitForNetwork, captureSnapshot, resultOverride } = toolResult;
|
||||||
const racingAction = action ? () => this._raceAgainstModalDialogs(action) : undefined;
|
|
||||||
|
|
||||||
if (resultOverride)
|
if (resultOverride)
|
||||||
return resultOverride;
|
return resultOverride;
|
||||||
@ -152,16 +150,17 @@ export class Context {
|
|||||||
|
|
||||||
const tab = this.currentTabOrDie();
|
const tab = this.currentTabOrDie();
|
||||||
// TODO: race against modal dialogs to resolve clicks.
|
// TODO: race against modal dialogs to resolve clicks.
|
||||||
let actionResult: { content?: (ImageContent | TextContent)[] } | undefined;
|
const actionResult = await this._raceAgainstModalDialogs(async () => {
|
||||||
try {
|
try {
|
||||||
if (waitForNetwork)
|
if (waitForNetwork)
|
||||||
actionResult = await waitForCompletion(this, tab, async () => racingAction?.()) ?? undefined;
|
return await waitForCompletion(this, tab, async () => action?.()) ?? undefined;
|
||||||
else
|
else
|
||||||
actionResult = await racingAction?.() ?? undefined;
|
return await action?.() ?? undefined;
|
||||||
} finally {
|
} finally {
|
||||||
if (captureSnapshot && !this._javaScriptBlocked())
|
if (captureSnapshot && !this._javaScriptBlocked())
|
||||||
await tab.captureSnapshot();
|
await tab.captureSnapshot();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const result: string[] = [];
|
const result: string[] = [];
|
||||||
result.push(`- Ran Playwright code:
|
result.push(`- Ran Playwright code:
|
||||||
|
@ -16,9 +16,6 @@
|
|||||||
|
|
||||||
import { test, expect } from './fixtures.js';
|
import { test, expect } from './fixtures.js';
|
||||||
|
|
||||||
// https://github.com/microsoft/playwright/issues/35663
|
|
||||||
test.skip(({ mcpBrowser, mcpHeadless }) => mcpBrowser === 'webkit' && mcpHeadless);
|
|
||||||
|
|
||||||
test('alert dialog', async ({ client, server }) => {
|
test('alert dialog', async ({ client, server }) => {
|
||||||
server.setContent('/', `<button onclick="alert('Alert')">Button</button>`, 'text/html');
|
server.setContent('/', `<button onclick="alert('Alert')">Button</button>`, 'text/html');
|
||||||
expect(await client.callTool({
|
expect(await client.callTool({
|
||||||
@ -49,7 +46,7 @@ await page.getByRole('button', { name: 'Button' }).click();
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(result).not.toContainTextContent('### Modal state');
|
expect(result).not.toContainTextContent('### Modal state');
|
||||||
expect(result).toHaveTextContent(`- Ran Playwright code:
|
expect(result).toContainTextContent(`- Ran Playwright code:
|
||||||
\`\`\`js
|
\`\`\`js
|
||||||
// <internal code to handle "alert" dialog>
|
// <internal code to handle "alert" dialog>
|
||||||
\`\`\`
|
\`\`\`
|
||||||
@ -58,14 +55,10 @@ await page.getByRole('button', { name: 'Button' }).click();
|
|||||||
- Page Title:
|
- Page Title:
|
||||||
- Page Snapshot
|
- Page Snapshot
|
||||||
\`\`\`yaml
|
\`\`\`yaml
|
||||||
- button "Button" [active] [ref=e2]
|
- button "Button"`);
|
||||||
\`\`\`
|
|
||||||
`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('two alert dialogs', async ({ client, server }) => {
|
test('two alert dialogs', async ({ client, server }) => {
|
||||||
test.fixme(true, 'Race between the dialog and ariaSnapshot');
|
|
||||||
|
|
||||||
server.setContent('/', `
|
server.setContent('/', `
|
||||||
<title>Title</title>
|
<title>Title</title>
|
||||||
<body>
|
<body>
|
||||||
@ -100,7 +93,18 @@ await page.getByRole('button', { name: 'Button' }).click();
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).not.toContainTextContent('### Modal state');
|
expect(result).toContainTextContent(`
|
||||||
|
### Modal state
|
||||||
|
- ["alert" dialog with message "Alert 2"]: can be handled by the "browser_handle_dialog" tool`);
|
||||||
|
|
||||||
|
const result2 = await client.callTool({
|
||||||
|
name: 'browser_handle_dialog',
|
||||||
|
arguments: {
|
||||||
|
accept: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result2).not.toContainTextContent('### Modal state');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('confirm dialog (true)', async ({ client, server }) => {
|
test('confirm dialog (true)', async ({ client, server }) => {
|
||||||
@ -210,3 +214,45 @@ test('prompt dialog', async ({ client, server }) => {
|
|||||||
- generic [active] [ref=e1]: Answer
|
- generic [active] [ref=e1]: Answer
|
||||||
\`\`\``);
|
\`\`\``);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('alert dialog w/ race', async ({ client, server }) => {
|
||||||
|
server.setContent('/', `<button onclick="setTimeout(() => alert('Alert'), 100)">Button</button>`, 'text/html');
|
||||||
|
expect(await client.callTool({
|
||||||
|
name: 'browser_navigate',
|
||||||
|
arguments: { url: server.PREFIX },
|
||||||
|
})).toContainTextContent('- button "Button" [ref=e2]');
|
||||||
|
|
||||||
|
expect(await client.callTool({
|
||||||
|
name: 'browser_click',
|
||||||
|
arguments: {
|
||||||
|
element: 'Button',
|
||||||
|
ref: 'e2',
|
||||||
|
},
|
||||||
|
})).toHaveTextContent(`- Ran Playwright code:
|
||||||
|
\`\`\`js
|
||||||
|
// Click Button
|
||||||
|
await page.getByRole('button', { name: 'Button' }).click();
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Modal state
|
||||||
|
- ["alert" dialog with message "Alert"]: can be handled by the "browser_handle_dialog" tool`);
|
||||||
|
|
||||||
|
const result = await client.callTool({
|
||||||
|
name: 'browser_handle_dialog',
|
||||||
|
arguments: {
|
||||||
|
accept: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).not.toContainTextContent('### Modal state');
|
||||||
|
expect(result).toContainTextContent(`- Ran Playwright code:
|
||||||
|
\`\`\`js
|
||||||
|
// <internal code to handle "alert" dialog>
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
- Page URL: ${server.PREFIX}
|
||||||
|
- Page Title:
|
||||||
|
- Page Snapshot
|
||||||
|
\`\`\`yaml
|
||||||
|
- button "Button"`);
|
||||||
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user