Skip to main content

Playwright Test Strategy

Issue: TRA-145 Milestone: M11 Stack: Playwright, TypeScript


Why E2E Tests​

Manual smoke testing after every deployment is not sustainable beyond soft beta. The M10 bugs (assign, notes, put on hold) were all caught by manual testing after deployment — a Playwright suite would have caught each one in under 30 seconds. As real users come on board, regressions in core workflows become a reputation risk, not just a development inconvenience.


Project Structure​

Create a new folder e2e/ at the root of the RoundTripWeb repo:

RoundTripWeb/
├── src/ # React app
├── e2e/ # Playwright project
│ ├── playwright.config.ts
│ ├── auth/
│ │ ├── dispatcher.json # Stored auth state — gitignored
│ │ └── technician.json # Stored auth state — gitignored
│ ├── fixtures/
│ │ └── test-data.ts # Shared test data helpers
│ ├── pages/ # Page Object Models
│ │ ├── DispatchBoardPage.ts
│ │ ├── TicketDetailPage.ts
│ │ └── InvoiceDetailPage.ts
│ └── tests/
│ ├── auth.setup.ts # One-time auth state setup
│ ├── tickets.spec.ts
│ ├── billing.spec.ts
│ └── technician.spec.ts
├── package.json

Authentication Strategy​

MSAL redirect-based auth cannot be easily automated. Use Playwright's storageState feature to capture authenticated session state once, then reuse it across all tests.

Setup (run once, commit auth state files)​

// e2e/tests/auth.setup.ts
import { test as setup } from '@playwright/test'

setup('authenticate as dispatcher', async ({ page }) => {
await page.goto('https://roundtrips.app')
// Complete MSAL login manually on first run
// Playwright captures the session cookies and localStorage
await page.context().storageState({ path: 'e2e/auth/dispatcher.json' })
})

Auth state files are gitignored — each developer generates their own. CI generates fresh auth state in the pipeline setup step.

Usage in tests​

// playwright.config.ts
export default defineConfig({
projects: [
{
name: 'dispatcher',
use: { storageState: 'e2e/auth/dispatcher.json' },
testMatch: '**/*.spec.ts',
},
{
name: 'technician',
use: { storageState: 'e2e/auth/technician.json' },
testMatch: '**/technician.spec.ts',
},
],
})

Test Tenant Strategy​

Tests run against the real production app but against a dedicated test tenant. This tenant is seeded with:

  • 1 Dispatcher user (test account)
  • 1 Technician user (test account)
  • 1 test client with a service address
  • A small inventory catalogue

Test data is not reset between runs for Phase 1 — tests create new data each run with a timestamp prefix (e.g. [TEST-{timestamp}] HVAC Repair). This avoids needing a reset endpoint while keeping tests independent.

In Phase 2 (M11), add a DELETE /v1/test/reset endpoint gated behind a TestOnly role that wipes test tenant data between runs.


Phase 1 — Happy Path Suite​

Test 1 — Auth redirect​

test('dispatcher login redirects to dispatch board', async ({ page }) => {
await page.goto('https://roundtrips.app')
await expect(page).toHaveURL(/\/dispatch\/board/)
await expect(page.locator('h1')).toContainText('Dispatch')
})

Test 2 — Create and assign ticket​

test('dispatcher creates a ticket and assigns to technician', async ({ page }) => {
const label = `[TEST-${Date.now()}] HVAC Repair`
// Open create ticket modal
// Fill in client search, select address, fill description
// Select technician
// Submit — verify ticket appears on board
await expect(page.locator(`text=${label}`)).toBeVisible()
})

Test 3 — Add note to ticket​

test('dispatcher adds a note to a ticket', async ({ page }) => {
// Navigate to ticket detail
// Type note text
// Submit
// Verify note appears in notes list with author name
})

Test 4 — Put ticket on hold​

test('dispatcher puts an InProgress ticket on hold', async ({ page }) => {
// Navigate to InProgress ticket
// Click Put On Hold
// Enter reason: "Awaiting customer callback"
// Confirm
// Verify status badge shows OnHold
// Verify hold reason is displayed
})

Test 5 — Complete ticket​

test('dispatcher completes a ticket', async ({ page }) => {
// Navigate to InProgress ticket
// Click Complete
// Enter completion note (min 10 chars)
// Confirm
// Verify status badge shows Completed
})

Test 6 — Generate invoice​

test('dispatcher generates an invoice from a completed ticket', async ({ page }) => {
// Navigate to billing
// Click Generate Invoice
// Search for the completed test ticket
// Select it
// Submit
// Verify redirect to invoice detail page
// Verify invoice status is Draft
})

Test 7 — Technician PWA​

test('technician views assigned ticket and starts it', async ({ page }) => {
// Uses technician auth state
// Navigate to /tech/today
// Verify assigned ticket is visible
// Tap Start Job
// Verify status changes to InProgress
})

Page Object Model Pattern​

Keep tests readable by abstracting page interactions:

// e2e/pages/TicketDetailPage.ts
export class TicketDetailPage {
constructor(private page: Page) {}

async putOnHold(reason: string) {
await this.page.click('button:has-text("Put On Hold")')
await this.page.fill('[placeholder="Hold reason"]', reason)
await this.page.click('button:has-text("Confirm")')
}

async getStatusBadge() {
return this.page.locator('[data-testid="status-badge"]').textContent()
}
}

Add data-testid attributes to key UI elements in the React components as you build tests — don't rely on text content or CSS classes alone.


Running the Tests​

# Install Playwright
cd e2e && npm install

# First run — generate auth state (requires manual login)
npx playwright test auth.setup.ts

# Run full suite
npx playwright test

# Run specific file
npx playwright test tickets.spec.ts

# Run with UI mode (headed, great for debugging)
npx playwright test --ui

# Run against local dev instead of production
BASE_URL=http://localhost:5173 npx playwright test

CI Integration (M11)​

Once the suite is stable, add a step to the GitHub Actions pipeline:

- name: Run E2E tests
run: npx playwright test
env:
BASE_URL: https://roundtrips.app
PLAYWRIGHT_DISPATCHER_EMAIL: ${{ secrets.E2E_DISPATCHER_EMAIL }}
PLAYWRIGHT_DISPATCHER_PASSWORD: ${{ secrets.E2E_DISPATCHER_PASSWORD }}

The CI setup step authenticates fresh each run using credentials stored in GitHub Secrets, generates the auth state files, then runs the suite.


What Not to Test in Phase 1​

  • SignalR real-time updates (requires two browser contexts — add in Phase 2)
  • Offline PWA behaviour (requires network interception — add in Phase 2)
  • PDF download verification (binary content — add in Phase 2)
  • Error states and edge cases (add after happy path suite is stable)