From c28b480b51c2ef303797f5cbff87760f478f8a13 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 9 May 2025 15:35:28 -0700 Subject: [PATCH] feat(wait): allow waiting for given text (#390) Fixes https://github.com/microsoft/playwright-mcp/issues/389 --- README.md | 10 +++-- src/tools/common.ts | 37 ++++++++++++++--- tests/capabilities.spec.ts | 4 +- tests/wait.spec.ts | 85 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 12 deletions(-) create mode 100644 tests/wait.spec.ts diff --git a/README.md b/README.md index e4560b9..07a170b 100644 --- a/README.md +++ b/README.md @@ -518,11 +518,13 @@ X Y coordinate space, based on the provided screenshot. -- **browser_wait** - - Title: Wait - - Description: Wait for a specified time in seconds +- **browser_wait_for** + - Title: Wait for + - Description: Wait for text to appear or disappear or a specified time to pass - Parameters: - - `time` (number): The time to wait in seconds + - `time` (number, optional): The time to wait in seconds + - `text` (string, optional): The text to wait for + - `textGone` (string, optional): The text to wait for to disappear - Read-only: **true** diff --git a/src/tools/common.ts b/src/tools/common.ts index 40c7e3f..ca2dab0 100644 --- a/src/tools/common.ts +++ b/src/tools/common.ts @@ -21,19 +21,44 @@ const wait: ToolFactory = captureSnapshot => defineTool({ capability: 'wait', schema: { - name: 'browser_wait', - title: 'Wait', - description: 'Wait for a specified time in seconds', + name: 'browser_wait_for', + title: 'Wait for', + description: 'Wait for text to appear or disappear or a specified time to pass', inputSchema: z.object({ - time: z.number().describe('The time to wait in seconds'), + time: z.number().optional().describe('The time to wait in seconds'), + text: z.string().optional().describe('The text to wait for'), + textGone: z.string().optional().describe('The text to wait for to disappear'), }), type: 'readOnly', }, handle: async (context, params) => { - await new Promise(f => setTimeout(f, Math.min(10000, params.time * 1000))); + if (!params.text && !params.textGone && !params.time) + throw new Error('Either time, text or textGone must be provided'); + + const code: string[] = []; + + if (params.time) { + code.push(`await new Promise(f => setTimeout(f, ${params.time!} * 1000));`); + await new Promise(f => setTimeout(f, Math.min(10000, params.time! * 1000))); + } + + const tab = context.currentTabOrDie(); + const locator = params.text ? tab.page.getByText(params.text).first() : undefined; + const goneLocator = params.textGone ? tab.page.getByText(params.textGone).first() : undefined; + + if (goneLocator) { + code.push(`await page.getByText(${JSON.stringify(params.textGone)}).first().waitFor({ state: 'hidden' });`); + await goneLocator.waitFor({ state: 'hidden' }); + } + + if (locator) { + code.push(`await page.getByText(${JSON.stringify(params.text)}).first().waitFor({ state: 'visible' });`); + await locator.waitFor({ state: 'visible' }); + } + return { - code: [`// Waited for ${params.time} seconds`], + code, captureSnapshot, waitForNetwork: false, }; diff --git a/tests/capabilities.spec.ts b/tests/capabilities.spec.ts index 4187be2..6daf467 100644 --- a/tests/capabilities.spec.ts +++ b/tests/capabilities.spec.ts @@ -43,7 +43,7 @@ test('test snapshot tool list', async ({ client }) => { 'browser_tab_new', 'browser_tab_select', 'browser_take_screenshot', - 'browser_wait', + 'browser_wait_for', ])); }); @@ -72,7 +72,7 @@ test('test vision tool list', async ({ visionClient }) => { 'browser_tab_list', 'browser_tab_new', 'browser_tab_select', - 'browser_wait', + 'browser_wait_for', ])); }); diff --git a/tests/wait.spec.ts b/tests/wait.spec.ts new file mode 100644 index 0000000..f98e346 --- /dev/null +++ b/tests/wait.spec.ts @@ -0,0 +1,85 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './fixtures.js'; + +test('browser_wait_for(text)', async ({ client, server }) => { + server.setContent('/', ` + + + +
Text to disappear
+ + `, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); + + await client.callTool({ + name: 'browser_click', + arguments: { + element: 'Click me', + ref: 's1e3', + }, + }); + + expect(await client.callTool({ + name: 'browser_wait_for', + arguments: { text: 'Text to appear' }, + })).toContainTextContent(`- generic [ref=s3e4]: Text to appear`); +}); + +test('browser_wait_for(textGone)', async ({ client, server }) => { + server.setContent('/', ` + + + +
Text to disappear
+ + `, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); + + await client.callTool({ + name: 'browser_click', + arguments: { + element: 'Click me', + ref: 's1e3', + }, + }); + + expect(await client.callTool({ + name: 'browser_wait_for', + arguments: { textGone: 'Text to disappear' }, + })).toContainTextContent(`- generic [ref=s3e4]: Text to appear`); +});