Skip to main content
Back to Blog
Tutorials
3 min read
November 15, 2024

How to Set Up Automated Testing with Playwright in Next.js

Write end-to-end tests for your Next.js app with Playwright. Browser automation, visual regression, and CI integration.

Ryel Banfield

Founder & Lead Developer

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.

Playwrighttestinge2eNext.jstutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles