A tab names the account it was claimed for on every request, beacon and upload (X-Siftlode-Expect-Account); current_user and /auth/logout refuse a mismatch with 401 account_changed and keep the session, so a tab left showing the account signed in before reloads instead of writing to the new one. /auth/login clears a connect marker left by an abandoned connect, and a link completes only on a session that still holds. The prefs merge reads the row under a lock. A tab's sessionStorage records its own owner. A page the account loses is given up, address bar and remembered page included. The panel layouts are preferences; the storage contract also refuses a key handed to the Storage API or to usePref outside its kind. A refused sign-in as someone else and a failed sign-out say so. The chromium-only e2e specs are in the gate register, the member walk waits for its requests, and devices.spec checks the save itself.
81 lines
4.7 KiB
Markdown
81 lines
4.7 KiB
Markdown
# E2E characterization (golden-master) net
|
||
|
||
Playwright tests that lock the app's five core flows — **feed** (scroll + filter), **channel**
|
||
(open → back), **player** (open/close), **settings** (dirty-save + guard), and **page switching** —
|
||
so the Platform refactor can _prove_ it changed nothing. They assert current, correct behavior;
|
||
they deliberately do **not** encode the known Platform bugs (those get their own specs when the
|
||
sprints that fix them land).
|
||
|
||
## Run
|
||
|
||
```sh
|
||
siftlode e2e # against the baked localdev :8080 stack (authoritative)
|
||
siftlode e2e-ui # same, Playwright UI mode
|
||
siftlode e2e-seed # (re)seed the test account only, no tests
|
||
```
|
||
|
||
The localdev stack must be up (`siftlode dev`). `global.setup.ts` reseeds the account and logs in
|
||
once, sharing the session via `e2e/.auth/state.json` (git-ignored).
|
||
|
||
**A dropped localhost connect** — on a Windows host, a burst of test traffic can drop a loopback
|
||
connect now and then (every port, any program). The browser resends it and only waits (a loader
|
||
outlives an `expect` timeout); Node does not, so a request from the test side fails with
|
||
`connect ETIMEDOUT`. Those requests go through `retryConnect` (`e2e/retryConnect.ts`), which tries
|
||
that one error again and passes every other failure straight on. Don't run two e2e suites on the
|
||
same machine at once.
|
||
|
||
**Browsers** — every flow runs in three projects: `chromium` (Desktop Chrome), `webkit` (Desktop
|
||
Safari) and `ipad` (iPad gen 7 landscape: 1080×810, touch, mobile). Install the engines once per
|
||
machine with `npx playwright install chromium webkit`; run one project with
|
||
`npx playwright test --project=ipad`. WebKit catches what every iOS browser inherits from the engine
|
||
(JS, CSS/layout, event order). It does **not** reproduce iPad media refusals: Playwright's WebKit
|
||
decodes through the host OS (Media Foundation on Windows, GStreamer on Linux), and measured on
|
||
2026-09-14 it played an mp3-in-mp4 and an AV1 mp4 that a real iPad refuses. Check a media format
|
||
on a real iPad against the dev stack over the LAN (`http://<desktop LAN IP>:8080`, e.g. a
|
||
login-free `/watch/<token>` share link).
|
||
|
||
**Target** — `PW_BASE_URL` (default `http://127.0.0.1:8080`):
|
||
|
||
- **`:8080`** (baked SPA) is authoritative and same-origin. After a **frontend change you must
|
||
`siftlode dev` (rebuild)** for its baked SPA to carry the latest test hooks.
|
||
- **`:5173`** (Vite dev) serves source live — the fast refactor loop; it proxies `/api` to `:8080`,
|
||
so the stack must still be up. All five flows are proxy-safe:
|
||
|
||
```sh
|
||
PW_BASE_URL=http://localhost:5173 siftlode e2e
|
||
```
|
||
|
||
**Accessibility** — `a11y.spec.ts` runs axe-core's WCAG 2.x A + AA rules (`@axe-core/playwright`)
|
||
over every rail page, every admin page and the player, in `chromium` only (the rules read the DOM,
|
||
which the engines build alike). It does not judge colour contrast: axe files text on a gradient —
|
||
every glass surface — as "incomplete", not as a pass or a hit.
|
||
|
||
## Account & fixture
|
||
|
||
Tests run as a dedicated **admin** account `e2e@siftlode.local` (never a real account;
|
||
admin so the suite can reach the admin-only pages / render branches, matching the real primary
|
||
user), reset to a deterministic baseline every run by
|
||
[`scripts/e2e_seed.py`](../../scripts/e2e_seed.py) (run
|
||
in-container so it reuses the app's models + argon2 hasher): the top-3 organic channels subscribed,
|
||
one in-progress + one watched + one saved video, prefs pinned to `language: en`. A second account,
|
||
the plain **member** `e2e-member@siftlode.local`, gets the same baseline and its own session
|
||
(`e2e/.auth/member.json`). Two specs run as it: `member.spec.ts`, holding what a non-admin sees (no
|
||
admin entry, no admin call, no failed request, an admin link lands on the feed), and `devices.spec.ts`,
|
||
which signs it in on a desktop and an iPad at once and changes its channel layout on the server (put
|
||
back in a `finally`) — a spec that reads the member's server prefs must not run beside it. Credentials come from `E2E_EMAIL` (default
|
||
`e2e@siftlode.local`), `E2E_MEMBER_EMAIL` (default `e2e-member@siftlode.local`) and `E2E_PASSWORD`
|
||
(both accounts) — **required, no default**: run the suite through the ops wrapper, which exports it
|
||
from the secret store. Override the container name with
|
||
`E2E_API_CONTAINER`; skip the in-container seed with `E2E_SKIP_SEED=1`.
|
||
|
||
## Selectors
|
||
|
||
The app has no `data-testid`s of its own and all copy is i18n (en/hu), so the specs key on a thin
|
||
`data-testid` layer (added with this sprint) plus the locale pinned to `en`. Stable ARIA hooks that
|
||
already existed are used directly (`role="dialog"` on the player, `aria-current="page"` on the
|
||
active nav row, `aria-pressed` on the feed toggles).
|
||
|
||
## Typecheck
|
||
|
||
`npx tsc -p e2e/tsconfig.json --noEmit` (the specs are outside the app's `src` tsconfig).
|