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.
| 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)¶
- Auth flow — login → cookie →
/lab/loads → logout → session expiry - Daily field log creation — engineer creates, all required fields, save
- PM approval gate — open daily log → review → mark reviewed → confirm edit button disappears + form locks
- Concrete pour create/edit — PM creates pour, assigns to engineer, pour shows in engineer's schedule
- API authentication — JWT mint, use, expire, revoke
be-pwa¶
- Auth flow — JWT login, persistence across reload, refresh on expiry
- My Day — tests + pours render correctly, mixed list, role-based filtering
- Test perform — happy path — open test → fill perform form → save → complete
- Equipment checklist — load needs, check off, persist; pour-kit items display correctly (no check button) when engineer has a pour
- PourDetail + LogFreshEvent — open pour → log slump/air/temp event → see event appear in list → submit cylinder set
- 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 preferdata-testidover 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