Playwright Testing Guide: E2E Tests for Next.js Applications
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("[email protected]");
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("[email protected]", "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("[email protected]");
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
--debugflag for step-by-step execution - Trace Viewer: Open traces to see every action, network request, and DOM snapshot
- Codegen: Run
npx playwright codegento 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
- Use role-based selectors: Prefer getByRole, getByLabel, getByText over CSS selectors
- Avoid hardcoded waits: Trust auto-wait instead of page.waitForTimeout()
- Isolate tests: Each test should be independent and not rely on other tests
- Test user flows, not implementation: Focus on what users do, not how the code works
- Keep tests fast: Use API shortcuts for setup (create test data via API, not UI)
- 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.
Written by
ECOSIRE TeamTechnical 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.
Related Articles
Odoo 19 Accounting: 8 New Features That Change Daily Workflows
Deep-dive into Odoo 19 accounting: AI bank reconciliation, redesigned tax engine, lock-date workflow, audit trail, payment matching, CFO dashboard.
Odoo CI/CD with GitHub Actions: Testing and Deployment
Build a production Odoo CI/CD pipeline with GitHub Actions: linting, runbot-style testing, multi-version matrix, staging deploy, zero-downtime production.
Odoo Tests: TransactionCase, HttpCase, Tags, post_install
Practical Odoo testing guide: TransactionCase vs HttpCase vs SavepointCase, test tags, post_install timing, tour tests, mocking, CI integration.