# 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.
