Skip to main content

Playwright Testing Guide: E2E Tests for Next.js Applications

Write reliable end-to-end tests for Next.js applications with Playwright. Complete guide covering setup, page objects, authentication testing, visual regression, and CI/CD integration.

E
ECOSIRE Research and Development Team
|March 5, 20266 min read1.0k Words|

End-to-end tests catch the bugs that unit tests miss -- broken authentication flows, form submission failures, navigation errors, and cross-browser rendering issues.

End-to-end tests catch the bugs that unit tests miss -- broken authentication flows, form submission failures, navigation errors, and cross-browser rendering issues. Playwright, built by Microsoft, has become the leading E2E testing framework with its multi-browser support, auto-wait capabilities, and powerful debugging tools.

Key Takeaways

  • Playwright tests run across Chromium, Firefox, and WebKit from a single test suite
  • Auto-wait eliminates flaky tests by waiting for elements to be actionable before interacting
  • Authentication state can be shared across tests for faster execution
  • CI integration with GitHub Actions provides automated testing on every pull request

Project Setup

Installation

npm init playwright@latest

This installs Playwright, creates a configuration file, and downloads browser binaries. For Next.js projects, configure the web server to start automatically:

// playwright.config.ts
import { defineConfig } from "@playwright/test";

export default defineConfig({
  testDir: "./e2e",
  timeout: 30000,
  retries: process.env.CI ? 2 : 0,
  use: {
    baseURL: "http://localhost:3000",
    screenshot: "only-on-failure",
    trace: "on-first-retry",
  },
  webServer: {
    command: "npm run dev",
    port: 3000,
    reuseExistingServer: !process.env.CI,
  },
  projects: [
    { name: "chromium", use: { browserName: "chromium" } },
    { name: "firefox", use: { browserName: "firefox" } },
    { name: "webkit", use: { browserName: "webkit" } },
  ],
});

Writing Your First Test

Basic Navigation Test

// e2e/navigation.spec.ts
import { test, expect } from "@playwright/test";

test("home page loads and displays title", async ({ page }) => {
  await page.goto("/");
  await expect(page).toHaveTitle(/ECOSIRE/);
  await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
});

test("navigation to services page", async ({ page }) => {
  await page.goto("/");
  await page.getByRole("link", { name: "Services" }).click();
  await expect(page).toHaveURL(/services/);
  await expect(page.getByRole("heading", { name: /Services/ })).toBeVisible();
});

Form Submission Test

test("contact form submission", async ({ page }) => {
  await page.goto("/contact");

  await page.getByLabel("Name").fill("Test User");
  await page.getByLabel("Email").fill("test@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")).toBeVisible();
});

Page Object Pattern

Encapsulate page interactions in page objects for maintainability:

// e2e/pages/login.page.ts
import { Page, expect } from "@playwright/test";

export class LoginPage {
  constructor(private page: Page) {}

  async goto() {
    await this.page.goto("/login");
  }

  async login(email: string, password: string) {
    await this.page.getByLabel("Email").fill(email);
    await this.page.getByLabel("Password").fill(password);
    await this.page.getByRole("button", { name: "Sign In" }).click();
  }

  async expectLoggedIn() {
    await expect(this.page).toHaveURL(/dashboard/);
  }

  async expectError(message: string) {
    await expect(this.page.getByText(message)).toBeVisible();
  }
}

Use in tests:

test("successful login", async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto();
  await loginPage.login("user@example.com", "password123");
  await loginPage.expectLoggedIn();
});

Authentication Testing

Shared Authentication State

Avoid logging in before every test by saving authentication state:

// e2e/auth.setup.ts
import { test as setup } from "@playwright/test";

setup("authenticate", async ({ page }) => {
  await page.goto("/login");
  await page.getByLabel("Email").fill("admin@example.com");
  await page.getByLabel("Password").fill("admin-password");
  await page.getByRole("button", { name: "Sign In" }).click();
  await page.waitForURL("/dashboard");

  // Save authentication state
  await page.context().storageState({ path: ".auth/user.json" });
});

Configure in playwright.config.ts:

projects: [
  { name: "setup", testMatch: /auth\.setup\.ts/ },
  {
    name: "authenticated",
    dependencies: ["setup"],
    use: { storageState: ".auth/user.json" },
  },
],

Visual Regression Testing

Catch unintended visual changes with screenshot comparisons:

test("home page visual regression", async ({ page }) => {
  await page.goto("/");
  await expect(page).toHaveScreenshot("home-page.png", {
    maxDiffPixelRatio: 0.01,
  });
});

On first run, Playwright saves baseline screenshots. Subsequent runs compare against baselines and fail if differences exceed the threshold. Update baselines with --update-snapshots when changes are intentional.


API Mocking

Mock API responses for deterministic testing:

test("displays products from API", async ({ page }) => {
  await page.route("/api/products", async (route) => {
    await route.fulfill({
      status: 200,
      contentType: "application/json",
      body: JSON.stringify([
        { id: 1, name: "Product A", price: 29.99 },
        { id: 2, name: "Product B", price: 49.99 },
      ]),
    });
  });

  await page.goto("/products");
  await expect(page.getByText("Product A")).toBeVisible();
  await expect(page.getByText("Product B")).toBeVisible();
});

CI/CD Integration

GitHub Actions

name: E2E Tests
on: [push, pull_request]
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: npx playwright test
      - uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: playwright-report
          path: playwright-report/

Failed test reports upload as artifacts for debugging, including screenshots, traces, and video recordings.


Debugging Tools

  • Playwright Inspector: Run tests with --debug flag for step-by-step execution
  • Trace Viewer: Open traces to see every action, network request, and DOM snapshot
  • Codegen: Run npx playwright codegen to record browser interactions and generate test code
  • VS Code Extension: Run and debug tests directly from VS Code with click-to-locate elements

Best Practices

  1. Use role-based selectors: Prefer getByRole, getByLabel, getByText over CSS selectors
  2. Avoid hardcoded waits: Trust auto-wait instead of page.waitForTimeout()
  3. Isolate tests: Each test should be independent and not rely on other tests
  4. Test user flows, not implementation: Focus on what users do, not how the code works
  5. Keep tests fast: Use API shortcuts for setup (create test data via API, not UI)
  6. Run in CI: Every pull request should pass E2E tests before merge

Frequently Asked Questions

Q: How does Playwright compare to Cypress?

Playwright supports multiple browsers (Chromium, Firefox, WebKit) natively, while Cypress primarily targets Chromium. Playwright is faster for parallel execution and handles multiple tabs/windows. Cypress has a slightly easier learning curve and stronger time-travel debugging.

Q: How do we handle flaky tests?

Playwright auto-wait eliminates most flakiness. For remaining issues: use web-first assertions (expect locator), avoid testing timing-dependent behavior, and configure retries in CI. Trace reports help identify the root cause of intermittent failures.

Q: Should we test every page?

Focus on critical user flows: authentication, core business workflows, form submissions, and payment flows. Not every page needs E2E testing -- use unit and integration tests for component-level coverage.

Q: How long should E2E tests take?

Individual tests should complete in under 30 seconds. A full suite for a mid-sized application (50-100 tests) should run in under 10 minutes with parallel execution.


What Is Next

E2E testing with Playwright provides confidence that your application works correctly from the user perspective. Start with critical flows and expand coverage as your application grows.

Contact ECOSIRE for testing automation help, or explore our Odoo implementation services for quality-assured ERP deployment.


Published by ECOSIRE -- helping businesses scale with enterprise software solutions.

E

Written by

ECOSIRE Team

Technical Writing

The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.

ECOSIRE

Grow Your Business with ECOSIRE

Enterprise solutions across ERP, eCommerce, AI, analytics, and automation.