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)