Playwright runs end-to-end tests in real browsers. It catches bugs that unit tests miss: broken forms, navigation issues, visual regressions. Here is how to set it up.
Step 1: Install Playwright
pnpm add -D @playwright/test
npx playwright install
Step 2: Configure Playwright
// playwright.config.ts
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./e2e",
timeout: 30_000,
expect: {
timeout: 5_000,
},
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [["html", { open: "never" }]],
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: "pnpm dev",
url: "http://localhost:3000",
reuseExistingServer: !process.env.CI,
},
});
Step 3: Write Your First Test
// e2e/home.spec.ts
import { test, expect } from "@playwright/test";
test.describe("Home Page", () => {
test("has title and heading", async ({ page }) => {
await page.goto("/");
await expect(page).toHaveTitle(/YourBrand/);
await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
});
test("navigation links work", async ({ page }) => {
await page.goto("/");
await page.getByRole("link", { name: "Services" }).click();
await expect(page).toHaveURL(/services/);
});
test("CTA button is visible", async ({ page }) => {
await page.goto("/");
const cta = page.getByRole("link", { name: /get started/i });
await expect(cta).toBeVisible();
});
});
Step 4: Test Forms
// e2e/contact.spec.ts
import { test, expect } from "@playwright/test";
test.describe("Contact Form", () => {
test("submits successfully", async ({ page }) => {
await page.goto("/contact");
await page.getByLabel("Name").fill("John Doe");
await page.getByLabel("Email").fill("john@example.com");
await page.getByLabel("Phone").fill("1234567890");
await page.getByLabel("Message").fill("I need a website.");
await page.getByRole("button", { name: /send/i }).click();
await expect(page.getByText(/thank you/i)).toBeVisible();
});
test("shows validation errors", async ({ page }) => {
await page.goto("/contact");
// Submit without filling
await page.getByRole("button", { name: /send/i }).click();
await expect(page.getByText(/required/i).first()).toBeVisible();
});
test("validates email format", async ({ page }) => {
await page.goto("/contact");
await page.getByLabel("Email").fill("not-an-email");
await page.getByLabel("Email").blur();
await expect(page.getByText(/invalid email/i)).toBeVisible();
});
});
Step 5: Test Navigation and Routing
// e2e/navigation.spec.ts
import { test, expect } from "@playwright/test";
test.describe("Navigation", () => {
test("mobile menu opens and closes", async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 });
await page.goto("/");
// Menu should be hidden
const menu = page.getByRole("navigation", { name: "Mobile menu" });
await expect(menu).not.toBeVisible();
// Open menu
await page.getByRole("button", { name: /menu/i }).click();
await expect(menu).toBeVisible();
// Close menu
await page.getByRole("button", { name: /close/i }).click();
await expect(menu).not.toBeVisible();
});
test("breadcrumbs show correct path", async ({ page }) => {
await page.goto("/services/web-design");
const breadcrumbs = page.getByRole("navigation", { name: "Breadcrumb" });
await expect(breadcrumbs.getByText("Home")).toBeVisible();
await expect(breadcrumbs.getByText("Services")).toBeVisible();
await expect(breadcrumbs.getByText("Web Design")).toBeVisible();
});
test("404 page shows for invalid routes", async ({ page }) => {
const response = await page.goto("/this-page-does-not-exist");
expect(response?.status()).toBe(404);
await expect(page.getByText(/page not found/i)).toBeVisible();
});
});
Step 6: Visual Regression Testing
// e2e/visual.spec.ts
import { test, expect } from "@playwright/test";
test.describe("Visual Regression", () => {
test("home page matches snapshot", async ({ page }) => {
await page.goto("/");
await expect(page).toHaveScreenshot("home.png", {
fullPage: true,
maxDiffPixelRatio: 0.01,
});
});
test("pricing page matches snapshot", async ({ page }) => {
await page.goto("/pricing");
await expect(page).toHaveScreenshot("pricing.png", {
fullPage: true,
maxDiffPixelRatio: 0.01,
});
});
});
Update snapshots:
npx playwright test --update-snapshots
Step 7: Test Accessibility
// e2e/accessibility.spec.ts
import { test, expect } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";
test.describe("Accessibility", () => {
test("home page has no violations", async ({ page }) => {
await page.goto("/");
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
test("contact page has no violations", async ({ page }) => {
await page.goto("/contact");
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
});
Install the axe plugin:
pnpm add -D @axe-core/playwright
Step 8: Run Tests
# Run all tests
npx playwright test
# Run specific file
npx playwright test e2e/home.spec.ts
# Run in headed mode (see the browser)
npx playwright test --headed
# Run specific browser
npx playwright test --project=chromium
# View test report
npx playwright show-report
Add to package.json:
{
"scripts": {
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui"
}
}
Step 9: GitHub Actions CI
# .github/workflows/e2e.yml
name: E2E Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- run: pnpm install
- run: npx playwright install --with-deps
- run: pnpm build
- run: pnpm test:e2e
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
Need Quality Assurance for Your App?
We build web applications with comprehensive testing — unit, integration, and end-to-end. Contact us for reliable software.