End-to-End Testing: Complete Guide with Playwright
End-to-end (E2E) testing automates real browser interactions to verify that your application works correctly from the user’s perspective — clicking buttons, filling forms, navigating pages, and checking that the right things happen at each step.
What You’ll Learn
- What E2E testing is and when to use it over unit or integration tests
- Setting up Playwright and writing your first browser test
- Using assertions, the Page Object Model, and visual regression
- Integrating E2E tests into your CI pipeline
Why E2E Testing Matters
Unit tests verify individual functions. Integration tests verify component interactions. But only E2E tests verify that the user’s journey works — from login to checkout to logout. E2E tests catch regressions that no other test type can: broken navigation, JavaScript errors in real browsers, CSS layout issues, and API integration failures.
Doda Browser uses Playwright E2E tests to verify that every browser feature works correctly across Chrome, Firefox, and WebKit before each release.
Learning Path
flowchart LR
A[Unit Testing] --> B[Integration Testing]
B --> C[E2E Testing<br/>You are here]
C --> D[CI Integration]
D --> E[Visual Regression]
style C fill:#f90,color:#fff
Playwright Setup
# Install Playwright
npm init playwright@latest
# Or add to existing project
npm install @playwright/test
npx playwright installExpected output:
✔ Installed Playwright Test
✔ Created playwright.config.ts
✔ Created example tests in tests/
✔ Installed browsers: Chromium, Firefox, WebKitConfiguration
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 4 : undefined,
reporter: [['html'], ['list']],
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'] } },
],
});Writing Your First Test
// e2e/login.spec.ts
import { test, expect } from '@playwright/test';
test('user can log in successfully', async ({ page }) => {
// Arrange: navigate to login page
await page.goto('/login');
// Act: fill form and submit
await page.fill('[data-testid="email"]', 'user@example.com');
await page.fill('[data-testid="password"]', 'correct-password');
await page.click('[data-testid="login-button"]');
// Assert: redirected to dashboard
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('[data-testid="welcome"]')).toContainText('Welcome, User');
});
test('shows error on invalid credentials', async ({ page }) => {
await page.goto('/login');
await page.fill('[data-testid="email"]', 'wrong@example.com');
await page.fill('[data-testid="password"]', 'wrong-password');
await page.click('[data-testid="login-button"]');
await expect(page.locator('[data-testid="error-message"]')).toBeVisible();
await expect(page.locator('[data-testid="error-message"]')).toContainText(
'Invalid email or password'
);
});Expected test output
Running 2 tests using 2 workers
✓ e2e/login.spec.ts:3:5 › user can log in successfully (4.2s)
✓ e2e/login.spec.ts:18:5 › shows error on invalid credentials (3.1s)
2 passed (7.5s)Assertions
Playwright provides auto-retrying assertions that wait for conditions to be met:
// Visibility and existence
await expect(page.locator('.loading-spinner')).toBeVisible();
await expect(page.locator('.error-message')).toBeHidden();
await expect(page.locator('#submit-btn')).toBeEnabled();
await expect(page.locator('#submit-btn')).toBeDisabled();
// Text content
await expect(page.locator('h1')).toHaveText('Dashboard');
await expect(page.locator('.price')).toContainText('$19.99');
// Form values
await expect(page.locator('#email')).toHaveValue('user@example.com');
// Count
await expect(page.locator('.todo-item')).toHaveCount(5);
// URL and attributes
await expect(page).toHaveURL(/\/dashboard/);
await expect(page.locator('img')).toHaveAttribute('alt', 'Profile photo');Page Object Model
The Page Object Model (POM) encapsulates page-specific selectors and actions into reusable classes:
// e2e/pages/LoginPage.ts
import { Page } from '@playwright/test';
export class LoginPage {
constructor(private page: Page) {}
// Locators
private emailInput = '[data-testid="email"]';
private passwordInput = '[data-testid="password"]';
private loginButton = '[data-testid="login-button"]';
private errorMessage = '[data-testid="error-message"]';
// Actions
async goto() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.page.fill(this.emailInput, email);
await this.page.fill(this.passwordInput, password);
await this.page.click(this.loginButton);
}
// Assertions
async expectError(message: string) {
await expect(this.page.locator(this.errorMessage)).toContainText(message);
}
}// e2e/login-pom.spec.ts
import { test } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
test('user can log in with POM', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'correct-password');
await expect(page).toHaveURL('/dashboard');
});CI Integration
# .github/workflows/e2e.yml
name: E2E Tests
on: [deployment_status]
jobs:
e2e:
if: github.event.deployment_status.state == 'success'
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
env:
BASE_URL: ${{ github.event.deployment_status.environment_url }}
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/Visual Regression Testing
Playwright can compare screenshots to detect visual changes:
test('homepage matches snapshot', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot('homepage.png', {
maxDiffPixels: 100,
});
});
test('checkout form renders correctly', async ({ page }) => {
await page.goto('/checkout');
await page.fill('#card-number', '4242424242424242');
await expect(page.locator('.payment-form')).toHaveScreenshot();
});Run npx playwright test --update-snapshots to update reference screenshots when intentional changes occur.
Common Errors
1. Flaky Tests from Timing Issues
Elements not loaded when Playwright tries to interact. Use auto-retrying assertions and waitFor instead of fixed delays.
// BAD: fixed wait
await page.waitForTimeout(2000);
// GOOD: wait for element
await page.waitForSelector('.table-loaded');2. Using CSS Classes for Locators
CSS classes change frequently. Use data-testid attributes for test selectors.
3. Tests Pass Locally but Fail in CI
Environment differences (screen size, timeouts, base URL). Use the same config in both environments. Set CI-specific timeouts.
4. Not Handling Authentication
Tests that require login should handle auth once. Use page.context().storageState() to save and restore sessions.
5. Running Too Many Tests in Parallel
Headless browsers consume memory. Limit workers based on CI runner specs.
6. Ignoring Test Failures
Failed E2E tests almost always indicate real bugs. Investigate every failure using traces and screenshots.
Practice Questions
What does E2E testing verify that other test types don’t? Real user journeys across pages, including navigation, JavaScript execution, CSS rendering, and API integration.
What is the Page Object Model? A pattern that encapsulates page-specific selectors and actions into reusable classes.
How does Playwright handle dynamic content? Auto-retrying assertions wait for conditions to be met within a configurable timeout.
What is visual regression testing? Comparing screenshots of pages/components to detect unintended visual changes.
How do you avoid flaky tests? Use auto-retrying assertions,
data-testidselectors, and proper waits instead of fixed timeouts.
Challenge: Write a complete E2E test suite for a login page plus a dashboard page. Include: successful login, failed login with error message, logout, and a visual regression snapshot. Use the Page Object Model pattern.
FAQ
What’s Next
| Tutorial | What You’ll Learn |
|---|---|
| Playwright Testing Guide | Deep dive into Playwright features |
| Integration Testing Guide | Testing service and API interactions |
| CI/CD Pipeline Guide | Automating tests in your pipeline |
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro. Updated 2026-06-19.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro