🎯 The Golden Rule
Test what the user sees – not the implementation.
// ✅ CORRECT: Semantic, robust
await page.getByRole('button', { name: 'Submit' }).click();
// ❌ WRONG: Breaks on CSS changes
await page.click('#submit-btn.btn-primary');
🔍 Locator Priority (in this order)
| Priority | Method | Example | When to use |
|---|---|---|---|
| 1 | getByRole() | getByRole('button', { name: 'Submit' }) | Always first – semantic, ARIA-compliant |
| 2 | getByLabel() | getByLabel('Password') | Form fields |
| 3 | getByPlaceholder() | getByPlaceholder('Enter email') | Input hints |
| 4 | getByText() | getByText('Welcome') | Non-interactive elements |
| 5 | getByTestId() | getByTestId('checkout-btn') | When nothing else works |
Never use: CSS selectors, XPath, complex DOM paths
✅ Web-First Assertions (with Auto-Wait)
Playwright automatically waits up to 5 seconds (configurable):
// ✅ CORRECT – waits for visibility
await expect(page.getByText('Success')).toBeVisible();
await expect(page).toHaveTitle(/Dashboard/);
await expect(page.getByRole('listitem')).toHaveCount(3);
// ❌ WRONG – immediate check, no waiting
expect(await page.getByText('Success').isVisible()).toBe(true);
Common Assertions
// Visibility & State
await expect(locator).toBeVisible();
await expect(locator).toBeHidden();
await expect(locator).toBeEnabled();
await expect(locator).toBeDisabled();
// Content & Values
await expect(locator).toHaveText('Exact text');
await expect(locator).toContainText('Partial text');
await expect(locator).toHaveValue('Input value');
await expect(locator).toHaveAttribute('href', '/link');
// Count & URL
await expect(page.getByRole('article')).toHaveCount(5);
await expect(page).toHaveURL('/dashboard');
⚡ Actions (Auto-Wait built-in)
No sleep() or waitForTimeout() needed:
// Navigation
await page.goto('https://example.com');
// Interactions
await page.getByRole('button').click();
await page.getByLabel('Username').fill('andi');
await page.getByRole('checkbox').check();
await page.getByLabel('Country').selectOption('Germany');
// Advanced
await page.getByText('Menu').hover();
await page.getByRole('textbox').focus();
await page.keyboard.press('Enter');
await page.getByLabel('Upload').setInputFiles('file.pdf');
🔗 Filtering & Chaining Locators
// Find element within another
const product = page.getByRole('listitem').filter({ hasText: 'Product A' });
await product.getByRole('button', { name: 'Add to cart' }).click();
// Filter by absent text
await expect(
page.getByRole('listitem').filter({ hasNotText: 'Sold out' })
).toHaveCount(5);
// Combined filters
await page
.getByRole('listitem')
.filter({ hasText: 'John' })
.filter({ has: page.getByRole('button', { name: 'Edit' }) })
.click();
🛠️ Debugging & Development
# Test generator (record locators)
npx playwright codegen example.com
# Debug with inspector
npx playwright test --debug
# Enable trace for CI
npx playwright test --trace on
npx playwright show-report
# Debug single test
npx playwright test test.spec.ts:42 --debug
Useful Debugging Config
// playwright.config.ts
export default defineConfig({
use: {
// Local: full trace
trace: 'on',
// CI: only on retry
// trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
});
📋 Test Structure
import { test, expect } from '@playwright/test';
// Parallel execution in this file
test.describe.configure({ mode: 'parallel' });
test.describe('Authentication', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login');
});
test('successful login', async ({ page }) => {
await page.getByLabel('Email').fill('test@example.com');
await page.getByLabel('Password').fill('secret');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page).toHaveURL('/dashboard');
await expect(page.getByText('Welcome back')).toBeVisible();
});
test('failed login shows error', async ({ page }) => {
await page.getByLabel('Email').fill('wrong@example.com');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByText('Invalid credentials')).toBeVisible();
});
});
⚙️ Performance Optimization
// playwright.config.ts
export default defineConfig({
// Parallel workers
workers: process.env.CI ? 4 : undefined,
// Retries on flakiness
retries: process.env.CI ? 2 : 0,
// Projects for different browsers
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
});
🚨 Common Mistakes
| Mistake | Solution |
|---|---|
Using sleep(1000) | Use auto-wait, expect().toBeVisible() |
CSS selectors like #btn-123 | Use getByRole() or getByTestId() |
| Tests depend on other tests’ state | Use test.beforeEach for isolation |
| No assertions after action | Always use expect() |
| Hard-coded timeouts | expect(...).toBeVisible({ timeout: 10000 }) |
| Testing external sites | Mock with page.route() |
🔗 Further Reading
This cheat sheet is updated regularly. Last updated: April 2026