Files
claude-code-guide/skills/playwright/playwright-cli/test-generation.md
T

11 KiB

Test Generation

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

Quick Reference

# Every CLI action outputs the equivalent Playwright code
playwright-cli open https://example.com/login
playwright-cli snapshot
playwright-cli fill e1 "user@example.com"
# Output: await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com');

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();

How It Works

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:

  1. Open a page → generates await page.goto(url)
  2. Take a snapshot → see element refs and their accessible roles
  3. Interact → each action generates one line of Playwright code
  4. Collect the code → assemble generated lines into a complete test

Recording a Flow

Example: Login Flow

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 "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();

Assemble into a Test

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("user@example.com")
  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("user@example.com")
  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()
})

Locator Strategies in Generated Code

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.

Recording Complex Flows

E-Commerce Checkout

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"

Multi-Step Wizard

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"

Search with Dynamic Results

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();

Adding Assertions

Generated code captures actions but not assertions. Always add assertions manually to create meaningful tests.

Common Assertions to Add

// 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")

Where to Place Assertions

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()
})

Best Practices

1. Explore Before Recording

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 interacting

2. Use Semantic Locators

The 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()

3. Keep Tests Focused

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...
})

4. Parameterize Test Data

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/)
})

5. Add Wait Strategies for Flaky Steps

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()

Tips

  • 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 --filename for snapshots — save before/after snapshots for complex flows to help write assertions later