From bde6b0493d2c36b4bdd61cac14a38d1dd8a7182c Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Mon, 20 Apr 2026 13:45:17 +0100 Subject: [PATCH] feat(cli): add generate-locator command --- .../playwright-cli-stub.js | 2 +- .../src/tools/cli-client/skill/SKILL.md | 3 ++ .../skill/references/test-generation.md | 52 +++++++++++++++++-- .../src/tools/cli-daemon/commands.ts | 12 +++++ tests/mcp/cli-devtools.spec.ts | 9 ++++ 5 files changed, 74 insertions(+), 4 deletions(-) diff --git a/packages/playwright-cli-stub/playwright-cli-stub.js b/packages/playwright-cli-stub/playwright-cli-stub.js index d33211f9bef8e..59772a7ce80f8 100755 --- a/packages/playwright-cli-stub/playwright-cli-stub.js +++ b/packages/playwright-cli-stub/playwright-cli-stub.js @@ -15,7 +15,7 @@ * limitations under the License. */ -const { program } = require('playwright-core/lib/coreBundle').cli; +const { program } = require('playwright-core/lib/tools/cli-client/program'); program().catch(e => { console.error(e.message); diff --git a/packages/playwright-core/src/tools/cli-client/skill/SKILL.md b/packages/playwright-core/src/tools/cli-client/skill/SKILL.md index 8a22932e73d1a..bd4fdc308a55a 100644 --- a/packages/playwright-core/src/tools/cli-client/skill/SKILL.md +++ b/packages/playwright-core/src/tools/cli-client/skill/SKILL.md @@ -162,6 +162,9 @@ playwright-cli video-stop # wait for the user to pick an element in the browser, print its ref and locator playwright-cli pick +# generate a Playwright locator for an element from its ref or selector +playwright-cli generate-locator e5 --raw + # show a persistent highlight overlay for an element, optionally with a custom style playwright-cli highlight e5 playwright-cli highlight e5 --style="outline: 3px dashed red" diff --git a/packages/playwright-core/src/tools/cli-client/skill/references/test-generation.md b/packages/playwright-core/src/tools/cli-client/skill/references/test-generation.md index 7a09df387f241..a045c55d6e673 100644 --- a/packages/playwright-core/src/tools/cli-client/skill/references/test-generation.md +++ b/packages/playwright-core/src/tools/cli-client/skill/references/test-generation.md @@ -77,12 +77,58 @@ playwright-cli click e5 ### 3. Add Assertions Manually -Generated code captures actions but not assertions. Add expectations in your test: +Generated code captures actions but not assertions. Add expectations in your test using one of the recommended matchers: + +- `toBeVisible()` — element is rendered and visible +- `toHaveText(text)` — element text content matches +- `toHaveValue(value) / toBeEmpty()` — input/select value matches +- `toBeChecked() / toBeUnchecked()` — checkbox state matches +- `toMatchAriaSnapshot(snapshot)` — page (or locator) matches a partial accessibility snapshot + +Use `playwright-cli generate-locator ` to produce the locator expression for the assertion, and the snapshot/eval commands to capture the expected value. + +When asserting text content, make sure that generated locator does not contain text from the element itself. `getByTestId()` or `getByLabel()` usually work well with asserting text. When locator is text-based, prefer `toBeVisible()` instead. + +Snapshot to be matched does not have to contain all the information - only capture what's necessary for the assertion. You can use regular expressions for unstable values. + +```bash +# Get a stable locator for an element ref to use in the assertion +playwright-cli --raw generate-locator e5 +# getByRole('button', { name: 'Submit' }) + +# Capture expected text content for toHaveText +playwright-cli --raw eval "el => el.textContent" e5 + +# Capture expected input value for toHaveValue/toBeEmpty +playwright-cli --raw eval "el => el.value" e5 + +# Capture expected aria snapshot for toMatchAriaSnapshot/toBeChecked +# (whole page, or use a ref to scope to a region) +playwright-cli --raw snapshot +playwright-cli --raw snapshot e5 +``` ```typescript // Generated action await page.getByRole('button', { name: 'Submit' }).click(); -// Manual assertion -await expect(page.getByText('Success')).toBeVisible(); +// Manual assertions using the outputs above: +await expect(page.getByRole('alert', { name: 'Success' })).toBeVisible(); +await expect(page.getByTestId('main-header')).toHaveText('Welcome, user'); +await expect(page.getByRole('textbox', { name: 'Email' })).toHaveValue('user@example.com'); +await expect(page.getByRole('checkbox', { name: 'Enable notifications' })).toBeChecked(); + +// toMatchAriaSnapshot on the whole page, finds a matching region +await expect(page).toMatchAriaSnapshot(` + - heading "Welcome, user" + - link /\\d+ new messages?/ + - button "Sign out" +`); + +// toMatchAriaSnapshot scoped to a region +await expect(page.getByRole('navigation')).toMatchAriaSnapshot(` + - link "Home" + - link /\\d+ new messages?/ + - link "Profile" +`); ``` diff --git a/packages/playwright-core/src/tools/cli-daemon/commands.ts b/packages/playwright-core/src/tools/cli-daemon/commands.ts index 3863c30dd60fa..5c1ae4b35e4d3 100644 --- a/packages/playwright-core/src/tools/cli-daemon/commands.ts +++ b/packages/playwright-core/src/tools/cli-daemon/commands.ts @@ -354,6 +354,17 @@ const pick = declareCommand({ toolParams: () => ({}), }); +const generateLocator = declareCommand({ + name: 'generate-locator', + description: 'Generate a Playwright locator for the given element', + category: 'devtools', + args: z.object({ + target: z.string().describe(elementTargetDescription), + }), + toolName: 'browser_generate_locator', + toolParams: ({ target }) => ({ target }), +}); + const highlight = declareCommand({ name: 'highlight', description: 'Show (or with --hide, remove) a highlight overlay for an element; `--hide` without a target hides all page highlights.', @@ -1074,6 +1085,7 @@ const commandsArray: AnyCommandSchema[] = [ resume, stepOver, pick, + generateLocator, highlight, // session category diff --git a/tests/mcp/cli-devtools.spec.ts b/tests/mcp/cli-devtools.spec.ts index d1df0e71b784e..ef7f6b665ffdc 100644 --- a/tests/mcp/cli-devtools.spec.ts +++ b/tests/mcp/cli-devtools.spec.ts @@ -207,6 +207,15 @@ test('pick activates dashboard session', async ({ cdpServer, cli, server, startD expect(output).toContain(`locator: getByRole('button', { name: 'Submit' })`); }); +test('generate-locator', async ({ cli, server }) => { + server.setContent('/', ``, 'text/html'); + await cli('open', server.PREFIX); + await cli('snapshot'); + + const { output } = await cli('generate-locator', 'e2', '--raw'); + expect(output).toContain(`getByRole('button', { name: 'Submit' })`); +}); + test('highlight', async ({ cdpServer, cli, server }) => { server.setContent('/', ``, 'text/html'); const browserContext = await cdpServer.start();