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 Dashboard → User 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¶
- The
auth.setup.tsfile runs first as a Playwright "setup" project - For each role, it navigates to
/login/, completes the Auth0 hosted login, and saves the browser state (cookies + localStorage) to.auth/<role>.json - Each role's test project loads its corresponding storageState file, so tests start already authenticated
- The setup only runs once per
playwright testinvocation — 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_progress → needs_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.