Skip to content

E2E Testing with Playwright

Hardhat Flow uses Playwright for end-to-end browser testing. Tests run against every application role to verify navigation, permissions, and feature access.

Prerequisites

  • Node.js >= 22.12
  • Chromium browser installed via Playwright (see below)
  • Running app (Docker or local dev server)
  • Auth0 test users (one per role)

Installation

cd frontend
npm install
npx playwright install --with-deps chromium

Auth0 Test User Setup

You need 7 Auth0 users, one for each role. These users must be created in your Auth0 tenant and assigned the correct role via Auth0's role management.

Step 1: Create Users in Auth0

In the Auth0 DashboardUser Management → Users, create these users:

Email (suggested) Role Notes
[email protected] badged Minimal access — can view bids, jobs, search only
[email protected] supervisor Can create bids, convert to jobs, sees management KPIs
[email protected] accounting Can create bids, convert to jobs, sees management + accounting KPIs
[email protected] project_manager Can see Clients/Contacts nav, cannot create bids
[email protected] owner Full tenant access — bids, approvals, conversions, all KPIs
[email protected] admin Everything owner has + admin pages (Tenants, Users, Access Requests)
[email protected] platform_admin Admin pages only — no bid creation, no Clients/Contacts nav

Step 2: Assign Roles

In Auth0 → User Management → Roles, ensure each role exists and maps to the https://hardhatpro.com/roles custom claim. Assign each test user to exactly one role.

If a user ever has more than one role in the JWT, the app treats permissions and dashboard KPI visibility as the union of those roles (for example, management and accounting KPIs appear if any assigned role qualifies). Keeping a single role per E2E user remains the recommended setup so Playwright expectations stay predictable.

Step 3: Add Users to a Tenant

Each test user (except platform_admin) must belong to a tenant in Hardhat Flow so the app can resolve their tenant context. The platform_admin user operates at the platform level.

Step 4: Configure Environment Variables

Copy the example file and fill in credentials:

cp .env.e2e.example .env.e2e

Edit .env.e2e:

[email protected]
E2E_BADGED_PASSWORD=<password>

[email protected]
E2E_SUPERVISOR_PASSWORD=<password>

[email protected]
E2E_ACCOUNTING_PASSWORD=<password>

[email protected]
E2E_PROJECT_MANAGER_PASSWORD=<password>

[email protected]
E2E_OWNER_PASSWORD=<password>

[email protected]
E2E_ADMIN_PASSWORD=<password>

[email protected]
E2E_PLATFORM_ADMIN_PASSWORD=<password>

Warning

Never commit .env.e2e to version control. It is already covered by the .env* gitignore pattern.

Running Tests

Start the app first:

# Option A: Docker
docker compose up -d

# Option B: Local dev
cd frontend && npm run dev   # in one terminal
cd .. && python manage.py runserver  # in another

Then run tests:

# Run all role tests
npm run test:e2e

# Run tests for a specific role
npx playwright test --project=owner
npx playwright test --project=badged

# Interactive UI mode
npm run test:e2e:ui

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

# View the HTML report
npx playwright show-report

Test Structure

frontend/
  playwright.config.ts          # Config with per-role projects
  .env.e2e                      # Credentials (gitignored)
  .env.e2e.example              # Template
  .auth/                        # Saved auth state per role (gitignored)
    badged.json
    supervisor.json
    accounting.json
    project_manager.json
    owner.json
    admin.json
    platform_admin.json
  e2e/
    auth.setup.ts               # Logs in all 7 roles, saves storageState
    badged.spec.ts              # Badge Worker tests
    supervisor.spec.ts          # Supervisor tests
    accounting.spec.ts          # Accounting tests
    project-manager.spec.ts     # Project Manager tests
    owner.spec.ts               # Owner tests
    admin.spec.ts               # Admin tests
    platform-admin.spec.ts      # Platform Admin tests

How Auth Works in Tests

  1. The auth.setup.ts file runs first as a Playwright "setup" project
  2. For each role, it navigates to /login/, completes the Auth0 hosted login, and saves the browser state (cookies + localStorage) to .auth/<role>.json
  3. Each role's test project loads its corresponding storageState file, so tests start already authenticated
  4. The setup only runs once per playwright test invocation — all tests for a role share the same session

What Each Role Tests

Role Tests
badged Nav: Dashboard/Bids/Jobs/Search only. No New Bid button. No admin access.
supervisor Nav: same as badged. New Bid button visible. Management KPIs shown. Invoice KPIs appear only if the JWT includes an accounting-capable role (union semantics).
accounting Nav: same as badged. New Bid button visible. Both management + accounting KPIs shown.
project_manager Nav: adds Clients/Contacts. No New Bid button. Can access client/contact pages.
owner Nav: adds Clients/Contacts. New Bid button. All KPIs. No admin access.
admin Full nav including admin section. All KPIs + Tenants button. Can access all admin pages.
platform_admin Admin nav but no Clients/Contacts. No New Bid button. Tenants button shown. Admin pages accessible.

Full Lifecycle Test

The lifecycle.spec.ts file covers the complete Bid → Job → Invoice → Payment workflow across four roles sequentially within a single test, verifying the hand-off chain end-to-end.

What it tests

Phase Role Mechanism What is verified
1 PM UI — /bids/new form Bid is created
2 PM API — POST /bids/{id}/submit/ Bid moves to awaiting_po
3 Owner UI — Approve button Bid moves to approved; job auto-created
4 PM API — start + complete Job moves through in_progressneeds_invoice
5 Accounting API — create invoice + 2 payments Invoice moves to paid
6 Accounting API — close invoice Status reaches workflow_complete
7 Owner UI — dashboard Invoice Paid KPI visible
8 API — activity feed ActivityLog entries created for transitions
9 Supervisor, Badge Worker API 403 on management transitions

One-time E2E tenant setup

Before running the lifecycle test, you need a dedicated E2E tenant that your 7 test users belong to. This is separate from any demo tenant you may already have.

Provision once (local dev):

python manage.py seed_e2e_tenant --provision --slug=e2e

Reset + reseed before each run (done automatically via globalSetup):

python manage.py seed_e2e_tenant --reset --slug=e2e

The command: - Creates the e2e tenant database if it does not exist (with --provision) - Deletes all bids, jobs, invoices, and payments from the tenant database (with --reset) - Seeds one client (E2E Construction Co.) and one contact - Associates each E2E user (found by email from E2E_<ROLE>_EMAIL env vars) with the E2E tenant

Running the lifecycle test

# Run only the lifecycle test
npx playwright test --project=lifecycle

# Run with visible browser
npx playwright test --project=lifecycle --headed

# Skip the automatic tenant re-seed (use existing data)
E2E_SKIP_SEED=true npx playwright test --project=lifecycle

globalSetup behavior

playwright.config.ts registers e2e/global-setup.ts as the globalSetup hook. Before any test project runs, it executes seed_e2e_tenant --reset to ensure a clean state.

In CI, it calls Django via docker compose exec -T web uv run python manage.py …. Locally (DEBUG=True), it calls python manage.py … directly from the repo root.

Set E2E_SKIP_SEED=true in .env.e2e to skip seeding when iterating on test logic.

Test structure

frontend/e2e/
  global-setup.ts              # Runs seed_e2e_tenant before all tests
  lifecycle.spec.ts            # Full Bid → Payment lifecycle test
  helpers/
    contexts.ts                # contextFor(browser, role) and captureToken(page)
    ids.ts                     # uniqueScope() for collision-free test data

Troubleshooting

Auth0 login times out: Ensure the test user exists in Auth0 and credentials are correct. Check that Auth0's Universal Login page hasn't changed its form field labels.

Tests fail with "not authenticated": Delete .auth/ and re-run to regenerate auth state. Auth tokens may have expired.

Port 3000 already in use: The config uses reuseExistingServer: true, so if the dev server is already running, Playwright will use it. If not, it starts npm run dev automatically.