From f9a6516518686561864ded1ccd83d970a9b8d9fe Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Mon, 23 Feb 2026 14:38:35 -0800 Subject: [PATCH] fix(mcp): make verify tools work in iframes --- .../playwright-core/src/client/locator.ts | 5 ++ .../src/mcp/browser/tools/verify.ts | 32 ++++++------ tests/mcp/verify.spec.ts | 49 +++++++++++++++++++ 3 files changed, 72 insertions(+), 14 deletions(-) diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts index 86650c00347ed..169ba66d755b0 100644 --- a/packages/playwright-core/src/client/locator.ts +++ b/packages/playwright-core/src/client/locator.ts @@ -262,6 +262,11 @@ export class Locator implements api.Locator { return await this._frame._channel.resolveSelector({ selector: this._selector }); } + async _resolveForCode(): Promise { + const { resolvedSelector } = await this._resolveSelector(); + return asLocatorDescription('javascript', resolvedSelector); + } + async getAttribute(name: string, options?: TimeoutOptions): Promise { return await this._frame.getAttribute(this._selector, name, { strict: true, ...options }); } diff --git a/packages/playwright/src/mcp/browser/tools/verify.ts b/packages/playwright/src/mcp/browser/tools/verify.ts index 381aecb864b4a..436b561d5025b 100644 --- a/packages/playwright/src/mcp/browser/tools/verify.ts +++ b/packages/playwright/src/mcp/browser/tools/verify.ts @@ -33,14 +33,16 @@ const verifyElement = defineTabTool({ }, handle: async (tab, params, response) => { - const locator = tab.page.getByRole(params.role as any, { name: params.accessibleName }); - if (await locator.count() === 0) { - response.addError(`Element with role "${params.role}" and accessible name "${params.accessibleName}" not found`); - return; + for (const frame of tab.page.frames()) { + const locator = frame.getByRole(params.role as any, { name: params.accessibleName }); + if (await locator.count() > 0) { + const resolved = await locator._resolveForCode(); + response.addCode(`await expect(page.${resolved}).toBeVisible();`); + response.addTextResult('Done'); + return; + } } - - response.addCode(`await expect(page.getByRole(${escapeWithQuotes(params.role)}, { name: ${escapeWithQuotes(params.accessibleName)} })).toBeVisible();`); - response.addTextResult('Done'); + response.addError(`Element with role "${params.role}" and accessible name "${params.accessibleName}" not found`); }, }); @@ -57,14 +59,16 @@ const verifyText = defineTabTool({ }, handle: async (tab, params, response) => { - const locator = tab.page.getByText(params.text).filter({ visible: true }); - if (await locator.count() === 0) { - response.addError('Text not found'); - return; + for (const frame of tab.page.frames()) { + const locator = frame.getByText(params.text).filter({ visible: true }); + if (await locator.count() > 0) { + const resolved = await locator._resolveForCode(); + response.addCode(`await expect(page.${resolved}).toBeVisible();`); + response.addTextResult('Done'); + return; + } } - - response.addCode(`await expect(page.getByText(${escapeWithQuotes(params.text)})).toBeVisible();`); - response.addTextResult('Done'); + response.addError('Text not found'); }, }); diff --git a/tests/mcp/verify.spec.ts b/tests/mcp/verify.spec.ts index e4b8da64bdde0..ceb2b0e717f86 100644 --- a/tests/mcp/verify.spec.ts +++ b/tests/mcp/verify.spec.ts @@ -88,6 +88,55 @@ test('browser_verify_element_visible (not found)', async ({ client, server }) => }); }); +test('browser_verify_element_visible (iframe)', async ({ client, server }) => { + server.setContent('/', ` + Test Page +

Outer frame text

+ + `, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); + + expect(await client.callTool({ + name: 'browser_verify_element_visible', + arguments: { + role: 'heading', + accessibleName: 'Inner iframe', + }, + })).toHaveResponse({ + result: 'Done', + code: `await expect(page.locator('iframe').contentFrame().getByRole('heading', { name: 'Inner iframe' })).toBeVisible();`, + }); +}); + +test('browser_verify_text_visible (iframe)', async ({ client, server }) => { + server.setContent('/', ` + Test Page +

Outer frame text

+ + `, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); + + expect(await client.callTool({ + name: 'browser_verify_text_visible', + arguments: { + text: 'Inner iframe', + }, + })).toHaveResponse({ + result: 'Done', + code: `await expect(page.locator('iframe').contentFrame().getByRole('heading', { name: 'Inner iframe' })).toBeVisible();`, + }); +}); + test('browser_verify_text_visible', async ({ client, server }) => { server.setContent('/', ` Test Page