404 Not Found

404 Not Found


nginx

E2E Testing: Playwright

E2E testing is an "end-to-end simulation" that mimics real user interactions—it tells you exactly how well your app works in a browser.

1. What You'll Learn


2. A True Story of a Test Engineer

(1) Pain Point: Incomplete manual test coverage and frequent production incidents

Diana is a test engineer on the TaskFlow team. The team maintains a SaaS platform serving more than 10,000 users, and before each release, three QA engineers must manually test more than 200 test cases—a process that takes two full days.

In last week's release, the "Edit Project Name" feature worked fine in Chrome, but the input field couldn't be focused in Safari. The QA team only tested it in Chrome, and after the release, they received more than 50 complaints.

The problem with manual testing is obvious:

Issue Impact
Incomplete browser coverage Tested only on Chrome; Safari and Firefox were omitted
Long development cycle Each release requires 2 days of manual testing
Inconsistent results Different testers use different methods
Unable to reproduce Need to retest after the bug is fixed

(2) Playwright E2E Solution

Diana introduced Playwright to replace manual operations with code:

TS
test('user can create a project', async ({ page }) => {
  await page.goto('/projects')
  await page.getByRole('button', { name: 'New Project' }).click()
  await page.getByPlaceholder('Project name').fill('E-commerce App')
  await page.getByRole('button', { name: 'Create' }).click()
  await expect(page.getByText('E-commerce App')).toBeVisible()
})

(3) Revenue

Dimension Manual Testing Playwright E2E
Test Duration 2 days 15 minutes
Browser Coverage Chrome Only Chrome + Firefox + Safari + Edge
Restore Cycle Full Restore Each Time Incremental Restore Each Time + CI Automatic
Bug Escape Rate 15% < 2%
Test Repeatability Low (human variation) High (100% consistency)

3. Setting Up the Playwright Environment

Playwright is a cross-browser automated testing framework maintained by Microsoft that supports Chromium, Firefox, and WebKit.

100%
graph TB
    A[playwright.config.ts] --> B[webServer Layout]
    A --> C[Browser Settings]
    A --> D[Test Directory]
    B --> E[Automatic Startup npm run dev]
    B --> F[Listening on a port 3000 Available]
    C --> G[Chromium / Firefox / WebKit]
    C --> H[Viewport 1280x720]
    
    style A fill:#cce5ff
    style B fill:#d4edda
Configuration File Purpose Key Options
playwright.config.ts E2E Main Configuration webServer Automated Lifecycle Management
playwright/index.html Component Testing Portal ct.target Configuration
.github/workflows/playwright.yml CI Integration npx playwright install --with-deps

(1) Installation and Initialization

BASH
npm init playwright@latest
# Select:
#   ✓ TypeScript
#   ✓ E2E tests
#   ✓ Add GitHub Actions workflow
#   ✓ Install browsers (Chromium, Firefox, WebKit)

(2) playwright.config.ts Core Configuration

TS
import { defineConfig, devices } from '@playwright/test'

export default defineConfig({
  testDir: './e2e',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: [
    ['html'],
    ['json', { outputFile: 'playwright-report/results.json' }]
  ],
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure'
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] }
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] }
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] }
    }
  ],
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
    timeout: 30000
  }
})

(3) Directory Structure

TEXT
├── e2e/
│   ├── auth.setup.ts        # Authentication Preprocessing
│   ├── models/
│   │   ├── LoginPage.ts     # Login Page POM
│   │   ├── ProjectPage.ts   # Project Page POM
│   │   └── TaskPage.ts      # Tasks Page POM
│   ├── navigation.spec.ts   # Navigation Test
│   ├── auth.spec.ts         # Certification Testing
│   ├── projects.spec.ts     # Project CRUD
│   └── visual.spec.ts       # Visual Regression
├── playwright.config.ts
├── playwright-report/
└── test-results/

▶ Example: Verifying that Playwright is running

TS
// e2e/example.spec.ts
import { test, expect } from '@playwright/test'

test('homepage has correct title', async ({ page }) => {
  await page.goto('/')
  await expect(page).toHaveTitle(/TaskFlow/)
  await expect(page.locator('h1')).toContainText('Welcome')
})
BASH
npx playwright test --project=chromium --headed
💻 Output:

TEXT
Running 1 test using 1 worker
  ✓ e2e/example.spec.ts:3:1 › homepage has correct title (2.3s)

Output:

TEXT
Running 1 test using 1 worker
  ✓ e2e/example.spec.ts:3:1 › homepage has correct title (2.3s)

  1 passed (2.3s)

4. Page Object Model (POM) Design Pattern

POM encapsulates page interaction logic into separate classes, so the test code focuses only on "what to do" rather than "how to do it."

100%
graph TB
    A[Test Cases] --> B[Page Object Layer]
    B --> C[LoginPage]
    B --> D[ProjectsPage]
    B --> E[TaskPage]
    C --> F[Browser API]
    D --> F
    E --> F
    
    style B fill:#cce5ff
    style C fill:#d4edda
    style D fill:#d4edda
    style E fill:#d4edda

(1) LoginPage POM Implementation

TS
// e2e/models/LoginPage.ts
import { type Page, type Locator } from '@playwright/test'

export class LoginPage {
  readonly page: Page
  readonly emailInput: Locator
  readonly passwordInput: Locator
  readonly submitButton: Locator
  readonly errorMessage: Locator

  constructor(page: Page) {
    this.page = page
    this.emailInput = page.getByPlaceholder('Enter your email address')
    this.passwordInput = page.getByPlaceholder('Enter your password')
    this.submitButton = page.getByRole('button', { name: 'Log In' })
    this.errorMessage = page.getByTestId('login-error')
  }

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

  async login(email: string, password: string) {
    await this.emailInput.fill(email)
    await this.passwordInput.fill(password)
    await this.submitButton.click()
  }

  async expectError(message: string) {
    await expect(this.errorMessage).toContainText(message)
  }

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

(2) ProjectsPage POM Implementation

TS
// e2e/models/ProjectsPage.ts
import { type Page, type Locator } from '@playwright/test'

export class ProjectsPage {
  readonly page: Page
  readonly newProjectButton: Locator
  readonly projectNameInput: Locator
  readonly createButton: Locator
  readonly projectList: Locator
  readonly searchInput: Locator

  constructor(page: Page) {
    this.page = page
    this.newProjectButton = page.getByRole('button', { name: 'New Project' })
    this.projectNameInput = page.getByPlaceholder('Project name')
    this.createButton = page.getByRole('button', { name: 'Create' })
    this.projectList = page.getByTestId('project-list')
    this.searchInput = page.getByPlaceholder('Search Items')
  }

  async goto() {
    await this.page.goto('/projects')
  }

  async createProject(name: string) {
    await this.newProjectButton.click()
    await this.projectNameInput.fill(name)
    await this.createButton.click()
  }

  async searchProject(name: string) {
    await this.searchInput.fill(name)
  }

  async expectProjectVisible(name: string) {
    await expect(this.projectList).toContainText(name)
  }

  async openProject(name: string) {
    await this.page.getByRole('link', { name }).first().click()
  }
}

▶ Example: POM-Driven Testing

Output:

TEXT
Defines TypeScript type(s): Page, Locator.
TS
// e2e/projects.spec.ts
import { test, expect } from '@playwright/test'
import { LoginPage } from './models/LoginPage'
import { ProjectsPage } from './models/ProjectsPage'

test.describe('Project Management', () => {
  let loginPage: LoginPage
  let projectsPage: ProjectsPage

  test.beforeEach(async ({ page }) => {
    loginPage = new LoginPage(page)
    projectsPage = new ProjectsPage(page)
    
    await loginPage.goto()
    await loginPage.login('alice@example.com', 'password123')
    await loginPage.expectLoggedIn()
  })

  test('create a new project', async () => {
    await projectsPage.goto()
    await projectsPage.createProject('E-commerce Dashboard')
    await projectsPage.expectProjectVisible('E-commerce Dashboard')
  })

  test('search for an existing project', async () => {
    await projectsPage.goto()
    await projectsPage.searchProject('E-commerce')
    await projectsPage.expectProjectVisible('E-commerce Dashboard')
  })
})

Output:

TEXT
TypeScript code executed successfully.

5. Critical Path Testing

The critical path is the business workflow most frequently used by users and must be prioritized.

(1) Authentication Process Testing

TS
// e2e/auth.spec.ts
import { test, expect } from '@playwright/test'

test.describe('Authentication Flow', () => {
  test('redirects unauthenticated user to login', async ({ page }) => {
    await page.goto('/dashboard')
    await expect(page).toHaveURL(/login/)
  })

  test('shows validation errors on empty form', async ({ page }) => {
    await page.goto('/login')
    await page.getByRole('button', { name: 'Log In' }).click()
    await expect(page.getByText(/Please enter your email address/i)).toBeVisible()
    await expect(page.getByText(/Please enter your password/i)).toBeVisible()
  })

  test('successful login redirects to dashboard', async ({ page }) => {
    await page.goto('/login')
    await page.getByPlaceholder('Enter your email address').fill('alice@example.com')
    await page.getByPlaceholder('Enter your password').fill('correct-password')
    await page.getByRole('button', { name: 'Log In' }).click()
    await expect(page).toHaveURL(/dashboard/)
    await expect(page.getByText(/Welcome back, Alice/i)).toBeVisible()
  })

  test('logout clears session', async ({ page }) => {
    await page.goto('/login')
    await page.getByPlaceholder('Enter your email address').fill('alice@example.com')
    await page.getByPlaceholder('Enter your password').fill('correct-password')
    await page.getByRole('button', { name: 'Log In' }).click()
    await page.getByRole('button', { name: /Exit/i }).click()
    await expect(page).toHaveURL(/login/)
  })
})

(2) Form Submission Test

TS
// e2e/task-creation.spec.ts
import { test, expect } from '@playwright/test'

test.describe('Task Creation Flow', () => {
  test.beforeEach(async ({ page }) => {
    // Log in and go to the project
    await page.goto('/login')
    await page.getByPlaceholder('Enter your email address').fill('alice@example.com')
    await page.getByPlaceholder('Enter your password').fill('password123')
    await page.getByRole('button', { name: 'Log In' }).click()
    await page.goto('/projects/p1')
  })

  test('creates a task with all fields', async ({ page }) => {
    await page.getByRole('button', { name: 'Add Task' }).click()
    await page.getByPlaceholder('Task title').fill('Implement user auth')
    await page.getByLabel('Priority').selectOption('High')
    await page.getByLabel('Assignee').selectOption('Bob')
    await page.getByRole('button', { name: 'Save' }).click()
    await expect(page.getByText('Implement user auth')).toBeVisible()
  })

  test('shows error for empty title', async ({ page }) => {
    await page.getByRole('button', { name: 'Add Task' }).click()
    await page.getByRole('button', { name: 'Save' }).click()
    await expect(page.getByText(/The title cannot be left blank./i)).toBeVisible()
  })
})

(3) Navigation and Routing Testing

TS
// e2e/navigation.spec.ts
import { test, expect } from '@playwright/test'

test.describe('Navigation Flow', () => {
  test('sidebar links navigate correctly', async ({ page }) => {
    await page.goto('/login')
    await page.getByPlaceholder('Enter your email address').fill('alice@example.com')
    await page.getByPlaceholder('Enter your password').fill('password123')
    await page.getByRole('button', { name: 'Log In' }).click()
    
    await page.getByRole('link', { name: 'Projects' }).click()
    await expect(page).toHaveURL(/\/projects/)
    
    await page.getByRole('link', { name: 'Dashboard' }).click()
    await expect(page).toHaveURL(/\/dashboard/)
    
    await page.getByRole('link', { name: 'Settings' }).click()
    await expect(page).toHaveURL(/\/settings/)
  })

  test('breadcrumb shows current location', async ({ page }) => {
    await page.goto('/login')
    await page.getByPlaceholder('Enter your email address').fill('alice@example.com')
    await page.getByPlaceholder('Enter your password').fill('password123')
    await page.getByRole('button', { name: 'Log In' }).click()
    await page.goto('/projects/p1/tasks/t1')
    await expect(page.getByTestId('breadcrumb')).toContainText([
      /Projects/, /Project 1/, /Task 1/
    ])
  })
})

6. Visual Regression Testing

Visual regression testing captures subtle changes in UI styling. Playwright enables pixel-level comparison through toHaveScreenshot().

100%
graph LR
    A[Test Run] --> B[Take a screenshot of the current status]
    B --> C{Comparison with the reference screenshot}
    C -->|Match| D[Test Passed]
    C -->|Differences > Threshold| E[Test Failed]
    E --> F[Generate a Variance Report]
    
    style A fill:#cce5ff
    style C fill:#fff3cd
    style D fill:#d4edda
    style E fill:#f8d7da
Option Description Recommended Value
maxDiffPixels Maximum number of differing pixels 100
maxDiffPixelRatio Maximum Difference Ratio 0.01
threshold Pixel Comparison Threshold 0.2
animations Disable animations 'disabled'
stylePath Additional CSS Overrides Hide Random Elements

(1) Configure Visual Testing

TS
// playwright.config.ts Excerpt
export default defineConfig({
  use: {
    screenshot: 'only-on-failure',
    viewport: { width: 1280, height: 720 }
  },
  expect: {
    toHaveScreenshot: {
      maxDiffPixels: 100,
      animations: 'disabled'
    }
  }
})

(2) Generate a reference screenshot

BASH
# Generate a baseline screenshot on first run
npx playwright test --update-snapshots

# Comparison of Subsequent Operations Against the Baseline
npx playwright test

▶ Example: Visual Regression Testing

Output:

TEXT
Running 5 tests using 1 worker

  ✓  navigation.spec.ts:3:1 › navigates to about page (1.2s)
  ✓  navigation.spec.ts:10:1 › displays product list (0.8s)
  ✓  form.spec.ts:5:1 › submits contact form (1.5s)

  3 passed (3.5s)
Running 5 tests using 1 worker

  ✓  navigation.spec.ts:3:1 › navigates to about page (1.2s)
  ✓  navigation.spec.ts:10:1 › displays product list (0.8s)
  ✓  form.spec.ts:5:1 › submits contact form (1.5s)

  3 passed (3.5s)
TS
// e2e/visual.spec.ts
import { test, expect } from '@playwright/test'

test.describe('Visual Regression', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/login')
    await page.getByPlaceholder('Enter your email address').fill('alice@example.com')
    await page.getByPlaceholder('Enter your password').fill('password123')
    await page.getByRole('button', { name: 'Log In' }).click()
  })

  test('dashboard page matches snapshot', async ({ page }) => {
    await page.goto('/dashboard')
    await page.waitForLoadState('networkidle')
    await expect(page).toHaveScreenshot('dashboard.png', {
      fullPage: true
    })
  })

  test('project list matches snapshot', async ({ page }) => {
    await page.goto('/projects')
    await page.waitForSelector('[data-testid="project-list"]')
    await expect(page).toHaveScreenshot('projects.png')
  })

  test('task detail matches snapshot', async ({ page }) => {
    await page.goto('/projects/p1/tasks/t1')
    await page.waitForLoadState('networkidle')
    await expect(page).toHaveScreenshot('task-detail.png', {
      mask: [page.locator('[data-testid="timestamp"]')]
    })
  })
})

Output:

TEXT
TypeScript code executed successfully.

7. API Mocking and Request Interception

Playwright's page.route() can intercept network requests at the browser level, allowing you to test front-end/back-end interactions without a real backend.

(1) Basics of Route Interception

TS
// Global Interception
await page.route('/api/', async route => {
  await route.fulfill({
    status: 200,
    contentType: 'application/json',
    body: JSON.stringify([{ id: 1, title: 'Mocked Task' }])
  })
})

// Specification URL Intercept
await page.route('https://api.example.com/products*', async route => {
  const response = await route.fetch() // Let It Go to Reality API
  const body = await response.json()
  body.push({ id: 999, name: 'Injected Product' })
  await route.fulfill({ response, body: JSON.stringify(body) })
})

(2) Comparison of API Mock Patterns

Pattern Method Applicable Scenarios
Full Mock route.fulfill() Testing the front end when the back end is not ready
Agent Modification route.fetch() + Modification Inject Test Data/Error
Proxy Pass-Through route.fetch() + route.fulfill({ response }) Listen for requests but do not modify the data
Request Interrupted route.abort() Testing Offline Status

▶ Example: A Complete Test with API Mock

Output:

TEXT
TypeScript code executed successfully.
TS
// e2e/api-mock.spec.ts
import { test, expect } from '@playwright/test'

test.describe('API Mock Scenarios', () => {
  test('shows empty state when no data', async ({ page }) => {
    await page.route('**/api/projects', async route => {
      await route.fulfill({
        status: 200,
        contentType: 'application/json',
        body: JSON.stringify([])
      })
    })
    
    await page.goto('/projects')
    await expect(page.getByText(/No projects yet/i)).toBeVisible()
  })

  test('handles 500 error gracefully', async ({ page }) => {
    await page.route('**/api/projects', async route => {
      await route.fulfill({
        status: 500,
        contentType: 'application/json',
        body: JSON.stringify({ error: 'Server error' })
      })
    })
    
    await page.goto('/projects')
    await expect(page.getByText(/Failed to load/i)).toBeVisible()
  })

  test('shows loading state then data', async ({ page }) => {
    // Delay responses to test under load conditions
    await page.route('**/api/projects', async route => {
      await new Promise(resolve => setTimeout(resolve, 1000))
      await route.fulfill({
        status: 200,
        body: JSON.stringify([
          { id: 'p1', name: 'Mock Project', taskCount: 5 }
        ])
      })
    })
    
    await page.goto('/projects')
    await expect(page.getByTestId('loading-skeleton')).toBeVisible()
    await expect(page.getByText('Mock Project')).toBeVisible({ timeout: 5000 })
  })

  test('network offline scenario', async ({ page }) => {
    await page.route('/api/', async route => {
      await route.abort('internetdisconnected')
    })
    
    await page.goto('/projects')
    await expect(page.getByText(/Network connection failed/i)).toBeVisible()
  })
})

Output:

TEXT
TypeScript code executed successfully.

8. CI Integration: GitHub Actions

Playwright integrates seamlessly with GitHub Actions, automatically running E2E tests with every push.

YAML
# .github/workflows/playwright.yml
name: Playwright Tests

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest

    strategy:
      matrix:
        browser: [chromium, firefox, webkit]

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install dependencies
        run: npm ci

      - name: Cache Playwright browsers
        uses: actions/cache@v4
        with:
          path: ~/.cache/ms-playwright
          key: ${{ runner.os }}-playwright-${{ hashFiles('package-lock.json') }}

      - name: Install Playwright browsers
        run: npx playwright install --with-deps ${{ matrix.browser }}

      - name: Run Playwright tests
        run: npx playwright test --project=${{ matrix.browser }}

      - uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: playwright-report-${{ matrix.browser }}
          path: playwright-report/
          retention-days: 7

9. Complete Example: TaskFlow Application E2E Suite

TS
// e2e/taskflow-e2e.spec.ts
// ============================================
// General E2E Test:TaskFlow The Complete User Flow of the Application
// ============================================
import { test, expect, type Page } from '@playwright/test'

// --- Utility Functions:Set Authentication Status ---
async function setupAuth(page: Page) {
  await page.goto('/login')
  await page.getByPlaceholder('Enter your email address').fill('alice@taskflow.io')
  await page.getByPlaceholder('Enter your password').fill('Password123!')
  await page.getByRole('button', { name: 'Log In' }).click()
  await expect(page).toHaveURL(/dashboard/)
}

test.describe('TaskFlow E2E Suite', () => {
  test.beforeEach(async ({ page }) => {
    await setupAuth(page)
  })

  // --- 1. Dashboard Loading ---
  test('dashboard displays key metrics', async ({ page }) => {
    await page.goto('/dashboard')
    await expect(page.getByTestId('total-projects')).toBeVisible()
    await expect(page.getByTestId('total-tasks')).toBeVisible()
    await expect(page.getByTestId('team-members')).toBeVisible()
    
    const projectCount = await page.getByTestId('total-projects').textContent()
    expect(Number(projectCount)).toBeGreaterThan(0)
  })

  // --- 2. Project CRUD Process ---
  test('complete project lifecycle', async ({ page }) => {
    // Create
    await page.goto('/projects')
    await page.getByRole('button', { name: 'New Project' }).click()
    await page.getByPlaceholder('Project name').fill('E2E Test Project')
    await page.getByPlaceholder('Description').fill('Created by Playwright')
    await page.getByRole('button', { name: 'Create' }).click()
    await expect(page.getByText('E2E Test Project')).toBeVisible()

    // Edit
    await page.getByRole('button', { name: /Edit/i }).click()
    await page.getByPlaceholder('Project name').fill('E2E Test Project v2')
    await page.getByRole('button', { name: 'Save' }).click()
    await expect(page.getByText('E2E Test Project v2')).toBeVisible()

    // Delete
    await page.getByRole('button', { name: /Delete/i }).click()
    await page.getByRole('button', { name: /Confirm/i }).click()
    await expect(page.getByText('E2E Test Project v2')).not.toBeVisible()
  })

  // --- 3. Task Management ---
  test('task drag and drop status change', async ({ page }) => {
    await page.goto('/projects/p1')
    
    // Create task
    await page.getByRole('button', { name: 'Add Task' }).click()
    await page.getByPlaceholder('Task title').fill('Setup Playwright tests')
    await page.getByText('TODO').click()
    await page.getByRole('button', { name: 'Save' }).click()
    
    // Verify task appears in TODO column
    await expect(page.getByTestId('column-todo'))
      .toContainText('Setup Playwright tests')
  })

  // --- 4. Search Function ---
  test('search filters projects correctly', async ({ page }) => {
    await page.goto('/projects')
    await page.getByPlaceholder('Search Items').fill('Marketing')
    await page.waitForTimeout(300) // debounce
    const items = page.getByTestId('project-item')
    const count = await items.count()
    for (let i = 0; i < count; i++) {
      await expect(items.nth(i)).toContainText(/Marketing/i)
    }
  })

  // --- 5. Responsive Layout ---
  test.describe('Responsive Design', () => {
    test('sidebar collapses on mobile viewport', async ({ page }) => {
      await page.setViewportSize({ width: 375, height: 812 })
      await page.goto('/dashboard')
      await expect(page.getByTestId('sidebar')).not.toBeVisible()
      await page.getByRole('button', { name: /menu/i }).click()
      await expect(page.getByTestId('sidebar')).toBeVisible()
    })
  })

  // --- 6. API Mock Test ---
  test('handles API errors gracefully', async ({ page }) => {
    await page.route('**/api/dashboard/metrics', async route => {
      await route.fulfill({
        status: 503,
        body: JSON.stringify({ error: 'Service Unavailable' })
      })
    })
    
    await page.goto('/dashboard')
    await expect(page.getByText(/The service is temporarily unavailable/i)).toBeVisible()
  })
})
💻 Output:

TEXT
Running 8 tests using 2 workers
  ✓ e2e/taskflow-e2e.spec.ts (8 tests) 34.2s
  ✓ chromium  | 8 passed (34.2s)
  ✓ firefox   | 8 passed (38.1s)
  ✓ webkit    | 8 passed (41.5s)

❓ FAQ

Q What is the difference between Playwright and Cypress?
A Playwright is maintained by Microsoft (45k⭐), supports multiple browsers (Chromium/Firefox/WebKit), and uses the CDP protocol to directly control the browser, making it faster. Cypress requires injecting JavaScript into the page to run and only supports Chromium-based browsers. Playwright’s webServer configuration is also better suited for Next.js’s npm run dev automatic startup scenarios.
Q How do I test a page that requires login?
A We recommend using Playwright’s storageState feature: After logging in on auth.setup.ts, save the cookies and localStorage to storageState.json, then reference that file in the configuration of other tests on use to avoid having to log in repeatedly for each test.
Q How do I choose between page.route() and MSW for E2E testing?
A page.route() intercepts requests at the browser level and requires no additional dependencies, making it suitable for temporary mocking. MSW requires Service Worker registration and is suitable for component testing (Vitest). For E2E testing, page.route() is recommended by default to reduce external dependencies.
Q What can I do if Playwright tests run slowly in CI?
A (1) Use workers: 1 to avoid resource contention; (2) Cache browser binaries (~/.cache/ms-playwright); (3) Use --project=chromium to run only critical browsers in CI; (4) Parallelize: Split tests across multiple jobs.
Q How should I handle random data in tests (such as timestamps and database IDs)?
A Use Playwright’s mask option to hide dynamic elements in visual tests: toHaveScreenshot({ mask: [page.locator('[data-testid="timestamp"]')] }). For text assertions, use regular expression patterns instead of fixed values.

📖 Summary


📝 Exercises

  1. Basic Question (⭐): Create a LoginPage Page Object containing three methods—goto(), login(email, password), and expectLoggedIn()—and write a test case that uses this POM.

  2. Advanced Exercise (⭐⭐): Write five end-to-end (E2E) tests for the complete "Create-Edit-Delete" (CRUD) workflow in your Next.js application, including page.route() mocks for form validation errors and API error scenarios.

  3. Challenge (⭐⭐⭐): Implement a complete visual regression test suite: (1) Generate baseline screenshots of 5 key pages; (2) Configure the expect.toHaveScreenshot and maxDiffPixels thresholds; (3) Write a CI script to automatically compare the screenshots in a pull request and add a comment with the difference report.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏