Playwright Testing
End-to-end testing with Playwright. Cross-browser testing, visual regression, API testing, and component testing. Use for E2E tests in TypeScript/JavaScript and Python projects.
- Category: discovered
- Tags: github, community
- Version: 1.0.0
- Author: walid
- Downloads: 0
- GitHub stars: 3
- Updated: 2025-11-21
Download the latest version (v1.0.0) from this page.
SKILL.md
---
name: Playwright Testing
description: End-to-end testing with Playwright. Cross-browser testing, visual regression, API testing, and component testing. Use for E2E tests in TypeScript/JavaScript and Python projects.
allowed-tools: Bash, Read, Edit, Write, Grep, Glob, TodoWrite
---
# Playwright Testing
Expert knowledge for end-to-end testing with Playwright - a modern cross-browser testing framework with powerful debugging and reliable automation.
## Core Expertise
**Playwright Advantages**
- **Cross-browser**: Chromium, Firefox, WebKit (Safari) support
- **Auto-wait**: Built-in smart waiting, no flaky tests
- **Powerful selectors**: CSS, text, role-based selectors
- **Network control**: Mock APIs, intercept requests
- **Visual testing**: Screenshots, videos, trace viewer
- **Parallel execution**: Fast test runs across browsers
- **TypeScript-first**: Excellent type safety and autocomplete
## Quick Start
### Installation (TypeScript/JavaScript)
```bash
# Using Bun
bun add -d @playwright/test
# Using npm
npm init playwright@latest
# Using pnpm
pnpm create playwright
```
### Installation (Python)
```bash
# Using uv
uv add --dev pytest-playwright
uv run playwright install
# Using pip
pip install pytest-playwright
playwright install
```
### Basic Configuration (TypeScript)
```typescript
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
// Mobile browsers
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
},
],
webServer: {
command: 'bun run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
})
```
### Basic Configuration (Python)
```python
# conftest.py
import pytest
@pytest.fixture(scope="session")
def browser_context_args(browser_context_args):
return {
**browser_context_args,
"viewport": {"width": 1920, "height": 1080},
"base_url": "http://localhost:3000",
}
```
## Running Tests
### TypeScript Commands
```bash
# Run all tests
npx playwright test
# Run in headed mode (see browser)
npx playwright test --headed
# Run specific test file
npx playwright test tests/login.spec.ts
# Run tests matching pattern
npx playwright test --grep="login"
# Run in debug mode
npx playwright test --debug
# Run with UI mode (interactive)
npx playwright test --ui
# Run specific browser
npx playwright test --project=chromium
npx playwright test --project=firefox
# Generate test report
npx playwright show-report
# Open trace viewer for failed tests
npx playwright show-trace trace.zip
```
### Python Commands
```bash
# Run all tests
uv run pytest
# Run with headed browser
uv run pytest --headed
# Run specific test
uv run pytest tests/test_login.py
# Run with specific browser
uv run pytest --browser chromium
uv run pytest --browser firefox
uv run pytest --browser webkit
# Slow motion (for debugging)
uv run pytest --slowmo 1000
# Generate video
uv run pytest --video on
```
## Writing Tests (TypeScript)
### Basic Test Structure
```typescript
import { test, expect } from '@playwright/test'
test.describe('Login flow', () => {
test('successful login', async ({ page }) => {
await page.goto('/')
await page.getByRole('link', { name: 'Login' }).click()
await page.getByLabel('Email').fill('user@example.com')
await page.getByLabel('Password').fill('password123')
await page.getByRole('button', { name: 'Sign in' }).click()
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible()
})
test('shows error for invalid credentials', async ({ page }) => {
await page.goto('/login')
await page.getByLabel('Email').fill('wrong@example.com')
await page.getByLabel('Password').fill('wrongpassword')
await page.getByRole('button', { name: 'Sign in' }).click()
await expect(page.getByText('Invalid credentials')).toBeVisible()
})
})
```
### Test Hooks
```typescript
import { test, expect } from '@playwright/test'
test.describe('User management', () => {
test.beforeAll(async () => {
// Setup database
})
test.beforeEach(async ({ page }) => {
await page.goto('/admin/users')
// Setup per test
})
test.afterEach(async ({ page }) => {
// Cleanup per test
})
test.afterAll(async () => {
// Teardown database
})
})
```
## Writing Tests (Python)
### Basic Test Structure
```python
import pytest
from playwright.sync_api import Page, expect
def test_successful_login(page: Page):
page.goto("/")
page.get_by_role("link", name="Login").click()
page.get_by_label("Email").fill("user@example.com")
page.get_by_label("Password").fill("password123")
page.get_by_role("button", name="Sign in").click()
expect(page.get_by_role("heading", name="Dashboard")).to_be_visible()
def test_invalid_credentials(page: Page):
page.goto("/login")
page.get_by_label("Email").fill("wrong@example.com")
page.get_by_label("Password").fill("wrongpassword")
page.get_by_role("button", name="Sign in").click()
expect(page.get_by_text("Invalid credentials")).to_be_visible()
```
### Test Fixtures (Python)
```python
import pytest
from playwright.sync_api import Page
@pytest.fixture
def authenticated_page(page: Page):
page.goto("/login")
page.get_by_label("Email").fill("user@example.com")
page.get_by_label("Password").fill("password123")
page.get_by_role("button", name="Sign in").click()
return page
def test_dashboard_access(authenticated_page: Page):
authenticated_page.goto("/dashboard")
expect(authenticated_page.get_by_role("heading", name="Dashboard")).to_be_visible()
```
## Selectors
### Role-Based Selectors (Recommended)
```typescript
// Most reliable - based on ARIA roles
await page.getByRole('button', { name: 'Submit' })
await page.getByRole('link', { name: 'Home' })
await page.getByRole('textbox', { name: 'Email' })
await page.getByRole('checkbox', { name: 'Remember me' })
await page.getByRole('heading', { name: 'Welcome', level: 1 })
```
### Text Selectors
```typescript
// Find by exact text
await page.getByText('Hello World')
// Find by partial text
await page.getByText('Hello', { exact: false })
// Case insensitive
await page.getByText(/hello world/i)
```
### Label Selectors
```typescript
// Find inputs by label
await page.getByLabel('Email')
await page.getByLabel('Password')
await page.getByLabel(/email/i)
```
### Placeholder and Alt Text
```typescript
await page.getByPlaceholder('Enter email')
await page.getByAltText('Company logo')
```
### Test ID (Fallback)
```typescript
// In HTML: <div data-testid="submit-button">
await page.getByTestId('submit-button')
```
### CSS Selectors (Use Sparingly)
```typescript
await page.locator('.btn-primary')
await page.locator('#login-form')
await page.locator('button.submit')
```
### Combining Selectors
```typescript
// Chaining
await page.getByRole('navigation').getByRole('link', { name: 'Home' })
// Filtering
await page.getByRole('listitem').filter({ hasText: 'Product 1' })
// First/Last/Nth
await page.getByRole('button').first()
await page.getByRole('button').last()
await page.getByRole('button').nth(2)
```
## Assertions
### Visibility Assertions
```typescript
await expect(page.getByText('Success')).toBeVisible()
await expect(page.getByText('Loading')).toBeHidden()
await expect(page.getByRole('button')).toBeEnabled()
await expect(page.getByRole('button')).toBeDisabled()
```
### Text Assertions
```typescript
await expect(page.getByRole('heading')).toHaveText('Welcome')
await expect(page.getByRole('heading')).toContainText('Welcome')
await expect(page.getByRole('alert')).toHaveText(/error/i)
```
### Attribute Assertions
```typescript
await expect(page.getByRole('link')).toHaveAttribute('href', '/home')
await expect(page.getByRole('img')).toHaveAttribute('alt', 'Logo')
await expect(page.getByRole('button')).toHaveClass('btn-primary')
```
### Value Assertions
```typescript
await expect(page.getByLabel('Email')).toHaveValue('user@example.com')
await expect(page.getByLabel('Count')).toHaveValue('5')
```
### URL Assertions
```typescript
await expect(page).toHaveURL('/dashboard')
await expect(page).toHaveURL(/\/dashboard/)
await expect(page).toHaveTitle('Dashboard')
```
### Count Assertions
```typescript
await expect(page.getByRole('listitem')).toHaveCount(5)
```
## Actions
### Clicking
```typescript
// Basic click
await page.getByRole('button', { name: 'Submit' }).click()
// Double click
await page.getByText('File').dblclick()
// Right click
await page.getByText('Item').click({ button: 'right' })
// Click with modifiers
await page.getByRole('link').click({ modifiers: ['Control'] })
// Force click (bypass visibility checks)
await page.getByRole('button').click({ force: true })
```
### Typing
```typescript
// Fill (clears then types)
await page.getByLabel('Email').fill('user@example.com')
// Type (appends)
await page.getByLabel('Search').type('query')
// Press keys
await page.getByLabel('Search').press('Enter')
await page.keyboard.press('Control+A')
// Type slowly (for animations)
await page.getByLabel('Search').type('query', { delay: 100 })
```
### Selecting
```typescript
// Select by value
await page.getByLabel('Country').selectOption('us')
// Select by label
await page.getByLabel('Country').selectOption({ label: 'United States' })
// Select multiple
await page.locator('select[multiple]').selectOption(['us', 'uk', 'ca'])
```
### File Upload
```typescript
// Single file
await page.getByLabel('Upload').setInputFiles('path/to/file.pdf')
// Multiple files
await page.getByLabel('Upload').setInputFiles([
'file1.pdf',
'file2.pdf',
])
// Remove files
await page.getByLabel('Upload').setInputFiles([])
```
### Drag and Drop
```typescript
await page.getByText('Item').dragTo(page.getByText('Target'))
```
## Network Mocking
### Intercept API Requests
```typescript
import { test, expect } from '@playwright/test'
test('mocks API response', async ({ page }) => {
// Mock API endpoint
await page.route('**/api/users', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 1, name: 'Test User' },
]),
})
})
await page.goto('/users')
await expect(page.getByText('Test User')).toBeVisible()
})
```
### Intercept and Modify
```typescript
test('modifies API response', async ({ page }) => {
await page.route('**/api/config', async route => {
const response = await route.fetch()
const json = await response.json()
json.feature_flag = true
await route.fulfill({ json })
})
await page.goto('/')
})
```
### Wait for Requests
```typescript
test('waits for API call', async ({ page }) => {
await page.goto('/')
// Wait for specific request
const response = await page.waitForResponse('**/api/users')
expect(response.status()).toBe(200)
// Wait for multiple requests
const [response1, response2] = await Promise.all([
page.waitForResponse('**/api/users'),
page.waitForResponse('**/api/posts'),
page.getByRole('button', { name: 'Load' }).click(),
])
})
```
## Visual Testing
### Screenshots
```typescript
import { test } from '@playwright/test'
test('captures screenshot', async ({ page }) => {
await page.goto('/')
// Full page screenshot
await page.screenshot({ path: 'screenshot.png', fullPage: true })
// Element screenshot
await page.getByRole('navigation').screenshot({ path: 'nav.png' })
// Compare screenshot
await expect(page).toHaveScreenshot('homepage.png')
})
```
### Video Recording
```typescript
// In playwright.config.ts
use: {
video: 'on', // or 'retain-on-failure', 'off'
videoSize: { width: 1280, height: 720 }
}
```
### Trace Viewer
```typescript
// In playwright.config.ts
use: {
trace: 'on-first-retry', // or 'on', 'off', 'retain-on-failure'
}
```
```bash
# View trace
npx playwright show-trace trace.zip
```
## Authentication & State
### Save Authentication State
```typescript
import { test as setup } from '@playwright/test'
setup('authenticate', async ({ page }) => {
await page.goto('/login')
await page.getByLabel('Email').fill('user@example.com')
await page.getByLabel('Password').fill('password123')
await page.getByRole('button', { name: 'Sign in' }).click()
// Save storage state
await page.context().storageState({ path: 'auth.json' })
})
```
### Reuse Authentication State
```typescript
// playwright.config.ts
export default defineConfig({
projects: [
{
name: 'setup',
testMatch: /auth\.setup\.ts/,
},
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'auth.json',
},
dependencies: ['setup'],
},
],
})
```
## Best Practices
**Selector Strategy**
1. **Prefer role-based selectors**: `getByRole('button', { name: 'Submit' })`
2. **Use labels for inputs**: `getByLabel('Email')`
3. **Text for content**: `getByText('Welcome')`
4. **Test IDs as fallback**: `getByTestId('submit-btn')`
5. **Avoid CSS selectors**: They break easily with styling changes
**Auto-Waiting**
- Playwright automatically waits for elements
- No need for manual `sleep()` or `waitFor()`
- Trust the built-in waiting mechanisms
**Isolation**
- Each test gets a fresh browser context
- No state pollution between tests
- Clean cookies, storage, cache per test
**Debugging**
```bash
# Debug mode (step through tests)
npx playwright test --debug
# Headed mode (see browser)
npx playwright test --headed --slowmo 1000
# UI mode (interactive explorer)
npx playwright test --ui
# Trace viewer (time-travel debugging)
npx playwright show-trace trace.zip
```
**Performance**
- Run tests in parallel (default)
- Use `fullyParallel: true` in config
- Limit workers in CI: `workers: 1`
- Reuse browser contexts when possible
**Flaky Tests**
- Use auto-waiting, avoid manual waits
- Use specific selectors, avoid brittle CSS
- Mock network requests for consistency
- Set proper timeouts for slow operations
**Test Data**
- Reset database state before tests
- Use unique test data per test
- Clean up after tests
- Mock external dependencies
## Common Patterns
### Page Object Model
```typescript
// pages/LoginPage.ts
import { Page, Locator } from '@playwright/test'
export class LoginPage {
readonly page: Page
readonly emailInput: Locator
readonly passwordInput: Locator
readonly submitButton: Locator
constructor(page: Page) {
this.page = page
this.emailInput = page.getByLabel('Email')
this.passwordInput = page.getByLabel('Password')
this.submitButton = page.getByRole('button', { name: 'Sign in' })
}
async goto() {
await this.page.goto('/login')
}
async login(email: string, password: string) {
await this.emailInput.fill(email)
await this.passwordInput.fill(password)
await this.submitButton.click()
}
}
// Usage in test
import { test, expect } from '@playwright/test'
import { LoginPage } from './pages/LoginPage'
test('login flow', async ({ page }) => {
const loginPage = new LoginPage(page)
await loginPage.goto()
await loginPage.login('user@example.com', 'password123')
await expect(page).toHaveURL('/dashboard')
})
```
### API Testing
```typescript
import { test, expect } from '@playwright/test'
test('API test', async ({ request }) => {
const response = await request.post('/api/users', {
data: {
name: 'John Doe',
email: 'john@example.com',
},
})
expect(response.ok()).toBeTruthy()
expect(response.status()).toBe(201)
const json = await response.json()
expect(json.id).toBeDefined()
expect(json.name).toBe('John Doe')
})
```
### Mobile Testing
```typescript
// playwright.config.ts
import { devices } from '@playwright/test'
export default defineConfig({
projects: [
{
name: 'iPhone 13',
use: { ...devices['iPhone 13'] },
},
{
name: 'iPad Pro',
use: { ...devices['iPad Pro'] },
},
],
})
```
## Troubleshooting
**Element not found**
```typescript
// Wait for element explicitly
await page.getByRole('button').waitFor()
// Check if element exists
const isVisible = await page.getByRole('button').isVisible()
```
**Timeout errors**
```typescript
// Increase timeout for specific action
await page.getByRole('button').click({ timeout: 10000 })
// Global timeout in config
use: {
actionTimeout: 10000,
navigationTimeout: 30000,
}
```
**Flaky tests**
```typescript
// Use auto-retry assertions
await expect(page.getByText('Success')).toBeVisible({ timeout: 5000 })
// Retry failed tests
retries: process.env.CI ? 2 : 0
```
## See Also
- `vitest-testing` - Unit and integration testing
- `test-quality-analysis` - Detecting test smells
- `nodejs-development` - TypeScript project setup
- `python-testing` - Python pytest integration
## References
- Official docs: https://playwright.dev/
- API reference: https://playwright.dev/docs/api/class-playwright
- Best practices: https://playwright.dev/docs/best-practices