Files
ERP-core/Testing/e2e/README.md
T
ImanThiyanga c31e23c2b9 feat(e2e): add Playwright end-to-end tests for authentication, GRN, production, stock transfers, and adjustments
- Introduced Playwright configuration for e2e testing.
- Implemented authentication tests to validate login functionality.
- Created tests for GRN (Goods Receipt Note) to ensure proper stock handling.
- Developed production run tests to verify lifecycle and stock posting.
- Added stock transfer tests to check movement between warehouses.
- Implemented stock adjustment tests for positive and negative adjustments.
- Established API seeder for test data setup and verification.
- Enhanced utility functions for UI interactions and response handling.
2026-08-05 13:26:41 +05:30

8.9 KiB

ERP-Core E2E tests (Playwright)

End-to-end tests for the three phases requested first: GRN (receiving), Production runs, and Stock movement (transfers + adjustments), plus one chained scenario that walks all three in sequence. Sales and Accounts are intentionally out of scope for now.

Why Playwright, not Selenium

The frontend is Next.js 16 / React 19. Playwright auto-waits for React state updates, ships trace/video capture on failure, and can drive the backend API directly (used here to seed test data), which made it a better fit than Selenium for this stack.

Prerequisites

  1. Backend running locally: cd Backend/ERPCore && dotnet ef database update && dotnet run (needs ASPNETCORE_ENVIRONMENT=Development set — see the repo's local-env notes — and a reachable Postgres instance). Defaults to http://localhost:5224.
  2. Frontend running locally: cd Frontend/erp-system && npm install && npm run dev. Defaults to http://localhost:3000 and proxies /api/v1/* to the backend same-origin.
  3. A real login for the tests. Auth is fronted by an external AuthHex identity provider (Backend/ERPCore/Controllers/AuthController.cs) — there is no local seed for a user account, so E2E_ADMIN_EMAIL/E2E_ADMIN_PASSWORD must be a real, already-provisioned account with access to Receiving, Production, and Stock.
  4. A fresh-ish database is fine: DataSeeder (Backend/ERPCore/Infra/Persistence/DataSeeder.cs) seeds the MAIN/SHOP warehouses, PCS/BOX UOMs, and a General Goods category that these tests rely on existing. Everything else (vendors, items, purchase orders, a production template) is created fresh per run by support/api.ts with unique timestamp-suffixed codes, so reruns never collide with previous data.
  5. E2E_BASE_URL must use http://localhost, not 127.0.0.1 or a LAN IP. The session cookie is written with Secure = true unconditionally (Backend/ERPCore/Infra/Auth/AuthCookieWriter.cs); Chromium only treats plain-HTTP localhost as a secure-enough origin to accept and resend a Secure cookie, so anything else silently drops the session and every post-login request 401s.

Setup

cd Testing/e2e
npm install
npx playwright install --with-deps chromium
cp .env.e2e.example .env.e2e   # then fill in E2E_ADMIN_EMAIL / E2E_ADMIN_PASSWORD

Running

npm run test:e2e          # headless, all specs
npm run test:e2e:ui       # interactive UI mode — best for first-run locator debugging
npm run test:e2e:headed   # headed browser
npm run report             # open the last HTML report

The setup project (specs/global.setup.ts) logs in once through the real /login form — the session is an httpOnly cookie, so there's no token to inject — and saves it to .auth/admin.json. Every other spec's chromium project reuses that storage state, so individual specs don't re-authenticate. auth.spec.ts is the exception: it explicitly runs with no stored session so it can exercise the login form itself.

Layout

Testing/e2e/
├── playwright.config.ts
├── support/
│   ├── env.ts        # reads .env.e2e, resolves the storageState path
│   └── api.ts        # ApiSeeder — creates vendors/items/POs/templates, reads stock on-hand
├── pages/             # Page Object Models (one file per module)
└── specs/
    ├── global.setup.ts
    ├── auth.spec.ts
    ├── grn.spec.ts
    ├── production.spec.ts
    ├── stock-transfers.spec.ts
    ├── stock-adjustments.spec.ts
    └── chained-flow.spec.ts   # GRN -> Production -> Stock Transfer, one continuous scenario

Coverage vs. what's deferred

18 tests across auth, GRN, production runs, stock transfers/adjustments, and one chained flow. Deliberately deferred (all would need a second, multi-stage production template or custom-field scaffolding to exercise, which felt like scope creep for a first pass):

  • Approve & transfer on a non-terminal stage, and transferring a held-back remainder from an Approved stage — both only apply to a multi-stage graph; the seeded template is single-stage (entry == terminal) so every run here only ever exercises "Approve & receive".
  • Reject intake (pulling back delivered upstream WIP) — same reason, needs a parent→child edge.
  • Client-side validation edges inside StageDrawer: completing a stage with produced qty over the staged input, or a scrap qty with no scrap reason selected.
  • Adjustment reason-code → ledger-entry tagging spot-check (GET /stock/ledger) — the positive/negative adjustment tests verify on-hand moves correctly but don't inspect the ledger rows themselves.

Long-run item-dropdown ceiling. The GRN/Transfer/Adjustment "new" pages load items via itemsApi.list({ pageSize: 200 }) (a fixed page, not paginated further in the UI). Every spec run mints 2-3 new permanent items through ApiSeeder.createItem, and nothing deletes them. Once a dev database accumulates more than 200 active items, freshly-seeded items stop appearing in the Item combobox (and if the list sorts ascending by id, it's exactly the newest ones that fall off) — locators like getByRole("option", { name: item.name }) will time out with no visible cause. If that starts happening, the fix is to seed one stable per-module item once and reuse it across runs instead of minting a fresh one each time (every assertion here is already delta-based, so that's a drop-in change).

Known limitation: a real, reproducible hydration bug

Every load of the GRN/Production/Stock pages throws a genuine React hydration error ("Minified React error #418" — text content mismatch between server and client render). It is not intermittent — it fires on every navigation — but its effect is: hydration recovery blanks the placeholder text of a random subset of that page's Select triggers for the rest of that page's life, while leaving the sibling <Label>/<FieldLabel> and the trigger's role="combobox" attribute intact. A getByRole("combobox", { name: ... }) lookup is therefore unreliable on these pages; support/ui.ts's comboboxByLabel() works around it by finding the trigger via its stable sibling label + role alone, never its (possibly-blanked) accessible name. The same file's retryClick/clickToReveal/ clickToRevealWithReload/submitAndWait cover two related, separately-confirmed issues: short-lived disabled/not-yet-mounted trigger buttons (RunActions.tsx's "Cancel run"/ "Return leftover", gated on run.status), and stage/document actions whose UI only reflects an async POST once the response lands — reading stock through the API immediately after a click can otherwise race the backend commit. This is worth a look on the product side (root-causing the actual SSR/CSR mismatch would remove the workaround entirely), but was out of scope for a first E2E pass.

Backend also can't take concurrent Playwright workers yet. Reference-data GETs (/warehouses, etc.) intermittently 500 when 2+ workers hit a plain dotnet run + local Postgres backend at once — confirmed by re-running the exact same suite at workers: 1 with zero failures. playwright.config.ts pins workers: 1 for that reason; raise it only against a backend that can actually take concurrent load.

Known limitation: no data-testids yet

None of the GRN/Production/Stock Transfer/Stock Adjustment components in Frontend/erp-system currently expose data-testid attributes, and several form controls have no accessible name at all (the Qty/Unit cost/Disc%/VAT% <Input type="number"> cells in the GRN and Transfer line tables aren't wrapped in a <label> or given aria-label). Locators in pages/ work around this with role/placeholder matching where an accessible name exists, and row + column-position locators (row.locator('input[type="number"]').nth(n)) where it doesn't — every such case is called out in a comment at the top of the relevant pages/*.ts file, along with the couple of same-text button pairs (e.g. "Cancel run" is both the trigger and the dialog's confirm label) that needed .first()/.last() to disambiguate. If a component's copy or layout changes, run npm run test:e2e:ui to see exactly which locator broke and fix it in pages/*.ts — the specs themselves shouldn't need to change.

Recommended fast-follow (not done here, since it's a product-code change rather than a test-authoring one): add data-testid to the Select triggers, the Qty/cost inputs, and the line-table rows in the receiving/production/stock components. That would let every locator above swap from role/position matching to exact data-testid matching in one pass.

CI

Not wired up yet — no GitHub Actions workflow exists in this repo. Once these specs are green locally, add .github/workflows/e2e.yml (spin up Postgres + backend + frontend as services, run npm run test:e2e, upload playwright-report/ as an artifact) as a follow-up.