# End-to-end smoke suite (Playwright)

Browser-driven smoke tests that exercise the **real running app** —
FastAPI serving the built React SPA plus `/api` — through Chromium.
They assert **behaviour** (counts, row state, toasts, totals), not
pixels, and cover the three highest-value M8 flows.

- **Config:** [`../playwright.config.ts`](../playwright.config.ts)
- **Specs + fixtures:** [`../e2e/`](../e2e/)
- **Runner:** `make e2e` (never `make check` — see [Why it's separate](#why-its-not-in-make-check))

## Why it's NOT in `make check`

`make check` is the hermetic gate every session runs (ruff + pytest +
tsc + vitest). The e2e suite is deliberately excluded because it needs
two things CI does not have:

1. **A Chromium binary.** Playwright drives a real browser; it must be
   installed once with `npx playwright install chromium`.
2. **A dedicated, migrated + seeded Postgres.** The specs read and
   write real rows (import a batch, edit entries, create/detach an
   invoice). They must never touch the dev sidecar or production data.

Keeping browsers and a live database out of the default gate keeps
`make check` fast and reproducible. Run the e2e suite explicitly when
touching entries / imports / invoices UI, and before an M8 cutover.

## One-time setup

```bash
# 1. Frontend deps + the Chromium browser (once per machine / CI image)
npm ci --prefix frontend
cd frontend && npx playwright install chromium && cd ..
```

## Every run

```bash
# 2. Point the app at a DEDICATED, migrated + seeded e2e Postgres.
#    NOT the dev sidecar, NOT production. SQLAlchemy URL, psycopg driver:
export WITH_DB_URL='postgresql+psycopg://with:<pw>@127.0.0.1:<port>/with_e2e'

# 3. Run it. `make e2e` builds the SPA, then Playwright boots the app
#    (uvicorn) against $WITH_DB_URL and runs the specs.
make e2e
```

`make e2e` = install `@playwright/test` **`--no-save`** into
`frontend/node_modules` (never into `package.json`/the lockfile — see
below) → build `frontend/dist` (the FastAPI webServer serves it) →
`cd frontend && NODE_PATH=node_modules npx playwright test --config
../playwright.config.ts`. Playwright starts the app itself via the
`webServer` block; you do **not** start uvicorn yourself.

> **Why `--no-save`:** Playwright is a dev/CI-only tool, not a shipped
> SPA dependency. Adding it to `frontend/package.json` would desync the
> lockfile and break the hermetic `npm ci` in the Docker build. `make
> e2e` installs it transiently instead.
>
> **Why `NODE_PATH`:** the specs live at the repo root under `e2e/`, but
> `@playwright/test` lands in `frontend/node_modules`. Node resolves
> modules from the importing file upward, which would miss
> `frontend/node_modules`; running with cwd `frontend` and
> `NODE_PATH=node_modules` points resolution there. If you invoke
> Playwright by hand from another directory, set `NODE_PATH` accordingly.

### The e2e database

The app under test runs with the **dev identity stub**:
`AUTH_DEV_USER=rian` + `APP_ENV=test`, so every request resolves to
`rian` (the seeded super-admin / owner) with no login. This is set by
the `webServer.env` in the config — you only supply `WITH_DB_URL`.

The target database **must already be migrated and seeded** — running
uvicorn directly does **not** run `alembic upgrade head` (only the
container CMD does). Seed it from the migrated fixture graph, which
contains:

- `rian` (owner) and `adi` as provisioned `users` with engagements;
- a worker party for **Adi** whose email is `info@adipramono.com`
  (the fixture CSV's "known worker" — lets the import produce a
  non-zero `matched` count);
- real projects/clients (e.g. **Brentwood**, **PlusROI**), invoices,
  and **at least one PENDING (unlocked, non-blocked) time entry**
  (T-E2E-002 and T-E2E-003 both need one to edit / apply).

The suite is **additive** (it creates invoices named `E2E <timestamp>`
and imports a batch each run). Treat the e2e DB as disposable — reseed
between full runs if you want a pristine graph. Specs run serially
(`workers: 1`, `fullyParallel: false`) so runs within a session don't
race each other.

## The three flows

### `import-resolve-commit.spec.ts` — T-E2E-001

Upload `e2e/fixtures/clockify-sample.csv` on `/import`, resolve unknown
entities on `/import/resolve` (create-by-default), commit, then assert:

- the post-import summary card quotes **matched / unmatched** counts;
- the row with an **unknown email** lands **Blocked** on `/entries`
  (`?status=blocked`) — its `worker_party_id` is NULL, which the
  eligibility predicate classifies as blocked.

**Fixture intent** — `clockify-sample.csv` is Clockify's exact 17-column
Detailed Report header. The importer reads the CSV **Client** column as
the *operator* and splits the **Project** column on `" : "` into
`client : project`. Four rows:

| Project (`client : project`) | Email | Outcome |
|---|---|---|
| `Brentwood : Website Build` | `info@adipramono.com` | matched (known worker) |
| `Brentwood : SEO Retainer` | `info@adipramono.com` | matched |
| `Brentwood : Website Build` | `info@adipramono.com` | matched |
| `Acme Co : Landing Page` | `casey.newcomer@unknown-worker.example` | **unmatched → Blocked** |

All four rows carry a full operator/client/project triple, so there
are **no "missing fields" buckets** — only new-entity decisions, which
default to Create. (If `info@adipramono.com` is not seeded, the three
Adi rows are unmatched too; the spec still passes — it only asserts the
card shows the counts and that a blocked row exists, which the unknown
row guarantees.)

### `entry-edit-bulk-undo.spec.ts` — T-E2E-002

On a **filtered + sorted** `/entries` URL
(`?status=pending&date=all&sort=date&dir=desc`):

- **Inline cell edit persists AND preserves URL state.** Edit the
  duration (text cell) and the project (EntityPicker cascade cell)
  inline; assert each new value renders and the query string is
  **byte-identical** afterwards (inline edits PATCH + update cache in
  place — they must not navigate).
- **Bulk reassign + Undo.** Select a subset, reassign the team member,
  confirm the exact-count strip, then assert the **verified result
  toast** ("Reassigned: matched N … — verified") and that its **Undo**
  produces a "restored …" toast (audit before-image restore).

### `invoice-apply-paste-detach.spec.ts` — T-E2E-003

- Create an invoice inline while **applying** a pending entry to it
  (bulk "Apply to invoice" → "Create + apply"); assert the verified
  "Applied N to <name>" toast and, on the invoice, **Entries = 1**
  (the lock made it a first-class line).
- **Copy** the PlusROI paste block; assert the verified "Copied N
  line(s)" toast (clipboard perms are granted in the config).
- **Detach** the entry from `/entries` (owner-only, explicit selection);
  assert the "Detached N … Pending" toast and that the invoice is now
  **Entries = 0** with "No entries attached yet." (partition restored).

## Selectors & the `// SELECTOR-REVIEW:` markers

The frontend has **no `data-testid` attributes**, so the specs use
Playwright role / label / text / placeholder selectors chosen from the
real JSX (button captions, headings, placeholders, aria-labels, and a
few stable component class names like `.picker-label`, `.toast-success`,
`.entries-summary-stat`). Shared hooks live in
[`../e2e/helpers.ts`](../e2e/helpers.ts).

Where a selector needed a judgement call it's flagged with a
`// SELECTOR-REVIEW:` comment for a human to confirm against the running
app after `playwright install`. Current markers:

- **`helpers.ts`** — entries column lookup keys off the visible header
  captions ("Hours", "Project", …); the picker-option helper keys off
  EntityPicker class names (`.picker-list` / `.picker-row` /
  `.picker-label`).
- **`import-resolve-commit.spec.ts`** — the single `<input type=file>`
  on `/import`; the resolve "Create new" radio's accessible name; the
  Blocked status assertion keys off the `title="Blocked: …"` prefix; a
  note on optionally exercising a **Map** decision (needs a seeded
  catalog candidate).
- **`entry-edit-bulk-undo.spec.ts`** — the EntityPicker option label is
  `"{client} : {project}"` while the entries cell shows just the project
  name (the spec compares the trailing segment); scoping the bulk
  Team-member picker by its field caption.
- **`invoice-apply-paste-detach.spec.ts`** — scoping the bulk "Apply to
  invoice" toggle to `.bulk-bar-row` (the panel has a same-named inner
  button); the clipboard-permission dependency for the copy toast.

## Notes

- **Spec filenames end in `.spec.ts`** — vitest (rooted at `frontend/`,
  matching `**/*.test.ts`) never collects them, and they live at the
  repo root under `e2e/`, outside vitest's root entirely. Playwright's
  `testMatch` is `**/*.spec.ts` under `testDir: ./e2e`.
- **Base URL / port:** `http://127.0.0.1:8099`. Change `PORT` in the
  config if it clashes locally.
- The suite cannot be executed in the authoring environment (no browsers
  installed). After `npx playwright install chromium` a human should do
  one confirming run and resolve any `// SELECTOR-REVIEW:` markers.
