When to use: Generating Playwright test code from interactive CLI sessions — recording user flows, building test scaffolds, converting manual testing into automated tests. Prerequisites: core-commands.md for basic CLI usage
# Every CLI action outputs the equivalent Playwright code
playwright-cli open https://example.com/login
playwright-cli snapshot
playwright-cli fill e1 "[email protected]"
# Output: await page.getByRole('textbox', { name: 'Email' }).fill('[email protected]');
playwright-cli fill e2 "password123"
# Output: await page.getByRole('textbox', { name: 'Password' }).fill('password123');
playwright-cli click e3
# Output: await page.getByRole('button', { name: 'Sign In' }).click();Every action you perform with playwright-cli automatically generates the corresponding Playwright TypeScript code in the output. This code uses the same role-based locators that Playwright recommends for production tests.
The workflow:
- Open a page → generates
await page.goto(url) - Take a snapshot → see element refs and their accessible roles
- Interact → each action generates one line of Playwright code
- Collect the code → assemble generated lines into a complete test
playwright-cli open https://example.com/login
# Ran Playwright code:
# await page.goto('https://example.com/login');
playwright-cli snapshot
# Output:
# e1 [textbox "Email"]
# e2 [textbox "Password"]
# e3 [button "Sign In"]
# e4 [link "Forgot password?"]
playwright-cli fill e1 "[email protected]"
# Ran Playwright code:
# await page.getByRole('textbox', { name: 'Email' }).fill('[email protected]');
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();Collect the generated code and wrap it in a Playwright test:
import { test, expect } from '@playwright/test';
test('user can log in', async ({ page }) => {
await page.goto('https://example.com/login');
await page.getByRole('textbox', { name: 'Email' }).fill('[email protected]');
await page.getByRole('textbox', { name: 'Password' }).fill('password123');
await page.getByRole('button', { name: 'Sign In' }).click();
// Add assertions (not generated — you add these)
await expect(page).toHaveURL(/.*dashboard/);
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});const { test, expect } = require('@playwright/test');
test('user can log in', async ({ page }) => {
await page.goto('https://example.com/login');
await page.getByRole('textbox', { name: 'Email' }).fill('[email protected]');
await page.getByRole('textbox', { name: 'Password' }).fill('password123');
await page.getByRole('button', { name: 'Sign In' }).click();
await expect(page).toHaveURL(/.*dashboard/);
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});The generated code uses Playwright's recommended locator hierarchy:
| Priority | Locator Type | Example | When used |
|---|---|---|---|
| 1 | Role-based | getByRole('button', { name: 'Submit' }) |
Elements with ARIA roles |
| 2 | Label-based | getByLabel('Email') |
Form inputs with labels |
| 3 | Placeholder | getByPlaceholder('Search...') |
Inputs with placeholder text |
| 4 | Text-based | getByText('Welcome back') |
Static text content |
| 5 | Test ID | getByTestId('submit-btn') |
Elements with data-testid |
These locators are resilient to markup changes — they mirror how users perceive the page rather than relying on CSS selectors or XPath.
playwright-cli open https://shop.example.com
# Browse products
playwright-cli snapshot
playwright-cli click e5 # "Add to Cart" button
# await page.getByRole('button', { name: 'Add to Cart' }).click();
playwright-cli click e12 # Cart icon
# await page.getByRole('link', { name: 'Cart' }).click();
playwright-cli snapshot
playwright-cli click e3 # "Proceed to Checkout"
# await page.getByRole('button', { name: 'Proceed to Checkout' }).click();
# Shipping info
playwright-cli snapshot
playwright-cli fill e1 "Jane Doe"
# await page.getByRole('textbox', { name: 'Full Name' }).fill('Jane Doe');
playwright-cli fill e2 "123 Main St"
# await page.getByRole('textbox', { name: 'Address' }).fill('123 Main St');
playwright-cli fill e3 "Springfield"
# await page.getByRole('textbox', { name: 'City' }).fill('Springfield');
playwright-cli select e4 "IL"
# await page.getByRole('combobox', { name: 'State' }).selectOption('IL');
playwright-cli fill e5 "62701"
# await page.getByRole('textbox', { name: 'ZIP Code' }).fill('62701');
playwright-cli click e6 # "Continue to Payment"playwright-cli open https://example.com/onboarding
# Step 1
playwright-cli snapshot
playwright-cli fill e1 "Acme Corp"
playwright-cli select e2 "technology"
playwright-cli click e3 # Next
# Step 2
playwright-cli snapshot
playwright-cli check e1 # Feature checkbox
playwright-cli check e3 # Another feature
playwright-cli click e5 # Next
# Step 3
playwright-cli snapshot
playwright-cli click e2 # "Complete Setup"playwright-cli open https://example.com
playwright-cli snapshot
playwright-cli fill e1 "playwright testing"
# await page.getByRole('searchbox', { name: 'Search' }).fill('playwright testing');
playwright-cli press Enter
# await page.keyboard.press('Enter');
playwright-cli snapshot # See search results — new refs assigned
playwright-cli click e3 # First result
# await page.getByRole('link', { name: 'Getting Started with Playwright' }).click();Generated code captures actions but not assertions. Always add assertions manually to create meaningful tests.
// URL changed after navigation
await expect(page).toHaveURL(/.*dashboard/);
await expect(page).toHaveURL('https://example.com/success');
// Element is visible
await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();
await expect(page.getByText('Order confirmed')).toBeVisible();
// Element contains text
await expect(page.getByTestId('total')).toHaveText('$99.99');
await expect(page.getByRole('alert')).toContainText('saved');
// Element has specific attribute
await expect(page.getByRole('button', { name: 'Submit' })).toBeDisabled();
await expect(page.getByRole('checkbox')).toBeChecked();
// Element count
await expect(page.getByRole('listitem')).toHaveCount(5);
// Page title
await expect(page).toHaveTitle(/Dashboard/);
// Screenshot comparison
await expect(page).toHaveScreenshot('checkout.png');test('complete checkout flow', async ({ page }) => {
await page.goto('https://shop.example.com/products');
// Action: Add item to cart
await page.getByRole('button', { name: 'Add to Cart' }).click();
// Assertion: Cart badge updates
await expect(page.getByTestId('cart-count')).toHaveText('1');
// Action: Go to cart
await page.getByRole('link', { name: 'Cart' }).click();
// Assertion: Correct page
await expect(page).toHaveURL(/.*cart/);
await expect(page.getByRole('heading', { name: 'Your Cart' })).toBeVisible();
// Action: Proceed to checkout
await page.getByRole('button', { name: 'Checkout' }).click();
// Action: Fill shipping
await page.getByRole('textbox', { name: 'Full Name' }).fill('Jane Doe');
await page.getByRole('textbox', { name: 'Address' }).fill('123 Main St');
await page.getByRole('button', { name: 'Place Order' }).click();
// Assertion: Order confirmed
await expect(page.getByText('Order confirmed')).toBeVisible();
await expect(page.getByTestId('order-number')).toBeVisible();
});Take a snapshot first to understand the page structure. Don't blindly click — know what elements are available:
playwright-cli open https://example.com
playwright-cli snapshot
# Review the elements, plan your flow, then start interactingThe generated code already prefers role-based locators. If you see CSS selectors in generated output, consider filing an issue — role-based locators are more resilient:
// Generated (good — semantic, resilient)
await page.getByRole('button', { name: 'Submit' }).click();
// Avoid writing manually (fragile — breaks if CSS changes)
await page.locator('#submit-btn').click();
await page.locator('.btn.btn-primary').click();One test = one user behavior. Don't record an entire session into a single test:
// Good: Focused test
test('user can add item to cart', async ({ page }) => {
// Just the add-to-cart flow
});
test('user can complete checkout', async ({ page }) => {
// Just the checkout flow (use auth state to skip login)
});
// Bad: Monolith test
test('user journey', async ({ page }) => {
// Login + browse + add to cart + checkout + verify email...
});Replace hardcoded values from the recording with variables or test data:
// Instead of hardcoded values from recording
test('registration', async ({ page }) => {
const user = {
name: 'Jane Doe',
email: `test+${Date.now()}@example.com`,
password: 'SecurePass123!'
};
await page.goto('/register');
await page.getByRole('textbox', { name: 'Name' }).fill(user.name);
await page.getByRole('textbox', { name: 'Email' }).fill(user.email);
await page.getByRole('textbox', { name: 'Password' }).fill(user.password);
await page.getByRole('button', { name: 'Create Account' }).click();
await expect(page).toHaveURL(/.*welcome/);
});If a recorded action depends on async content loading, add explicit waits:
// Before clicking a dynamically loaded element
await page.waitForSelector('.results-loaded');
await page.getByRole('link', { name: 'First Result' }).click();
// Or use Playwright's auto-waiting (preferred)
await expect(page.getByRole('link', { name: 'First Result' })).toBeVisible();
await page.getByRole('link', { name: 'First Result' }).click();- Generated code is a starting point — always review, add assertions, and parameterize before committing to your test suite
- Re-snapshot after dynamic changes — refs change when the DOM updates
- Combine with
state-save— record a login flow once, save state, then start all other recordings from the authenticated state - Use
--filenamefor snapshots — save before/after snapshots for complex flows to help write assertions later