Test generation (plan → generate → heal)
Part of the
playwright-cliskill. See SKILL.md for full context.
End-to-end workflow for authoring and maintaining Playwright tests with playwright-cli. Every playwright-cli action emits the equivalent Playwright TypeScript, and that generated code is the raw material for every test. The sections below can be used independently:
- How generation works — the core mechanic everything else relies on: actions become TypeScript, plus how to add assertions.
- Plan — explore the app, produce a spec file describing what to test.
- Generate — turn a spec into Playwright test files. Update the spec if it's vague or stale.
- Heal — diagnose failing tests, fix the code, reconcile the spec with reality.
Plan / generate / heal lean on the same mechanic: run npx playwright test --debug=cli in the background, then playwright-cli attach tw-XXXX to drive the paused page interactively. See playwright-tests.md for the debug/attach mechanics.
0. How generation works
Every action you perform with playwright-cli generates corresponding Playwright TypeScript code. This code appears in the output and can be copied directly into your test files.
# Start a session
playwright-cli open https://example.com/login
# Take a snapshot to see elements
playwright-cli snapshot
# Output shows: e1 [textbox "Email"], e2 [textbox "Password"], e3 [button "Sign In"]
# Fill form fields - generates code automatically
playwright-cli fill e1 "user@example.com"
# Ran Playwright code:
# await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com');
playwright-cli fill e2 "password123"
# Ran Playwright code:
# await page.getByRole('textbox', { name: 'Password' }).fill('password123');
playwright-cli click e3
# Ran Playwright code:
# await page.getByRole('button', { name: 'Sign In' }).click();Building a test file
Collect the generated code into a Playwright test:
import { test, expect } from '@playwright/test';
test('login flow', async ({ page }) => {
// Generated code from playwright-cli session:
await page.goto('https://example.com/login');
await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com');
await page.getByRole('textbox', { name: 'Password' }).fill('password123');
await page.getByRole('button', { name: 'Sign In' }).click();
// Add assertions
await expect(page).toHaveURL(/.*dashboard/);
});Use semantic locators
The generated code uses role-based locators when possible, which are more resilient:
// Generated (good - semantic)
await page.getByRole('button', { name: 'Submit' }).click();
// Avoid (fragile - CSS selectors)
await page.locator('#submit-btn').click();Explore before recording
Take snapshots to understand the page structure before recording actions:
playwright-cli open https://example.com
playwright-cli snapshot
# Review the element structure
playwright-cli click e5Add assertions manually
Generated code captures actions but not assertions. Add expectations in your test using one of the recommended matchers:
toBeVisible()— element is rendered and visibletoHaveText(text)— element text content matchestoHaveValue(value) / toBeEmpty()— input/select value matchestoBeChecked() / toBeUnchecked()— checkbox state matchestoMatchAriaSnapshot(snapshot)— page (or locator) matches a partial accessibility snapshot
Use playwright-cli generate-locator <target> 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.
# 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// Generated action
await page.getByRole('button', { name: 'Submit' }).click();
// 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"
`);Full plan → generate → heal workflow
The Planning, Generate, and Heal phases of the spec-driven workflow are documented in spec-driven-testing.md and spec-driven-heal.md. This file focuses on the generation mechanic itself (how playwright-cli actions become TypeScript), which those phases build on.
Cross-references
| For... | See |
|---|---|
--debug=cli / attach mechanics |
playwright-tests.md |
| Mocking requests during exploration/generation | request-mocking.md |
| Managing the CLI browser session | session-management.md |