Skip to content

End-to-end test plan

This document is the canonical reference for the E2E test suite covering both be-platform and be-pwa. Workers picking up E2E tickets should read it first.

Tooling decision

Playwright for both apps: - TypeScript-first; already a devDependency in be-pwa. - Cross-browser (Chromium, WebKit, Firefox) — important because the PWA must work on iOS Safari for field engineers. - First-class service-worker + offline simulation (context.setOffline()). - Trace viewer for failure debugging. - The monorepo CLAUDE.md already mandates Playwright for portal regression — this formalizes it for both surfaces.

Architecture

balanced-engineering/
├── docker-compose.test.yml         # Foundation: spins up both apps + seeded DB
├── apps/
│   ├── be-platform/
│   │   ├── tests/e2e/
│   │   │   ├── package.json        # Own Node project, Playwright only
│   │   │   ├── playwright.config.ts
│   │   │   ├── fixtures/           # Authenticated context per role
│   │   │   ├── pages/              # Page Object Models (Login, DailyLog, Project, ...)
│   │   │   ├── support/            # Helpers — login(), seedDb(), createPour()
│   │   │   └── *.spec.ts           # One file per workflow
│   │   └── scripts/seed_test_data.sh   # Idempotent test fixture loader
│   └── be-pwa/
│       └── tests/e2e/
│           ├── playwright.config.ts    # Extends existing pnpm setup
│           ├── fixtures/
│           ├── pages/
│           ├── support/
│           └── *.spec.ts
└── docs/
    └── E2E_TEST_PLAN.md            # this file

Separation of concerns: - be-platform's tests/e2e/ is a self-contained Node project (own package.json) so the Rust crate doesn't grow a Node toolchain in its primary build path. - be-pwa's tests/e2e/ uses the existing pnpm setup (Playwright is already a dep). - Cross-system flows (e.g. "PM creates pour on portal → engineer logs fresh event on PWA → both surfaces show same data") live in be-pwa's tree because they originate from the PWA's worker assignment and the PWA's Playwright config can drive both URLs.

Test environment — docker-compose.test.yml

Foundation infra. Spins up:

Service Port (host) Notes
be-platform 5001 Built from apps/be-platform/Dockerfile. SQLite at a known path. SEED_TEST_DATA=1 env var.
be-pwa 5002 Built from apps/be-pwa/Dockerfile. nginx serves /app/dist. Points at http://be-platform:8000 via VITE_API_BASE build arg.
seed One-shot container that runs seed_test_data.sh against the fresh DB then exits 0. be-platform depends_on=seed.

Tests run on the host (or in their own container — both work) hitting http://localhost:5001 (portal) and http://localhost:5002 (PWA).

Lifecycle: - docker compose -f docker-compose.test.yml up -d before running tests - Tests connect to known ports - docker compose down -v after — -v wipes the volume so the next run starts from a clean DB - Total spin-up: ~30s on warm caches

State strategy: the seeded DB is the canonical starting point. Tests should add data they need on top, not assume mutations from prior tests. Where a test needs a known existing row (e.g. "Corey's pour #4"), it lives in the seed.

Test users

Seeded by seed_test_data.sh. Passwords are hardcoded constants in the seed script — they are NOT prod credentials and exist only in the ephemeral test DB.

Email Role(s) Password Purpose
[email protected] admin test-admin-pw Admin UI flows
[email protected] project_manager test-pm-pw PM approval, project create
[email protected] principal_engineer test-principal-pw Multi-role tests
[email protected] field_engineer test-engineer-1-pw Primary engineer flows
[email protected] field_engineer test-engineer-2-pw Multi-engineer tests, assignment changes

The seed also creates: - One test client (Acme Concrete) - One test project (2026-TEST-001 Hampton Inn) assigned to test_pm - One scheduled concrete pour (Pour #1, 2026-06-02) assigned to test_engineer_1 - One scheduled material test (SOIL-PROC, material_tests.test_number = 1 → renders as T-2026-0001 after be-platform's "T-{year}-{NNNN}" format) assigned to test_engineer_1

Workflows to cover

Priority order — write the spec files in this order, ship as separate PRs, each independently mergeable.

be-platform (portal)

  1. Auth flow — login → cookie → /lab/ loads → logout → session expiry
  2. Daily field log creation — engineer creates, all required fields, save
  3. PM approval gate — open daily log → review → mark reviewed → confirm edit button disappears + form locks
  4. Concrete pour create/edit — PM creates pour, assigns to engineer, pour shows in engineer's schedule
  5. API authentication — JWT mint, use, expire, revoke

be-pwa

  1. Auth flow — JWT login, persistence across reload, refresh on expiry
  2. My Day — tests + pours render correctly, mixed list, role-based filtering
  3. Test perform — happy path — open test → fill perform form → save → complete
  4. Equipment checklist — load needs, check off, persist; pour-kit items display correctly (no check button) when engineer has a pour
  5. PourDetail + LogFreshEvent — open pour → log slump/air/temp event → see event appear in list → submit cylinder set
  6. Offline-first sync — go offline → submit fresh event → confirm queued in IndexedDB → reconnect → confirm sync drains and server row matches

Naming conventions

  • Spec files: <workflow-slug>.spec.ts (e.g. pm-approval.spec.ts)
  • Page objects: <Screen>Page.ts (e.g. DailyLogPage.ts)
  • Test IDs in app: data-testid="<surface>-<role>-<purpose>" — e.g. data-testid="daily-log-form-mark-reviewed". Tests prefer data-testid over CSS classes. Adding these IDs to the source is in scope when needed for the test.

Fixture-based auth

Each spec that requires a logged-in user uses Playwright fixtures to load a pre-authenticated context, avoiding re-running the login flow per test (which is slow and tests something separate).

// In tests/e2e/fixtures/auth.ts
export const test = base.extend<{
  asEngineer: BrowserContext;
  asPm: BrowserContext;
  asAdmin: BrowserContext;
}>({
  asEngineer: async ({ browser }, use) => {
    const ctx = await browser.newContext({
      storageState: 'fixtures/.auth/engineer.json',
    });
    await use(ctx);
    await ctx.close();
  },
  // ... similar for pm, admin
});

A global setup runs the login flow once per role, persists storageState to fixtures/.auth/<role>.json. Spec files import the extended test object.

Offline simulation

For be-pwa offline tests:

await context.setOffline(true);
// trigger mutation
await context.setOffline(false);
await page.waitForFunction(() => /* sync queue empty */);

The PWA already has the sync-queue UI on /sync — tests assert on that surface to confirm queue state.

CI integration — deferred

Per current direction, E2E tests run locally + on-demand. CI gating is a separate decision after the suite stabilizes. Tickets for that come later.

Smoke subset

A subset tagged @smoke runs in <30s and covers the highest-value paths only: - Engineer login → My Day loads (1 test) - Equipment checklist load + check off (1 test) - Offline submit → sync (1 test)

These can be run post-deploy as a sanity check without standing up the full Docker env (they target prod or a staging URL via env var). The full suite runs against the docker-compose env.

Ticket sequencing

Foundation tickets must land first. Workflow tickets are independent of each other and can be picked up in any order once foundation is in.

Foundation: 1. docker-compose.test.yml (cross-cutting) 2. Seed test data script (be-platform) 3. Playwright harness in apps/be-platform/tests/e2e/ (be-platform) 4. Playwright harness in apps/be-pwa/tests/e2e/ (be-pwa)

Workflows (be-platform): 5 tickets, one per spec file. Workflows (be-pwa): 6 tickets, one per spec file.

Total: 15 tickets.

Out of scope (for now)

  • Visual regression testing (Playwright supports it; defer until UX stabilizes more)
  • Performance budget enforcement in tests
  • Mobile device emulation beyond viewport (iOS-specific behaviors that need real devices)
  • A/B branch comparison between two builds
  • Browser-version matrix beyond Chromium + WebKit