Skip to content

djproject-id/Playwright-E2E-Template

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Playwright E2E Testing Template

Reusable, battle-tested template for Playwright end-to-end testing. Works with any web framework — Next.js, Vite, Remix, Nuxt, SvelteKit, and more.

Built from real-world experience testing a production Next.js 16 app with 38 pages, 60+ API routes, 5 roles, and 132 test cases.

Features

  • Role-based auth — Login once per role in global setup, reuse across all tests
  • Retry-resilient auth — 3-attempt retry logic for flaky server cold starts
  • SSR-aware — Wait utilities that handle hydration timing in SSR frameworks
  • Framework-agnostic — Config supports Next.js, Vite, Remix, Nuxt, SvelteKit, CRA
  • Battle-tested patterns — Fixes for .or() strict mode, SSR blank pages, concurrency issues
  • CI/CD ready — GitHub Actions workflow included, GitLab CI example in docs
  • Markdown reporter — Optional custom reporter for Markdown test summaries

Quick Start

# 1. Copy to your project
cd your-project
bash /path/to/Playwright-E2E-Template/setup.sh

# 2. Configure (3 files to edit)
#    e2e/fixtures/test-data.ts   → users, routes, credentials
#    e2e/helpers/auth.ts         → login page selectors
#    e2e/playwright.config.ts    → port, build command

# 3. Run tests
npm run test:e2e

Manual Setup

# Install
npm install -D @playwright/test       # or pnpm / yarn / bun
npx playwright install chromium

# Copy template
cp -r Playwright-E2E-Template/e2e/ your-project/e2e/

# Add to package.json scripts:
# "test:e2e": "npx playwright test --config=e2e/playwright.config.ts"

Project Structure

e2e/
├── playwright.config.ts          # Config: port, workers, timeouts, webServer
├── global-setup.ts               # Pre-authenticates all roles (with retry)
│
├── fixtures/
│   ├── test-data.ts              # Users, routes, constants (EDIT THIS FIRST)
│   └── base.fixture.ts           # Extended test fixture with role pages
│
├── helpers/
│   ├── auth.ts                   # Login/logout helpers (EDIT FOR YOUR APP)
│   ├── selectors.ts              # Centralized DOM selectors
│   ├── navigation.ts             # SPA navigation with SSR waits
│   ├── form.ts                   # Form fill, select, upload, submit
│   ├── wait.ts                   # Loading, skeleton, toast, SSR waiters
│   ├── api.ts                    # Direct API call helpers
│   └── markdown-reporter.ts      # Custom Markdown report generator
│
├── auth/                         # Saved auth states (gitignored)
│   └── *.json
│
└── tests/
    ├── _example-auth.spec.ts     # Login, logout, session, redirect
    ├── _example-crud.spec.ts     # List, create, edit, delete
    ├── _example-form.spec.ts     # Validation, submit, cancel
    ├── _example-navigation.spec.ts # Sidebar, heading, header
    └── _example-rbac.spec.ts     # Role-based access control

Configuration

3 Files to Edit

File What to Change
e2e/fixtures/test-data.ts User credentials, roles, app routes
e2e/helpers/auth.ts Login page selectors, post-login redirect URL
e2e/playwright.config.ts Port, build + start command

Environment Variables

Variable Default Description
E2E_PORT 3000 App port
E2E_BASE_URL http://localhost:3000 Full base URL
E2E_SERVER_CMD npm run build && npm run start Build + serve command
CI Set automatically in CI; reduces workers, increases retries

Framework Commands

Framework Port E2E_SERVER_CMD
Next.js 3000 npm run build && npm run start
Vite 4173 npm run build && npm run preview
Remix 3000 npm run build && npm run start
Nuxt 3 3000 npm run build && npm run preview
SvelteKit 4173 npm run build && npm run preview
CRA 3000 npm run build && npx serve -s build

How It Works

Auth Strategy

Global Setup (runs once before all tests)
  ├── Launch browser
  ├── For each role in USERS:
  │   ├── Open login page
  │   ├── Fill email + password
  │   ├── Click submit
  │   ├── Wait for redirect
  │   ├── Save cookies to e2e/auth/{role}.json
  │   └── Retry up to 3 times on failure
  └── Close browser

Tests (run in parallel)
  ├── test.use({ storageState: USERS.admin.storageState })
  │   → Browser loads with admin's saved cookies
  │   → No login needed — test starts on any page
  └── ...

Two Ways to Use Auth in Tests

Option A: Per-describe block (recommended)

test.describe('Admin features', () => {
  test.use({ storageState: USERS.admin.storageState });

  test('should see admin panel', async ({ page }) => {
    await page.goto('/admin');
    // page is already authenticated as admin
  });
});

Option B: Named fixture pages (for multi-role tests)

test('admin and user see different data', async ({ adminPage, userPage }) => {
  await adminPage.goto('/dashboard');
  await userPage.goto('/dashboard');
  // Each page has its own authenticated context
});

Writing Tests

Start from Examples

Copy _example-*.spec.ts files and rename:

cp e2e/tests/_example-crud.spec.ts e2e/tests/products.spec.ts

Test Pattern

import { test, expect } from '../fixtures/base.fixture';
import { USERS, ROUTES } from '../fixtures/test-data';
import { waitForPageLoad, waitForToast } from '../helpers/wait';
import { navigateTo } from '../helpers/navigation';

test.describe('Products', () => {
  test.use({ storageState: USERS.admin.storageState });

  test('should display product list', async ({ page }) => {
    await navigateTo(page, '/products');
    await waitForPageLoad(page);

    // Assert content is visible
    const table = page.locator('table').first();
    await expect(table).toBeVisible();
  });

  test('should create product', async ({ page }) => {
    await navigateTo(page, '/products/new');
    await page.getByLabel('Name').fill('Test Product');
    await page.getByLabel('Price').fill('99.99');
    await page.locator('button[type="submit"]').click();
    await waitForToast(page, /success|created/i);
  });
});

Skip Guard Pattern

For tests that depend on data that might not exist:

test('should edit item', async ({ page }) => {
  await navigateTo(page, '/items');
  const editBtn = page.getByRole('button', { name: /edit/i }).first();

  if (!(await editBtn.isVisible().catch(() => false))) {
    test.skip(true, 'No items available to edit');
    return;
  }

  await editBtn.click();
  // ... continue test
});

npm Scripts

Add to your package.json:

{
  "scripts": {
    "test:e2e": "npx playwright test --config=e2e/playwright.config.ts",
    "test:e2e:ui": "npx playwright test --config=e2e/playwright.config.ts --ui",
    "test:e2e:headed": "npx playwright test --config=e2e/playwright.config.ts --headed",
    "test:e2e:debug": "npx playwright test --config=e2e/playwright.config.ts --debug",
    "test:e2e:report": "npx playwright show-report playwright-report"
  }
}

Running Specific Tests

# Single file
npm run test:e2e -- auth

# By grep pattern
npm run test:e2e -- -g "should login"

# Headed (see the browser)
npm run test:e2e:headed

# Debug mode (step through)
npm run test:e2e:debug

# UI mode (interactive)
npm run test:e2e:ui

Best Practices

Selectors

// Prefer (most resilient → least resilient):
page.getByRole("button", { name: "Save" })    // ARIA role
page.getByLabel("Email")                        // Form label
page.getByTestId("submit-btn")                  // data-testid
page.locator('button[type="submit"]')           // CSS (last resort)

Avoid .or() Strict Mode Violations

// BAD — throws if both match
await expect(page.locator("h1").or(page.locator("main"))).toBeVisible();

// GOOD — always add .first()
const hasHeading = await page.locator("h1").first().isVisible().catch(() => false);
const hasMain = await page.locator("main").first().isVisible().catch(() => false);
expect(hasHeading || hasMain).toBeTruthy();

Idempotent Tests

// BAD — state accumulates across runs
const name = currentName + " Updated";  // → "Name Updated Updated Updated..."

// GOOD — fixed value, same result every run
const name = "Test Company";

SSR Pages

// Always use networkidle for SSR frameworks
await page.goto("/dashboard", { waitUntil: "networkidle" });

// Wait for main content (handles slow hydration)
await page.locator("main").first()
  .waitFor({ state: "visible", timeout: 20_000 })
  .catch(() => {});

.gitignore

Add these entries:

/playwright-report/
/test-results/
/e2e/auth/*.json

Documentation

Guide Description
docs/ADAPTING.md Step-by-step adaptation for different frameworks, auth systems, UI libraries
docs/TROUBLESHOOTING.md Common issues and solutions from real-world usage
docs/CI-CD.md GitHub Actions, GitLab CI setup with examples

CI/CD

A GitHub Actions workflow is included at .github/workflows/e2e.yml. Copy it to your project:

cp -r Playwright-E2E-Template/.github/ your-project/.github/

See docs/CI-CD.md for GitLab CI and other platforms.


License

MIT

About

Battle-tested, framework-agnostic Playwright E2E testing template — 132 test cases, multi-role auth, SSR-aware

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors