Comprehensive guide for modern front-end testing with Playwright (2025)...
To build reliable, maintainable end-to-end tests for modern web applications, follow Playwright best practices with TypeScript, Page Object Model patterns, and automated CI/CD integration. This skill provides comprehensive guidance for testing SPAs (React, Vue, Angular) and traditional web applications using Playwright's powerful automation capabilities.
Current Playwright Version: 1.48+ (as of 2025) Language: TypeScript with strict typing Test Runner: Playwright Test (@playwright/test) CI/CD: GitHub Actions, Docker support Standard Patterns: Page Object Model, Fixtures, Component Testing
User Request → What type of test are you creating?
|
├─ End-to-end user flow test?
│ ├─ Single page interaction? → Use Quick Start: Basic Test
│ └─ Multi-page workflow? → Use Page Object Model Pattern
│
├─ Component testing in isolation?
│ └─ Load references/component-testing-guide.md
│
├─ Visual regression testing?
│ └─ Load references/visual-testing-guide.md
│
├─ Setting up new Playwright project?
│ └─ Run scripts/init-playwright.ts
│
├─ Creating reusable page objects?
│ ├─ Run scripts/generate-page-object.ts for scaffolding
│ └─ Load references/page-object-patterns.md for patterns
│
├─ Debugging flaky tests?
│ └─ Load references/anti-patterns.md
│
├─ Optimizing test selectors?
│ └─ Load references/locator-strategies.md
│
└─ Setting up CI/CD pipeline?
└─ Use assets/workflows/playwright-tests.yml
import { test, expect } from '@playwright/test';
test('user can login successfully', async ({ page }) => {
// Navigate to application
await page.goto('https://app.example.com');
// Interact with elements using recommended locators
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('Password').fill('SecurePass123');
await page.getByRole('button', { name: 'Sign in' }).click();
// Assert expected outcomes
await expect(page).toHaveURL('/dashboard');
await expect(page.getByRole('heading', { name: 'Welcome back' })).toBeVisible();
});
import { test, expect } from '@playwright/test';
test.describe('Shopping Cart', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/products');
});
test('add item to cart', async ({ page }) => {
// Find and click product
const product = page.getByRole('article').filter({ hasText: 'Premium Headphones' });
await product.getByRole('button', { name: 'Add to cart' }).click();
// Verify cart updated
await expect(page.getByTestId('cart-count')).toHaveText('1');
// Navigate to cart
await page.getByRole('link', { name: 'Cart' }).click();
// Verify item in cart
await expect(page.getByRole('heading', { name: 'Premium Headphones' })).toBeVisible();
await expect(page.getByText('$199.99')).toBeVisible();
});
});
Follow this structured approach to build maintainable test suites for your application.
To begin testing effectively:
Initialize your Playwright project with proper TypeScript configuration:
# Run initialization script
npx tsx scripts/init-playwright.ts
This creates:
Create page objects for reusable, maintainable tests:
// pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
readonly errorMessage: 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' });
this.errorMessage = page.getByRole('alert');
}
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();
}
async expectError(message: string) {
await expect(this.errorMessage).toContainText(message);
}
}
Use the page object generator for scaffolding:
npx tsx scripts/generate-page-object.ts --url="/login" --name="LoginPage"
Structure tests using Given-When-Then pattern:
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test.describe('Authentication', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
});
test('successful login redirects to dashboard', async ({ page }) => {
// Given: Valid user credentials
const email = 'user@example.com';
const password = 'ValidPass123';
// When: User attempts to login
await loginPage.login(email, password);
// Then: User is redirected to dashboard
await expect(page).toHaveURL('/dashboard');
});
test('invalid credentials show error', async () => {
// When: User enters invalid credentials
await loginPage.login('bad@email.com', 'wrong');
// Then: Error message is displayed
await loginPage.expectError('Invalid email or password');
});
});
Add visual regression tests for UI consistency:
import { test, expect } from '@playwright/test';
test('homepage visual consistency', async ({ page }) => {
await page.goto('/');
// Full page screenshot
await expect(page).toHaveScreenshot('homepage.png', {
fullPage: true,
animations: 'disabled'
});
// Component screenshot
const header = page.getByRole('banner');
await expect(header).toHaveScreenshot('header.png');
});
Update baselines when intentional changes are made:
npx tsx scripts/visual-baseline.ts --update
Deploy the GitHub Actions workflow for automated testing:
# .github/workflows/playwright.yml
name: Playwright Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
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: npm run test:e2e
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
Write tests from the user's perspective:
// ✅ Good: Tests user-visible behavior
await page.getByRole('button', { name: 'Submit order' }).click();
await expect(page.getByText('Order confirmed')).toBeVisible();
// ❌ Bad: Tests implementation details
await page.locator('#submit-btn-2947').click();
await expect(page.locator('.success-msg-div')).toHaveClass('visible');
Prefer accessible, semantic locators that won't break with styling changes:
// Priority order for locators:
// 1. User-facing attributes
await page.getByRole('button', { name: 'Save' });
await page.getByLabel('Email address');
await page.getByPlaceholder('Search products...');
await page.getByText('Welcome back');
// 2. Test IDs (when needed for complex elements)
await page.getByTestId('product-card-12345');
// 3. CSS/XPath (avoid when possible)
await page.locator('css=.dynamic-content'); // Last resort
Each test should run independently:
test.describe('User Profile', () => {
// Create fresh user for each test
test.beforeEach(async ({ page, request }) => {
const user = await createTestUser(request);
await authenticateUser(page, user);
});
test.afterEach(async ({ request }, testInfo) => {
// Cleanup test data
await cleanupTestUser(request, testInfo);
});
test('update profile name', async ({ page }) => {
// Test runs with isolated user
});
});
Playwright auto-waits, but be explicit when needed:
// Auto-wait handles most cases
await page.getByRole('button', { name: 'Load data' }).click();
await expect(page.getByTestId('data-table')).toBeVisible();
// Explicit wait for specific conditions
await page.waitForResponse(resp =>
resp.url().includes('/api/data') && resp.status() === 200
);
// Wait for network idle
await page.waitForLoadState('networkidle');
Configure retries for flaky scenarios, but investigate root causes:
// playwright.config.ts
export default defineConfig({
retries: process.env.CI ? 2 : 0,
use: {
// Retry navigation on failure
navigationTimeout: 30000,
actionTimeout: 15000,
},
});
// Per-test retry configuration
test('flaky integration test', async ({ page }) => {
test.info().annotations.push({
type: 'flaky',
description: 'External API occasionally slow'
});
// Test implementation
});
// Wait for React to hydrate
await page.waitForFunction(() => window.React && window.React.version);
// Test React components
const component = page.getByTestId('user-list');
await expect(component).toBeVisible();
await expect(component.locator('[data-testid="user-item"]')).toHaveCount(5);
// Wait for Vue app to mount
await page.waitForFunction(() => window.Vue && document.querySelector('#app').__vue__);
// Interact with Vue components
await page.getByRole('button', { name: 'Toggle' }).click();
await expect(page.getByTestId('toggle-state')).toHaveText('active');
// Wait for Angular to bootstrap
await page.waitForFunction(() => window.getAllAngularTestabilities);
// Test Angular forms
await page.getByLabel('Username').fill('testuser');
await page.getByLabel('Email').fill('test@example.com');
await expect(page.getByRole('button', { name: 'Submit' })).toBeEnabled();
export async function authenticateUser(page: Page, credentials: UserCredentials) {
await page.goto('/login');
await page.getByLabel('Email').fill(credentials.email);
await page.getByLabel('Password').fill(credentials.password);
await page.getByRole('button', { name: 'Sign in' }).click();
await page.waitForURL('/dashboard');
}
await page.route('**/api/users', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{ id: 1, name: 'Test User' }])
});
});
const fileInput = page.getByLabel('Upload file');
await fileInput.setInputFiles('path/to/file.pdf');
await expect(page.getByText('file.pdf')).toBeVisible();
import { injectAxe, checkA11y } from 'axe-playwright';
test('page is accessible', async ({ page }) => {
await page.goto('/');
await injectAxe(page);
await checkA11y(page, null, {
detailedReport: true,
detailedReportOptions: { html: true }
});
});
This skill includes comprehensive reference materials and templates:
Advanced Page Object Model patterns including fixtures, page managers, composition patterns, and enterprise-scale organization strategies.
Load when: Implementing page objects, organizing large test suites, or creating reusable test components.
Guide for testing UI components in isolation for React, Vue, and Angular applications, including mounting strategies and state management.
Load when: Testing individual components, setting up component test harnesses, or testing component interactions.
Visual regression testing strategies including screenshot comparison, viewport testing, cross-browser visual testing, and baseline management.
Load when: Implementing visual tests, managing screenshot baselines, or debugging visual differences.
Modern selector strategies for reliable element location, including auto-waiting patterns, shadow DOM traversal, and dynamic content handling.
Load when: Writing resilient selectors, debugging flaky locators, or handling complex DOM structures.
Common Playwright testing mistakes and how to avoid them, including test coupling, hard-coded waits, and improper assertion patterns.
Load when: Reviewing test quality, debugging failures, or training team members on best practices.
To optimize context usage, load reference files strategically:
Load page-object-patterns.md when:
Load component-testing-guide.md when:
Load visual-testing-guide.md when:
Load locator-strategies.md when:
Load anti-patterns.md when: