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.
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
# 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
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
```bash
|
||||
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-testid`s 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.
|
||||
Reference in New Issue
Block a user