playwright
Playwright
Setup
npm init playwright@latest # scaffolds project with config, sample test, browsers
npx playwright install # install all browsers
npx playwright install chromium # install single browser
Configuration
// 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'], ['list']],
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'] } },
{ name: 'mobile-chrome', use: { ...devices['Pixel 5'] } },
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
Writing Tests
import { test, expect } from '@playwright/test';
test.describe('Feature Name', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('should do something specific', async ({ page }) => {
await page.getByRole('button', { name: 'Submit' }).click();
await expect(page.getByText('Success')).toBeVisible();
});
});
Playwright auto-waits for elements to be visible, stable, enabled, and receiving events before performing actions. Assertions auto-retry until the timeout (default 5s).
Locators
Prefer role-based and user-facing locators over CSS selectors.
// Role-based (best for accessibility)
page.getByRole('button', { name: 'Sign In' })
page.getByRole('heading', { name: 'Dashboard', level: 2 })
page.getByRole('textbox', { name: 'Email' })
page.getByRole('checkbox', { name: 'Remember me' })
// Text and label-based
page.getByText('Welcome back')
page.getByLabel('Email address')
page.getByPlaceholder('Enter your email')
page.getByTestId('submit-button') // matches [data-testid="submit-button"]
// CSS and XPath (last resort)
page.locator('.nav-item.active')
page.locator('xpath=//div[@class="container"]//span')
// Filtering and chaining
page.getByRole('listitem').filter({ hasText: 'Product A' })
page.getByRole('listitem').filter({ has: page.getByRole('button', { name: 'Buy' }) })
page.locator('.card').nth(2)
page.locator('.card').first()
Actions
// Click variants
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByRole('button').dblclick();
await page.getByRole('button').click({ button: 'right' });
// Text input
await page.getByLabel('Email').fill('user@example.com'); // clears then sets value
await page.getByLabel('Name').pressSequentially('John', { delay: 50 }); // simulates typing
await page.getByLabel('Name').clear();
// Keyboard
await page.keyboard.press('Enter');
await page.keyboard.press('Control+A');
// Select, checkbox, radio
await page.getByLabel('Country').selectOption('us');
await page.getByLabel('Country').selectOption({ label: 'United States' });
await page.getByRole('checkbox', { name: 'Agree' }).check();
await page.getByRole('checkbox', { name: 'Agree' }).uncheck();
// Hover, focus, drag
await page.getByText('Menu').hover();
await page.getByLabel('Email').focus();
await page.getByTestId('source').dragTo(page.getByTestId('target'));
Assertions
await expect(page.getByText('Welcome')).toBeVisible();
await expect(page.getByText('Loading')).toBeHidden();
await expect(page.getByRole('heading')).toHaveText('Dashboard');
await expect(page.getByRole('heading')).toHaveText(/dashboard/i);
await expect(page.getByRole('status')).toContainText('3 items');
await expect(page.getByLabel('Email')).toHaveValue('user@example.com');
await expect(page).toHaveURL('/dashboard');
await expect(page).toHaveTitle('My App - Dashboard');
await expect(page.getByRole('button')).toBeEnabled();
await expect(page.getByRole('checkbox')).toBeChecked();
await expect(page.getByRole('listitem')).toHaveCount(5);
await expect(page.getByTestId('card')).toHaveClass(/highlighted/);
await expect(page.getByTestId('card')).toHaveAttribute('data-status', 'active');
await expect(page.getByText('Error')).not.toBeVisible();
// Soft assertions (do not stop the test on failure)
await expect.soft(page.getByText('Title')).toHaveText('Expected');
Page Navigation and Waiting
await page.goto('https://example.com');
await page.goto('/relative-path'); // uses baseURL from config
await page.goBack();
await page.reload();
await page.waitForURL('**/dashboard');
await page.waitForSelector('.dynamic-content', { state: 'visible' });
await page.waitForSelector('.spinner', { state: 'detached' });
const response = await page.waitForResponse(
resp => resp.url().includes('/api/users') && resp.status() === 200
);
await page.waitForLoadState('networkidle');
await page.waitForFunction(() => document.title.includes('Ready'));
await page.getByText('Loaded').click({ timeout: 10000 });
Network Interception
// Mock an API response
await page.route('**/api/users', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{ id: 1, name: 'Alice' }]),
});
});
// Modify a real response
await page.route('**/api/settings', async route => {
const response = await route.fetch();
const json = await response.json();
json.featureFlag = true;
await route.fulfill({ response, json });
});
// Block resources to speed up tests
await page.route('**/*.{png,jpg,jpeg,gif,svg}', route => route.abort());
// Inspect outgoing requests
await page.route('**/api/submit', async route => {
const postData = route.request().postDataJSON();
expect(postData.email).toBe('user@example.com');
await route.continue();
});
// HAR recording and replay
await page.routeFromHAR('tests/data/api.har', { update: true }); // record
await page.routeFromHAR('tests/data/api.har'); // replay
Authentication
Save login state once, reuse across all tests:
// auth.setup.ts
import { test as setup } from '@playwright/test';
const authFile = 'playwright/.auth/user.json';
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();
await page.waitForURL('/dashboard');
await page.context().storageState({ path: authFile });
});
// In playwright.config.ts projects array
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
dependencies: ['setup'],
use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/user.json' },
},
Add playwright/.auth/ to .gitignore.
Multiple Pages, Tabs, and Popups
const newPage = await page.context().newPage();
await newPage.goto('/another-page');
// Handle popup (OAuth window, target="_blank")
const popupPromise = page.waitForEvent('popup');
await page.getByRole('button', { name: 'Sign in with Google' }).click();
const popup = await popupPromise;
await popup.waitForLoadState();
// Isolated sessions with separate browser contexts
const context1 = await browser.newContext();
const context2 = await browser.newContext();
const page1 = await context1.newPage();
const page2 = await context2.newPage();
Screenshots and Video
await page.screenshot({ path: 'screenshots/home.png' });
await page.screenshot({ path: 'full.png', fullPage: true });
await page.getByTestId('chart').screenshot({ path: 'chart.png' });
Configure globally in playwright.config.ts under use: screenshot: 'only-on-failure', video: 'retain-on-failure'.
Visual Regression Testing
await expect(page).toHaveScreenshot();
await expect(page).toHaveScreenshot('homepage.png');
await expect(page).toHaveScreenshot({ maxDiffPixels: 100 });
await expect(page.getByTestId('header')).toHaveScreenshot('header.png');
Update baselines with npx playwright test --update-snapshots. Baselines are stored alongside the test file in a -snapshots/ directory. Commit them to version control.
Parallel Execution and Sharding
// playwright.config.ts
export default defineConfig({
fullyParallel: true, // parallelize tests within a single file
workers: 4, // fixed worker count (or '50%' for percentage of CPUs)
});
// Force serial execution for a specific describe block
test.describe.configure({ mode: 'serial' });
Shard across CI machines: npx playwright test --shard=1/3, --shard=2/3, --shard=3/3.
Debugging
npx playwright test --headed # see the browser
npx playwright test --debug # step through with Inspector
npx playwright codegen https://example.com # record actions as code
npx playwright show-trace trace.zip # open trace viewer
npx playwright test --grep "login" # filter by test name
npx playwright test tests/login.spec.ts # run specific file
Use await page.pause() inside a test to pause execution and open Inspector.
Trace viewer shows a timeline of actions, DOM snapshots at each step, network requests, and console logs. Enable with trace: 'on-first-retry' in config or record manually:
await page.context().tracing.start({ screenshots: true, snapshots: true });
// ... actions ...
await page.context().tracing.stop({ path: 'trace.zip' });
Page Object Model
// pages/login.page.ts
import { type Locator, type Page, expect } from '@playwright/test';
export class LoginPage {
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
constructor(private 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();
}
}
// tests/login.spec.ts
test('successful login', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'password123');
await expect(page).toHaveURL('/dashboard');
});
CI/CD Integration
GitHub Actions
# .github/workflows/playwright.yml
name: Playwright Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with: { name: playwright-report, path: playwright-report/, retention-days: 30 }
Sharded CI
jobs:
test:
strategy:
matrix:
shard: [1/4, 2/4, 3/4, 4/4]
steps:
- run: npx playwright test --shard=${{ matrix.shard }}
Docker
FROM mcr.microsoft.com/playwright:v1.48.0-noble
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npx playwright test
Common Patterns
File Upload and Download
await page.getByLabel('Upload').setInputFiles('tests/fixtures/doc.pdf');
await page.getByLabel('Upload').setInputFiles(['a.png', 'b.png']); // multiple
await page.getByLabel('Upload').setInputFiles([]); // clear
const downloadPromise = page.waitForEvent('download');
await page.getByRole('link', { name: 'Download Report' }).click();
const download = await downloadPromise;
await download.saveAs('downloads/' + download.suggestedFilename());
Iframe Handling
const frame = page.frameLocator('#my-iframe');
await frame.getByRole('button', { name: 'Click me' }).click();
await expect(frame.getByText('Done')).toBeVisible();
Dialog Handling
page.on('dialog', dialog => dialog.accept()); // accept all alerts/confirms
page.on('dialog', dialog => dialog.dismiss()); // dismiss all
page.once('dialog', async dialog => {
expect(dialog.message()).toBe('Are you sure?');
await dialog.accept();
});
await page.getByRole('button', { name: 'Delete' }).click();
Waiting for API Before Asserting
const responsePromise = page.waitForResponse('**/api/save');
await page.getByRole('button', { name: 'Save' }).click();
const response = await responsePromise;
expect(response.status()).toBe(200);
await expect(page.getByText('Saved')).toBeVisible();
Form Validation Testing
test('validates required fields', async ({ page }) => {
await page.goto('/contact');
await page.getByRole('button', { name: 'Submit' }).click();
await expect(page.getByText('Email is required')).toBeVisible();
await page.getByLabel('Email').fill('invalid');
await expect(page.getByText('Invalid email format')).toBeVisible();
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('Message').fill('Hello');
await page.getByRole('button', { name: 'Submit' }).click();
await expect(page.getByText('Message sent')).toBeVisible();
});
More from 1mangesh1/dev-skills-collection
curl-http
HTTP request construction and API testing with curl and HTTPie. Use when user asks to "test API", "make HTTP request", "curl POST", "send request", "test endpoint", "debug API", "upload file", "check response time", "set auth header", "basic auth with curl", "send JSON", "test webhook", "check status code", "follow redirects", "rate limit testing", "measure API latency", "stress test endpoint", "mock API response", or any HTTP calls from the command line.
28database-indexing
Database indexing internals, index type selection, query plan analysis, and write-overhead tradeoffs across PostgreSQL, MySQL, and MongoDB. Use when user asks to "optimize queries", "create indexes", "fix slow queries", "read EXPLAIN output", "reduce query time", "index strategy", "database performance", "composite index", "covering index", "partial index", "index bloat", "unused indexes", or needs help diagnosing and resolving database performance problems.
13testing-strategies
Testing strategies, patterns, and methodologies across the full testing spectrum. Use when asked about unit tests, integration tests, e2e tests, test pyramid, mocking, test doubles, TDD, property-based testing, snapshot testing, test coverage, mutation testing, contract testing, performance testing, test data management, CI/CD testing, flaky tests, test anti-patterns, test organization, test isolation, test fixtures, test parameterization, or any testing strategy, approach, or methodology.
10secret-scanner
This skill should be used when the user asks to "scan for secrets", "find API keys", "detect credentials", "check for hardcoded passwords", "find leaked tokens", "scan for sensitive keys", "check git history for secrets", "audit repository for credentials", or mentions secret detection, credential scanning, API key exposure, token leakage, password detection, or security key auditing.
10terraform
Terraform infrastructure as code for provisioning, modules, state management, and workspaces. Use when user asks to "create infrastructure", "write Terraform", "manage state", "create module", "import resource", "plan changes", or any IaC tasks.
10kubernetes
Kubernetes and kubectl mastery for deployments, services, pods, debugging, and cluster management. Use when user asks to "deploy to k8s", "create deployment", "debug pod", "kubectl commands", "scale service", "check pod logs", "create ingress", or any Kubernetes tasks.
10