webapp-testing

Web Application Testing with Playwright

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "webapp-testing" with this command: npx skills add vapvarun/claude-backup/vapvarun-claude-backup-webapp-testing

Web Application Testing with Playwright

Comprehensive E2E testing patterns for web applications.

Quick Start

Python Setup

from playwright.sync_api import sync_playwright

with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page() page.goto('http://localhost:3000') page.wait_for_load_state('networkidle') # ... test logic browser.close()

JavaScript/TypeScript Setup

import { test, expect } from '@playwright/test';

test('example test', async ({ page }) => { await page.goto('http://localhost:3000'); await expect(page.locator('h1')).toContainText('Welcome'); });

Server Management

Using Helper Scripts

Single server:

python scripts/with_server.py --server "npm run dev" --port 5173 -- python your_test.py

Multiple servers:

python scripts/with_server.py
--server "cd backend && python server.py" --port 3000
--server "cd frontend && npm run dev" --port 5173
-- python your_test.py

Playwright Config (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', },

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'] } }, { name: 'Mobile Safari', use: { ...devices['iPhone 12'] } }, ],

webServer: { command: 'npm run dev', url: 'http://localhost:3000', reuseExistingServer: !process.env.CI, }, });

Selectors

Best Practices

// BEST: Test IDs (most reliable) page.locator('[data-testid="submit-button"]') page.getByTestId('submit-button')

// GOOD: Role-based (accessible) page.getByRole('button', { name: 'Submit' }) page.getByRole('heading', { level: 1 }) page.getByRole('link', { name: 'Learn more' })

// GOOD: Label-based (forms) page.getByLabel('Email address') page.getByPlaceholder('Enter your email')

// GOOD: Text content page.getByText('Welcome back') page.getByText(/welcome/i) // Case-insensitive regex

// AVOID: CSS selectors (brittle) page.locator('.btn-primary') // Class might change page.locator('#submit') // ID might change

Selector Chaining

// Find within a container const form = page.locator('form[data-testid="login-form"]'); await form.getByLabel('Email').fill('user@example.com'); await form.getByRole('button', { name: 'Log in' }).click();

// Filter results await page.getByRole('listitem') .filter({ hasText: 'Product 1' }) .getByRole('button', { name: 'Add to cart' }) .click();

Common Test Patterns

Authentication Flow

import { test, expect } from '@playwright/test';

test.describe('Authentication', () => { test('successful login', 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: 'Log in' }).click();

// Verify redirect to dashboard
await expect(page).toHaveURL('/dashboard');
await expect(page.getByText('Welcome back')).toBeVisible();

});

test('invalid credentials show error', 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: 'Log in' }).click();

await expect(page.getByText('Invalid credentials')).toBeVisible();
await expect(page).toHaveURL('/login'); // Still on login page

});

test('logout', async ({ page }) => { // Login first (or use authenticated state) await page.goto('/dashboard'); await page.getByRole('button', { name: 'Logout' }).click();

await expect(page).toHaveURL('/login');

}); });

Form Submission

test.describe('Contact Form', () => { test.beforeEach(async ({ page }) => { await page.goto('/contact'); });

test('submits form with valid data', async ({ page }) => { await page.getByLabel('Name').fill('John Doe'); await page.getByLabel('Email').fill('john@example.com'); await page.getByLabel('Message').fill('This is a test message'); await page.getByRole('button', { name: 'Send' }).click();

await expect(page.getByText('Message sent successfully')).toBeVisible();

});

test('shows validation errors for empty fields', async ({ page }) => { await page.getByRole('button', { name: 'Send' }).click();

await expect(page.getByText('Name is required')).toBeVisible();
await expect(page.getByText('Email is required')).toBeVisible();

});

test('validates email format', async ({ page }) => { await page.getByLabel('Name').fill('John'); await page.getByLabel('Email').fill('invalid-email'); await page.getByRole('button', { name: 'Send' }).click();

await expect(page.getByText('Invalid email address')).toBeVisible();

}); });

Navigation Testing

test.describe('Navigation', () => { test('main menu links work', async ({ page }) => { await page.goto('/');

// Test each navigation link
const navLinks = [
  { name: 'Home', url: '/' },
  { name: 'About', url: '/about' },
  { name: 'Products', url: '/products' },
  { name: 'Contact', url: '/contact' },
];

for (const link of navLinks) {
  await page.getByRole('link', { name: link.name }).click();
  await expect(page).toHaveURL(link.url);
}

});

test('breadcrumbs show correct path', async ({ page }) => { await page.goto('/products/category/item-1');

const breadcrumbs = page.getByRole('navigation', { name: 'Breadcrumb' });
await expect(breadcrumbs.getByText('Home')).toBeVisible();
await expect(breadcrumbs.getByText('Products')).toBeVisible();
await expect(breadcrumbs.getByText('Category')).toBeVisible();

}); });

CRUD Operations

test.describe('Product Management', () => { test('creates new product', async ({ page }) => { await page.goto('/admin/products'); await page.getByRole('button', { name: 'Add Product' }).click();

await page.getByLabel('Name').fill('New Product');
await page.getByLabel('Price').fill('29.99');
await page.getByLabel('Description').fill('Product description');
await page.getByRole('button', { name: 'Save' }).click();

await expect(page.getByText('Product created')).toBeVisible();
await expect(page.getByText('New Product')).toBeVisible();

});

test('edits existing product', async ({ page }) => { await page.goto('/admin/products');

// Find product row and click edit
await page.getByRole('row', { name: /Existing Product/ })
  .getByRole('button', { name: 'Edit' })
  .click();

await page.getByLabel('Name').fill('Updated Product');
await page.getByRole('button', { name: 'Save' }).click();

await expect(page.getByText('Product updated')).toBeVisible();
await expect(page.getByText('Updated Product')).toBeVisible();

});

test('deletes product with confirmation', async ({ page }) => { await page.goto('/admin/products');

// Click delete button
await page.getByRole('row', { name: /Product to Delete/ })
  .getByRole('button', { name: 'Delete' })
  .click();

// Handle confirmation dialog
await page.getByRole('button', { name: 'Confirm' }).click();

await expect(page.getByText('Product deleted')).toBeVisible();
await expect(page.getByText('Product to Delete')).not.toBeVisible();

}); });

Modal/Dialog Testing

test.describe('Modal Dialogs', () => { test('opens and closes modal', async ({ page }) => { await page.goto('/');

// Open modal
await page.getByRole('button', { name: 'Open Modal' }).click();
await expect(page.getByRole('dialog')).toBeVisible();

// Close with X button
await page.getByRole('button', { name: 'Close' }).click();
await expect(page.getByRole('dialog')).not.toBeVisible();

});

test('closes modal on escape key', async ({ page }) => { await page.goto('/');

await page.getByRole('button', { name: 'Open Modal' }).click();
await expect(page.getByRole('dialog')).toBeVisible();

await page.keyboard.press('Escape');
await expect(page.getByRole('dialog')).not.toBeVisible();

});

test('closes modal on backdrop click', async ({ page }) => { await page.goto('/');

await page.getByRole('button', { name: 'Open Modal' }).click();
await expect(page.getByRole('dialog')).toBeVisible();

// Click outside modal
await page.locator('.modal-backdrop').click({ position: { x: 10, y: 10 } });
await expect(page.getByRole('dialog')).not.toBeVisible();

}); });

Waiting Strategies

Explicit Waits

// Wait for element await page.waitForSelector('[data-testid="content"]');

// Wait for element state await page.getByRole('button').waitFor({ state: 'visible' }); await page.getByRole('button').waitFor({ state: 'hidden' });

// Wait for navigation await page.waitForURL('/dashboard'); await page.waitForURL(//user/\d+/);

// Wait for network await page.waitForResponse('/api/users'); await page.waitForResponse(response => response.url().includes('/api/') && response.status() === 200 );

// Wait for load state await page.waitForLoadState('networkidle'); await page.waitForLoadState('domcontentloaded');

Auto-Waiting

// Playwright auto-waits for these await page.click('button'); // Waits for button to be actionable await page.fill('input', 'text'); // Waits for input to be editable await expect(locator).toBeVisible(); // Waits up to timeout

Assertions

Common Assertions

// Visibility await expect(locator).toBeVisible(); await expect(locator).toBeHidden(); await expect(locator).not.toBeVisible();

// Text content await expect(locator).toHaveText('exact text'); await expect(locator).toContainText('partial'); await expect(locator).toHaveText(/regex/i);

// Attributes await expect(locator).toHaveAttribute('href', '/about'); await expect(locator).toHaveClass(/active/); await expect(locator).toHaveId('main-content');

// Input values await expect(locator).toHaveValue('input value'); await expect(locator).toBeChecked(); await expect(locator).toBeDisabled(); await expect(locator).toBeEditable();

// Count await expect(locator).toHaveCount(5);

// Page assertions await expect(page).toHaveURL('/dashboard'); await expect(page).toHaveTitle('Dashboard | My App');

Soft Assertions

// Continue test even if assertion fails await expect.soft(locator).toHaveText('text'); await expect.soft(locator).toBeVisible();

// Check all soft assertions at end expect(test.info().errors).toHaveLength(0);

API Testing Integration

Mock API Responses

test('shows loading and data states', async ({ page }) => { // Intercept API request await page.route('/api/users', async route => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify([ { id: 1, name: 'John' }, { id: 2, name: 'Jane' }, ]), }); });

await page.goto('/users'); await expect(page.getByText('John')).toBeVisible(); await expect(page.getByText('Jane')).toBeVisible(); });

test('handles API errors gracefully', async ({ page }) => { await page.route('/api/users', route => route.fulfill({ status: 500 }) );

await page.goto('/users'); await expect(page.getByText('Failed to load users')).toBeVisible(); });

Wait for API Calls

test('submits form and waits for API', async ({ page }) => { await page.goto('/contact');

// Start waiting for API response before triggering it const responsePromise = page.waitForResponse('/api/contact');

await page.getByLabel('Email').fill('test@example.com'); await page.getByRole('button', { name: 'Submit' }).click();

const response = await responsePromise; expect(response.status()).toBe(200); });

Visual Testing

Screenshots

test('homepage visual test', async ({ page }) => { await page.goto('/');

// Full page screenshot await expect(page).toHaveScreenshot('homepage.png', { fullPage: true, }); });

test('component visual test', async ({ page }) => { await page.goto('/');

// Element screenshot await expect(page.getByTestId('header')).toHaveScreenshot('header.png'); });

Screenshot Options

await page.screenshot({ path: 'screenshots/test.png', fullPage: true, animations: 'disabled', // Reduce flakiness mask: [page.locator('.dynamic-content')], // Hide changing content });

Authentication Reuse

Save Auth State

// auth.setup.ts import { test as setup, expect } 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('password'); await page.getByRole('button', { name: 'Sign in' }).click();

await page.waitForURL('/dashboard');

// Save signed-in state await page.context().storageState({ path: authFile }); });

Use Auth State

// playwright.config.ts export default defineConfig({ projects: [ { name: 'setup', testMatch: /.*.setup.ts/ }, { name: 'tests', dependencies: ['setup'], use: { storageState: 'playwright/.auth/user.json', }, }, ], });

Accessibility Testing

import { test, expect } from '@playwright/test'; import AxeBuilder from '@axe-core/playwright';

test.describe('Accessibility', () => { test('homepage has no a11y violations', async ({ page }) => { await page.goto('/');

const results = await new AxeBuilder({ page }).analyze();

expect(results.violations).toEqual([]);

});

test('form is accessible', async ({ page }) => { await page.goto('/contact');

const results = await new AxeBuilder({ page })
  .include('form')
  .analyze();

expect(results.violations).toEqual([]);

}); });

Performance Testing

test('page loads within performance budget', async ({ page }) => { await page.goto('/');

const metrics = await page.evaluate(() => JSON.stringify(window.performance.timing) ); const timing = JSON.parse(metrics);

const loadTime = timing.loadEventEnd - timing.navigationStart; expect(loadTime).toBeLessThan(3000); // 3 seconds });

test('tracks Core Web Vitals', async ({ page }) => { await page.goto('/');

const lcp = await page.evaluate(() => { return new Promise(resolve => { new PerformanceObserver(list => { const entries = list.getEntries(); resolve(entries[entries.length - 1].startTime); }).observe({ type: 'largest-contentful-paint', buffered: true }); }); });

expect(lcp).toBeLessThan(2500); // Good LCP is < 2.5s });

Debug Helpers

Debugging Commands

Run with headed browser

npx playwright test --headed

Run with debugging UI

npx playwright test --debug

Run specific test

npx playwright test -g "test name"

Show report

npx playwright show-report

In-Test Debugging

test('debug example', async ({ page }) => { await page.goto('/');

// Pause execution await page.pause();

// Take screenshot await page.screenshot({ path: 'debug.png' });

// Log to console console.log(await page.content());

// Slow down await page.setDefaultTimeout(30000); });

Console Logs

Python: Capture browser console

page.on('console', lambda msg: print(f'Browser log: {msg.text}')) page.on('pageerror', lambda err: print(f'Browser error: {err}'))

// TypeScript: Capture browser console page.on('console', msg => console.log('Browser:', msg.text())); page.on('pageerror', err => console.log('Error:', err));

Test Organization

File Structure

tests/ ├── e2e/ │ ├── auth/ │ │ ├── login.spec.ts │ │ └── logout.spec.ts │ ├── products/ │ │ ├── listing.spec.ts │ │ └── details.spec.ts │ └── checkout/ │ └── flow.spec.ts ├── fixtures/ │ └── test-data.ts └── utils/ └── helpers.ts

Custom Fixtures

// fixtures/test-fixtures.ts import { test as base } from '@playwright/test';

type Fixtures = { authenticatedPage: Page; };

export const test = base.extend<Fixtures>({ authenticatedPage: async ({ page }, use) => { await page.goto('/login'); await page.fill('[name="email"]', 'test@example.com'); await page.fill('[name="password"]', 'password'); await page.click('button[type="submit"]'); await page.waitForURL('/dashboard'); await use(page); }, });

// Usage test('authenticated test', async ({ authenticatedPage }) => { await authenticatedPage.goto('/profile'); // Already logged in });

Common Pitfalls

Pitfall Problem Solution

Race conditions Test checks before page updates Use waitFor or expect with retries

Flaky selectors CSS classes change Use data-testid or role selectors

Hard-coded waits page.waitForTimeout(3000)

Wait for specific conditions

Not waiting for hydration JS not executed waitForLoadState('networkidle')

Shared state Tests affect each other Use isolated storage/auth per test

Ignoring errors Uncaught exceptions Check page.on('pageerror')

Checklist

  • Use stable selectors (test IDs, roles, labels)

  • Wait for appropriate conditions (not arbitrary timeouts)

  • Test both happy path and error states

  • Include accessibility checks

  • Test responsive breakpoints

  • Mock external API dependencies

  • Capture screenshots on failure

  • Run tests in CI/CD

  • Keep tests independent

  • Organize tests by feature

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

php

No summary provided by upstream source.

Repository SourceNeeds Review
General

laravel

No summary provided by upstream source.

Repository SourceNeeds Review
General

javascript

No summary provided by upstream source.

Repository SourceNeeds Review
General

email-marketing

No summary provided by upstream source.

Repository SourceNeeds Review