Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
23 changes: 11 additions & 12 deletions src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import { ManualPromise } from './manualPromise.js';
import { Tab } from './tab.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 { FullConfig } from './config.js';
import type { BrowserContextFactory } from './browserContextFactory.js';
Expand Down Expand Up @@ -136,7 +135,6 @@ export class Context {
// Tab management is done outside of the action() call.
const toolResult = await tool.handle(this, tool.schema.inputSchema.parse(params || {}));
const { code, action, waitForNetwork, captureSnapshot, resultOverride } = toolResult;
const racingAction = action ? () => this._raceAgainstModalDialogs(action) : undefined;

if (resultOverride)
return resultOverride;
Expand All @@ -152,16 +150,17 @@ export class Context {

const tab = this.currentTabOrDie();
// TODO: race against modal dialogs to resolve clicks.
let actionResult: { content?: (ImageContent | TextContent)[] } | undefined;
try {
if (waitForNetwork)
actionResult = await waitForCompletion(this, tab, async () => racingAction?.()) ?? undefined;
else
actionResult = await racingAction?.() ?? undefined;
} finally {
if (captureSnapshot && !this._javaScriptBlocked())
await tab.captureSnapshot();
}
const actionResult = await this._raceAgainstModalDialogs(async () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks the same as #598, did something change?

try {
if (waitForNetwork)
return await waitForCompletion(this, tab, async () => action?.()) ?? undefined;
else
return await action?.() ?? undefined;
} finally {
if (captureSnapshot && !this._javaScriptBlocked())
await tab.captureSnapshot();
}
});

const result: string[] = [];
result.push(`- Ran Playwright code:
Expand Down
66 changes: 56 additions & 10 deletions tests/dialogs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@

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 }) => {
server.setContent('/', `<button onclick="alert('Alert')">Button</button>`, 'text/html');
expect(await client.callTool({
Expand Down Expand Up @@ -49,7 +46,7 @@ await page.getByRole('button', { name: 'Button' }).click();
});

expect(result).not.toContainTextContent('### Modal state');
expect(result).toHaveTextContent(`- Ran Playwright code:
expect(result).toContainTextContent(`- Ran Playwright code:
\`\`\`js
// <internal code to handle "alert" dialog>
\`\`\`
Expand All @@ -58,14 +55,10 @@ await page.getByRole('button', { name: 'Button' }).click();
- Page Title:
- Page Snapshot
\`\`\`yaml
- button "Button" [active] [ref=e2]
\`\`\`
`);
- button "Button"`);
});

test('two alert dialogs', async ({ client, server }) => {
test.fixme(true, 'Race between the dialog and ariaSnapshot');

server.setContent('/', `
<title>Title</title>
<body>
Expand Down Expand Up @@ -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 }) => {
Expand Down Expand Up @@ -210,3 +214,45 @@ test('prompt dialog', async ({ client, server }) => {
- 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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unclear why this test would work. Click will return immediately, snapshot will be empty. Then the dialog will appear.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/s

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are asking the question that you yourself answered :P

\`\`\`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