> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sesameterminal.com/llms.txt
> Use this file to discover all available pages before exploring further.

# End-to-end UI suite

**Fixed: fake books report the market's own tick (37659ff).** **Fixed: fake fills carry their outcome (37659ff).** **Fixed: notifications carry `client_order_id`, and the tab that clicked leaves the toast to the click (contract bebb70f, BE 70a6f28, FE fe/phase-4-fixes).** **Fixed: shared `formatCount` caps the tab at `99+` (fe/phase-4-fixes).** **Fixed: the phone score takes its own line (fe/phase-4-fixes).** **Fixed: each delete has its own toast id (fe/phase-4-fixes).** # End-to-end UI suite

A Playwright suite in `e2e/` (TypeScript, exact pinned versions, its own `package.json` and
lockfile) that drives the real frontend against the real backend running the fake venue,
including the Phase 3 trading engine and the Phase 4 alert and notification actors
(`engine/alerts`, `notify`), which fire on the fake venue's moving books and scores. It is **not** part of `task lint` or `task test`. It
**runs before every phase tag**: the tech lead (or QA for the exit gate) runs `task e2e` on
`main` and it must pass before `phase-<n>` is tagged. The manual review that goes with it is
[ui-checklist.md](ui-checklist.md).

## Running it

| Task | What it does |
| - | - |
| `task e2e:install` | `npm ci` in `e2e/` and `playwright install chromium firefox webkit`. Once per machine and after a Playwright bump |
| `task e2e` | The whole suite, all projects. Arguments after `--` go to `playwright test`, e.g. `task e2e -- --project=chromium tests/trading.spec.ts`, `task e2e -- --grep palette`, `task e2e -- --headed` |
| `task e2e:update` | Rewrites the visual baselines (`e2e/tests/__screenshots__/`) after an intended UI change. Review the new PNGs in the diff before committing them |

On this host the full run takes about **6 minutes** with the default 6 workers. Most of it is
the trading, alert and notification tests, each on its own backend, the idle-timeout test (about
70 s by design) and the two tests that wait for a real book move or score change (usually
5–20 s, at most 120 s).

Reports: `e2e/playwright-report/index.html` (HTML, with traces for failures: `npx playwright
show-report` in `e2e/`), `e2e/reports/axe-report.md` (every axe violation of the run), and backend
logs per stack in `e2e/.cache/logs/`. All are git-ignored.

Environment switches:

| Variable | Effect |
| - | - |
| `E2E_WORKERS=<n>` | Worker count (default 6, see "Why 6 workers") |
| `E2E_WEB=dev` | Serve the frontend with the Vite dev server instead of a production build plus `vite preview` |
| `E2E_MOBILE_WEBKIT=1` | Run the `mobile-webkit` project on Windows anyway (see Known gaps) |
| `E2E_IGNORE_KNOWN=1` | Run every known-finding check as a normal test and fail axe on known rules too, to re-verify the list after UI work |

## How it is built

* **Projects:** `chromium`, `firefox`, `webkit` at 1440 × 900; `mobile-chromium` (Pixel 7 profile)
  and `mobile-webkit` (iPhone 14 profile) at 390 × 844 with touch; `stress-chromium` (1440) and
  `stress-mobile-chromium` (390), which run only `stress.spec.ts`, against backends started with
  `SESAME_FAKE_VENUE_STRESS=1` (the `stackEnv` worker option).
* **Global setup** (`support/global-setup.ts`) builds `backend/cmd/sesame` into `e2e/.cache/bin`,
  builds the frontend into `e2e/.cache/web`, makes a random throwaway password, hashes it with
  `sesame hash-password`, and starts the **shared stack**: the backend with `SESAME_FAKE_VENUE=1`,
  that hash, a random `SESSION_SECRET`, a temp `DATA_DIR` and free ports, plus Vite in front of it.
  It logs in once through the API and saves the storage state, sets `theme_mode` to `system`, and
  pins two markets so the watchlist has stable content. Teardown stops everything and writes the
  axe report.
* **Free ports without touching the frontend:** `frontend/vite.config.ts` hard-codes the proxy
  target (127.0.0.1:8080) and port 5173. `support/web-server.mjs` loads that same config through
  Vite's JS API with an inline override of the port and proxy target, so the suite runs beside a
  developer's `task dev` and other agents' servers.
* **Clean environment:** backend and Vite get an allowlisted environment (PATH, temp dirs, proxy
  settings). Nothing from `.env`, which Task loads, reaches them, so real venue credentials never
  mix with the fake venue.
* **Three kinds of stack** (`support/fixtures.ts`):
  * `test`: the shared stack and its one session, for read-only flows. It sets no alert and
    makes no notification (the fake account generates no activity of its own), so no toast or
    unread badge reaches the visual baselines or the hygiene checks.
  * `privateTest`: one backend and Vite per worker with its own session, for tests that change
    server state or stop the backend (login and logout, pins, theme, stale/restart, stress).
  * `freshTest`: one backend per **test**, for trading, alerts and notifications. Every test
    starts SAFE with the seeded account, no alert and no notification, and no order, fill,
    alert or ARMED state leaks into the next. It also gives the test an
    API client with its session and CSRF token, for setup the test does not cover (stake
    presets, risk limits) and for reading the backend's view after a UI action.
* **Themes:** the shared backend's `theme_mode` is `system`, and each test picks dark or light
  with Playwright's `colorScheme`, so parallel tests never fight over a server-side setting. The
  product default (dark) is covered on a private stack by `theme.spec.ts`.

### Why 6 workers

Every shared-stack page uses the one saved session, and the hub allows 8 WebSockets per session
(`hub.DefaultMaxSessionSockets`). With 16 workers the backend refused upgrades (`ws upgrade
refused reason="socket limit"`) and pages sat on "Loading orders". Separate sessions per worker
are not possible on one backend either, because logins are limited to 5 per IP per minute. Six
workers leave headroom for sockets that are still closing.

## What it covers

| Spec | Covers | |
| - | - | - |
| `auth.spec.ts` | A fresh load of `/login` fetches its own chunk and neither the `app-shell` nor the `lightweight-charts` chunk; wrong password (message, value kept, still on login), right password returns to the asked route; logout clears account data from the page, the API answers 401, Back stays on login | |
| `board.spec.ts` | Two live games with Buy/Sell prices for every outcome; the board changes without a reload; window and league filters; the focused panel's top line shows after `F6` and not after a click in Safe (desktop) | |
| `palette.spec.ts` | Mod+K (⌘ on the Safari profile, the Search tab on phones), search, Enter opens the event; Esc clears then closes; `g p` typed in the palette stays text; Shift+Enter (phone: pin button) pins, the watchlist unpins | |
| `watchlist.spec.ts` | Seeded pins with live prices; pin from the event view, unpin from the watchlist, survives reload | |
| `market.spec.ts` | Event view: markets, chart canvas and ranges, book with best bid/ask that moves live (desktop embedded ladder, phone sheet; waits for the ladder's `data-book="ready"` first); docking a ladder, dock kept across routes, close | |
| `portfolio.spec.ts` | Open (partial-liquidation marker), Redeemable with the `Claim on polymarket.com` link (https, new tab, noopener noreferrer), Closed, the Positions/Activity tabs | |
| `orders.spec.ts` | Open/Rejected/Fills tabs, grouped by market, the Sell filter, cancel-market control live in SAFE; dock tabs, collapse, the `` ` `` hotkey, collapse on Portfolio | |
| `theme.spec.ts` | Dark by default, switch to Light (menu on desktop, sheet on phone; the phone `Menu` button is matched by its whole name, since the board's `Game menu: …` buttons contain the word), survives reload | |
| `alerts.spec.ts` | Phase 4 alerts on fresh stacks (below) | |
| `notifications.spec.ts` | Phase 4 notifications and Settings > Notifications on fresh stacks (below) | |
| `stale.spec.ts` | Backend stopped: prices keep their value, `data-stale` and inert; status `Disconnected`; restart: live again. Records the recovery time (1–2 s here) | |
| `trading.spec.ts` | Phase 3 trading against the real engine and the fake venue's rules (below) | |
| `combos.spec.ts` | Phase 8 combos on fresh stacks against the fake venue's combo rules (stake or shares modulo 100 picks no quote, unknown, declined, failed, rejected, lost): build a 3-leg combo and buy it within the click budget; the other outcome replaces a leg in place; an expired quote offers `Get new quote`; no quote and a rejected request say so; an Accept with no answer reads `Checking…` and is sent once; exit a combo position within the click budget, with the fill on the status line (on a phone the sheet stays open over it until closed); on a phone a filled exit accepted from the combo bar keeps the bar, with a close button, until closed; an ineligible outcome's toggle is disabled with the reason | |
| `a11y.spec.ts` | axe (WCAG 2.2 A/AA) on 10 screens × 2 themes in Chromium at 1440 and 390 (Phase 4 adds the empty `/alerts` and `/notifications`; `notifications.spec.ts` scans them with rows); serious or critical fails unless known | |
| `visual.spec.ts` | `toHaveScreenshot` baselines of 10 screens × 2 themes in Chromium at 1440 and 390, re-baselined on `lead/phase-4-kickoff` | |
| `layout.spec.ts` | One control height per row (a segmented control counts as one control), nothing past its container or the viewport, no wrapped labels or numbers: top bar (also at 1024–2560), mobile header, portfolio tabs and summary, board toolbar and rows, dock tabs, no page scrolling sideways (eight routes, with `/alerts` and `/notifications`) | |
| `content.spec.ts` | Visual simplicity: price precision, trailing zeros, units in headers, blank for nothing, no Buy/Sell text under labelled columns, no pictographs on eight routes (all engines, and under reduced motion) | |
| `hygiene.spec.ts` | Copy, state and polish rules on eight routes (Phase 4 adds `/alerts` and `/notifications`) × 2 themes at 1440 and 390: no `undefined`/`NaN`/`null`/`[object Object]`/`Invalid Date`/lorem, no console or page errors, a `<page> · sesame` title per route, sentence case (no upper-case transform, no all-caps words outside an abbreviation list), no `!`, no pictographs outside `<kbd>`, named icon-only controls, a visible focus ring when tabbing through the top bar and a table | |
| `stress.spec.ts` | Stress projects only. Each worker's stack is seeded once with an alert of every kind and state on the long names, the long tennis score, the 0.0001-tick market and the book that never loads, plus the notifications they make (`seedAlerts`). Board, portfolio, orders and the alerts list keep one row height (alerts 36 px desktop, 64 px phone), nothing overflows, no wrapped numbers, no page scrolling sideways; the phone board shows the tennis score `7–6(10–8), 6–7(5–7), 7–6(12–10)` whole (FE-Trading 0bb5dc1); the board reads the captured esports scores as `Maps 0–1 · Bo3` and `Maps 0–0 · Bo3 · Current map 0–1` with `Map 2` as the period, and shows no raw \` | `score. On eight routes (with`/alerts`and`/notifications`; `/settings` includes the notifications section), with reduced motion off and on (`support/clipping.ts`): nothing squeezed below 1 px, its min-width or its square shape; essential values (`data-essential`, else prices, money, clocks, scores, sizes) never truncate; other truncated text has a tooltip and no more than 24 px free in its row; the phone Sell/Buy grid and the stake presets use one gap across and down; price values are centred in their buttons within 1 px. The orders list is virtualised (only rows near the viewport render), so the orders check scrolls the list and counts at least 20 distinct order rows; on desktop the down arrow walks the cursor to the last order, below the fold, and scrolls it into view, and `−1 tick` on a selected order scrolled out of the list sends its replace one tick lower (`Repriced 1\`) |

### Trading tests

Each runs on a fresh stack (`freshTest`) against `engine/orders`, `engine/risk` and the fake
venue's rules (`backend/internal/testutil/fakevenue/trading.go`): 13 shares is rejected, a price
ending in 7 ticks is "unknown" and reaches the venue 1 s later, and marketable orders on sports
markets wait out a 3 s delay window before they fill.

| Test | Checks |
| - | - |
| Arm and safe | Price buttons disabled with `SAFE: arm to trade`, enabled when ARMED, disabled again in SAFE; `/trading` agrees; Cancel all stays live in SAFE |
| One-click buy | One click on the board sends `notional` (the stake), no `size`, GTC, at the shown price; accepted; after the delay it fills into the position (or rests if the asks moved) |
| Ladder | Click places at the level (row label `your buy`), dragging the chip replaces it one level lower, right-click cancels; the backend's open orders agree at each step |
| Exit and join | Exit sends `exit_mode: walk` and the 150 sh position fills down (or rests at the walk limit); Join sends `join` and a resting sell appears (`Cancel 2 orders in Fulham`) |
| Cancel market | Cancels the Celtics orders only; other markets' orders stay |
| Cancel all | The top-bar button (header on phones) and `Mod+Shift+X` while typing in the palette both cancel every live order |
| Reject toast | A stake of 13 × price makes a 13 sh order; the toast reads `Rejected · refused by the venue` and names the price; nothing rests |
| Double-click guard | A double click sends one `client_order_id` and the backend holds one order |
| Unknown outcome | Reconciled by the backend to the same `client_order_id` and never resent; a second test checks that the `Checking…` toast turns into `Order placed` and that the placements topic carries the settled `PlaceOrderResponse` (`status: accepted`) for that `client_order_id`, read on a second socket the test opens in the page (the app drops the topic once its open orders show the order, which is the same outcome seen sooner); a third checks one set of toasts per unknown placement (NT-01, fixed). The fake venue cannot make a never-seen order (every price-ending-in-7 order reaches it 1 s later), so `Order not placed` (`venue_not_found`) is covered by the frontend and Go unit tests only |
| Risk step-up | `PUT /settings/risk` raising a limit without a password is 403 `step_up_required`; the Settings dialog with the password saves (200) and the value survives a reload |
| Idle to SAFE | `arm_idle_minutes` 1 (lowering needs no password); ARMED drops to SAFE on its own |
| Restart comes back SAFE | ARMED, backend restart: the switch shows SAFE and a placement is refused `not_armed` |
| Cancel all over the sheet (phone) | With the ladder sheet open, the header's Cancel all can be tapped (known finding T-02) |

### Phase 4: alerts and notifications

Each runs on a fresh stack. The fake books move every 1–2 s; about one turn in ten moves the
mid by a tick, and the book keeps one empty level between bid and ask (a 2¢ spread). The two
live games (Celtics v Nuggets, Everton v Fulham) score about once every 15 s. A spread alert
`at or below 2¢` fires as soon as it is set, which is how the tests make many notifications
quickly. Helpers are in `support/alerts.ts`: a recorder of the page's `/ws` frames, a toast
timer that runs in the page, the one-click ladder alert (the bell on the hovered row on
desktop, a dispatched 500 ms touch long-press and `Set alert at …` on a phone), and a book
replay that finds the delta that met an alert's condition.

| Test | Checks |
| - | - |
| Ladder bell (all engines, 1440 and 390) | One action at the empty level between bid and ask sets `ask at or below` (the level is at the midpoint); toast `Alert set · Boston Celtics ask at or below N¢`; the backend holds `price`, `below`, the level, `repeat: false`; the level keeps an `Edit alert · …` marker; the row shows `Active` in `/alerts` |
| Undo on the set toast | Undo deletes the alert (backend empty) and the marker goes |
| Fired alert, exit gate | The ladder alert plus its mirror (`above` at the same level, via the API) so the first mid move either way fires one. The notification arrives as a `notifications` delta with body `Boston Celtics ask N¢ · at or below N¢`; the toast shows; the row is in the history (inbox popover on desktop, the menu's notifications sheet on a phone). **Latency** is measured from the book delta's `updated_at` (the feed's time of the change, found by replaying the page's book frames) to the toast appearing in the page, one host clock, and must be under 2 s |
| Board game menu | `Alert on score change` from the `Game menu: …` button on both live games; the item shows checked, `Alert at game start` is disabled with `Game already live`; the first score change fires `Score change` with `{home}–{away} · {clock} · {game}`, the score matches a `game:` frame, and the toast follows the game frame's `updated_at` within 2 s |
| Pause, resume, delete | Hover actions on desktop, the row's `Alert menu · …` on a phone. Delete removes the row at once; Undo brings it back with no request; left alone, the `DELETE` is sent when the 5 s toast closes (at least 4.5 s after the click) |
| Empty and error states | `No alerts` with its line; with a silent socket (`routeWebSocket`) the skeleton, then `Alerts not loaded · …` and Retry; with `GET /notifications` failing, `History unavailable · …` and Retry, which recovers |
| 200 alerts | The 201st `POST /alerts` is 429 `limit_exceeded`; `Set alert…` is `aria-disabled` with the tooltip `200 alerts, the maximum`; the ladder bell gives `Alert not set · 200 alerts, the maximum` |
| History paging | 55 alerts fire; the history is virtualised, so the first row reads `aria-posinset` 1 and `aria-setsize` 50 with `Load older`, then `aria-setsize` equal to every notification and no button |
| Unread over 99 | 100 notifications: the rail and menu badges read `99+`; the Notifications tab too (AL-03, fixed) |
| Read in another tab | A second browser context with the same session (no shared BroadcastChannel): both show the unread count on the rail link (the phone's menu button); opening the history in one sends `POST /notifications/read` (204), and the other drops its count from the server's re-sent snapshot (`unread_count` 0 after the delta) |
| Settings, saving | A checkbox saves on its own (`PUT /settings/notifications` after 400 ms, the rule in the body, 200) and survives a reload; a failed `PUT` (500, routed) puts the box back and the result line reads `Not saved · …` |
| Send test, Telegram | `Send test` is 204, the toast `Test notification` / `Sent from Settings` shows, the button waits out its cooldown, the history has the `test` row. With no `TELEGRAM_*`, the status line reads `Not configured · Telegram runbook` (an https link, new tab, `noopener noreferrer`) and the Telegram column is disabled |
| Desktop permission (Chromium) | Headless Chromium reports `denied` for an ungranted permission: the Desktop column is disabled with `Blocked in this browser`. With `default` emulated in the page and `requestPermission` wired to `context.grantPermissions`, loading `/live`, `/alerts` and `/settings` asks nothing; the first Desktop checkbox asks once, saves `desktop: true` and registers `/sw.js`; a second Desktop checkbox does not ask again |
| Screens with content | With an alert of each kind and state and several notifications: the hygiene copy checks and axe on `/alerts`, `/notifications` and `/settings` at 1440 and 390 |

Measured latency, book or score change to toast on screen, over five runs: 10–150 ms in every
engine, once 840 ms on the phone project under full parallel load (the gate allows 2 s). Toasts come only from notifications that arrive as deltas, so tests that expect a
toast first wait for the page's `notifications` snapshot (`subscribed`); a notification made
before the page subscribed arrives inside the snapshot and, correctly, raises no toast.

**Telegram is not covered end to end.** `notify.TelegramConfig.BaseURL` exists, but
`internal/app` never sets it from the environment, so the backend cannot be pointed at a local
stub without a code change. The client is covered by the Go tests against `httptest` servers.

### Visual baselines

Re-baselined with `task e2e:update` on `lead/phase-4-kickoff` (the Phase 3 polish-round
baselines before that). Every logged-in screen changed, for Phase 4 shell work only: the
Alerts item in the rail (desktop), the inbox button in the top bar, the refitted top bar and
phone header (4a2af0b), and the trading-state note bar under the header (`Adopted 3 orders open at first
start`), which pushes the page content down. The login screens did not change. `alerts` and `notifications` are new
(empty states, as the shared stack has none). `visual.spec.ts`
keeps a `pendingRebaseline` switch: set it to `true` while a UI round is in flight to hold the
comparison as `fixme` (`task e2e:update` still runs the tests, because it passes
`--update-snapshots=all`), and back to `false` with the new PNGs.

### Counts on fe/phase-8-polish (Phase 8)

The full run on `lead/phase-8` plus the Phase 8 polish (the quiet panel indicator, esports
scores, the phone combo exit kept on screen): 419 tests pass, none fail, none are flaky and
none are expected failures, 302 are skipped, in about 6 minutes with 6 workers. 721 tests in
all; Phase 8 adds `combos.spec.ts` (9 tests, one of them phone only: the click budgets, quote
states and exits) and combo screens in the visual, axe and stress checks. The run on `lead/phase-8`
before the polish passed 413 and skipped 296.

| Project | Passed | Skipped |
| - | - | - |
| chromium | 134 | 3 |
| firefox | 65 | 72 |
| webkit | 65 | 72 |
| mobile-chromium | 122 | 15 |
| mobile-webkit | 0 | 137 (the host crash, Known gaps) |
| stress-chromium | 17 | 1 |
| stress-mobile-chromium | 16 | 2 |

Rebaselined with `--update-snapshots=changed`, so only the screens that changed were
rewritten: `live-combo-dark` and `live-combo-light` in `chromium` and `mobile-chromium`. The
setup clicks inside the board in Safe, which used to draw the heavy focus ring round the
board; the quiet indicator does not show there. The new baselines passed 20 of 20 with
`--repeat-each=5`.

Stress mode adds two CS2 games carrying the esports scores captured from the sports channel
(`backend/internal/venue/polymarket/testdata/sports_ws_frames_esports.jsonl`). Its long
league name found a league header that truncated without a tooltip; the header now has one.

### Counts on qa/phase-6-finish (final, before the phase 6 tag)

The full run on `lead/phase-6` plus the last Phase 6 fixes (virtualised lists, the mobile pass):
361 tests pass, none fail, none are flaky and none are expected failures, 264 are skipped, in
about 5.5 minutes with 6 workers. The three tests that assumed every row renders (history paging
in chromium and mobile-chromium, stress orders on the phone) now read `aria-setsize` or scroll
and count. The new and changed tests also passed 50 of 50 with `--repeat-each=5`.

| Project | Passed | Skipped |
| - | - | - |
| chromium | 117 | 2 |
| firefox | 56 | 63 |
| webkit | 56 | 63 |
| mobile-chromium | 105 | 14 |
| mobile-webkit | 0 | 119 (the host crash, Known gaps) |
| stress-chromium | 14 | 1 |
| stress-mobile-chromium | 13 | 2 |

No visual baseline changed, so none was regenerated: the shared stack has no unread badge and
short names, so at 390 px the status word, league headers and orders filter bar look as before,
and the visual test empties the orders list. The fixes show in stress mode and at 360 px, which
the stress checks and the Ghostframe pass cover.

Known issues: none new. mobile-webkit still skips on this Windows host (Known gaps), and the
status word depends on the host (this one is geoblocked, US-VA), which is why the pill is masked.

### Counts on lead/phase-4-kickoff (final, before the phase 4 tag)

The full run after the hardening and the fixes below: 355 tests pass, none fail, none are
flaky and none are expected failures (`support/known-issues.ts` lists no findings), 261 are
skipped, in about 5 minutes. mobile-webkit still skips all 118 (the host crash).

The earlier run on 7332fc4 had 7 expected failures (AL-01, AL-02, AL-03, NT-01); all are fixed.

Skips are by design: axe, visual and hygiene checks run in Chromium only, and some flows are
desktop-only (drag, right-click, hotkeys) or phone-only.

## Flakiness policy

* **No fixed sleeps.** Every wait is a web-first assertion (`expect(locator).toBeVisible()`,
  `toHaveAccessibleName`, `expect.poll`) or waits for a real event (a process printing `ready`, the
  backend's `/api/health`). Locators use roles and accessible names, the same names screen readers
  get. The one timed loop samples the page for pictographs under reduced motion and stops at the
  first hit.
* **No retries** (`retries: 0`). A flaky test is fixed at its cause or quarantined with
  `test.fixme` and a note here; a retry would hide exactly the kind of race a trading UI must not
  have.
* **Market movement is expected.** The fake books random-walk, so a marketable order may fill or
  rest; trading tests accept either outcome where the venue's behaviour allows both, and choose
  resting prices relative to the current best bid.
* **Stable visuals:** numerals are made transparent by `support/visual.css` (the boxes keep their
  size), prices, clocks, charts, order dots, account figures and the host-dependent status pill
  are masked, the page clock is paused before the capture, and the orders list is emptied to its
  fixed-size box.
* **Time-dependent fake data:** the books and games move on their own, so tests assert what is
  always there (the seeded orders and positions), never exact counts. The fake account generates
  no order or fill of its own in the app (that would be foreign activity).
* **Book readiness:** a test that reads a ladder first waits for `data-book="ready"` on its level
  area. The event-view test that watched the book move was flaky while the fake books moved
  rarely; with a move every 1–2 s and that wait it passed 40 of 40 (`--repeat-each=10`, four
  projects, 6 workers in parallel) and in every full run since.
* **The baseline notice:** the shared stack never arms, so the first start's `Adopted N orders
  open at first start` bar stays under the header. It appears when the trading topic's snapshot
  arrives and moves the page down, so the visual test waits for it before the capture.
* **Waiting for a real trigger:** the fired-alert and score tests set alerts that any mid move or
  either game's next score fires, so they usually finish in 5–20 s; their budget is 120 s.

## Known findings

Measured on `lead/phase-4-kickoff` (7332fc4). Layout, content, trading and hygiene findings are
`test.fail` entries in `support/known-issues.ts`: the test runs, is reported as an expected
failure, and turns into an unexpected pass (which fails the run) once the UI is fixed, so the
entry must then be deleted. Axe findings are listed in `knownAxe` in `support/axe.ts`: reported
in `reports/axe-report.md`, not failing. `E2E_IGNORE_KNOWN=1` shows the current state of all of
them.

| Id | Severity | Where | Finding | Owner |
| - | - | - | - | - |
| AL-01 | Minor | Alerts list, desktop | Delete, Undo, then Delete again before the undone toast has left (about 200 ms): the new toast reuses the id `alert-deleted:<id>` of the leaving one, so it closes at once with no Undo, no `DELETE` is ever sent, and the row stays hidden until a reload while the alert stays active on the server (`components/trading/alerts/alert-actions.ts`). Reproduced in Chromium; slower clicks miss the window | FE-Trading |
| AL-02 | Minor | Alerts list, phone, stress | A score alert on the stress tennis game: the long score `7–6(10–8), 6–7(5–7), 7–6(12–10) · S4 03:41` takes line 2, so the game title is squeezed to 0 px and `Active · Repeat` runs under the row's menu button (`AlertCard` in `features/alerts/alert-row.tsx`). The score could take its own line, as on the phone board (0bb5dc1) | FE-Trading |
| AL-03 | Minor | Alerts screen tab strip | The `Notifications` tab shows the raw unread count (`Notifications 203`) where the rail, inbox and menu badges show `99+` (alerts.md "Unread count": above 99 it reads `99+`) | FE-Trading |
| NT-01 | Minor | Unknown placement | One unknown placement shows the click's `Checking with the venue` → `Order placed` toast and also the `Checking order` and `Order checked` notification toasts, so the same event is announced twice. notifications.md keeps the click's toast for `order_rejected` and turns that kind's in-app rule off by default; `order_unknown` and `order_resolved` default to in-app on (contract `NotificationSettings.rules` and notifications.md defaults). Needs a design decision: default those two off in-app, or drop the click's toast | UX designer, then BE-Core (defaults) or FE-Platform |
| NT-02 | Low | Fill notifications | `notify`'s fill copy uses only the fill's own outcome and market title; a fill that carries neither (every stress-mode fill) gets the body `Buy 200 sh at 35¢` with no `{outcome} ·`, while the other kinds look the label up in the catalog. Real Polymarket trade events carry an outcome, so this may be fake-only. Not asserted in the suite | BE-Core |
| FV-01 | Low | Fake venue, stress | The fake book reports a 0.01 `tick_size` for the 0.0001-tick stress market (`fakevenue/feeds.go` builds every snapshot with the package tick), so its ladder shows a 1¢ grid with none of the book's 0.10¢ and 99.90¢ levels, and `POST /alerts` refuses sub-cent thresholds there (`price is not on the market's tick of 0.01`). Stress mode therefore never exercises the 0.0001 ladder. Not asserted in the suite | BE-Core |

No axe rule is known-failing: `knownAxe` is empty, so every serious or critical violation fails.

Fixed and removed since the first runs: every earlier layout finding (L-01 to L-09, including the
1024 px top-bar overflow), trailing zeros, units in cells and `0`/`×0` for nothing (S-01 to S-03),
all axe findings (A-01 to A-05), the default document title (H-01), the upper-case transforms and
BUY/SELL/SAFE/ARMED capitals and the chart range `ALL` (H-02), the missing focus rings on the
portfolio table and phone tabs (H-03), the reduced-motion `▲`/`▼` glyph (E-01), Cancel all under
the phone ladder sheet (T-02) and the truncated tennis score on the phone board (C-03).

Observed, not findings: stress mode publishes its 200 fills of the day as live fill events at
start, so a stress stack begins with 200 `Order filled` notifications (fake data, not a replay
by the app).

The layout helper now measures a segmented control (a `fieldset` or `role=group` of buttons) as
one control at its own height. The earlier "24 px segments beside 28 px chips" findings compared
the buttons inside the padded group with their neighbours, which the rule does not ask for.

## Known gaps

* **Mobile WebKit on Windows:** WebKit for Windows (WinCairo) crashes the page at phone widths
  (390, and intermittently other widths) as soon as the app's stylesheet applies; desktop WebKit
  at 1440 is fine, and a plain page at 390 is fine. The `mobile-webkit` project stays configured
  and runs on macOS and Linux; on Windows it is skipped unless `E2E_MOBILE_WEBKIT=1`. Because this
  may also be a real Safari crash, check 390 in Safari on a Mac or iPhone by hand each phase
  (checklist §13).
* **Visual baselines** exist for Chromium only (1440 and 390) and for Windows. A Linux runner
  needs its own baselines (`task e2e:update` there); the path includes the platform.
* **Telegram** is not reached end to end: the backend's Telegram base URL cannot be set from the
  environment (Phase 4 section above). Covered by Go tests; the live check is the Telegram runbook.
* **Desktop notifications themselves** (the OS notification from `/sw.js`, its click opening the
  target, one per tab set) are not observable from Playwright; the permission flow and the worker
  registration are. Checklist §23.
* **Sound** is not checked (Web Audio output); checklist §23.
* **Never-seen placement** (`Order not placed`, `venue_not_found`): the fake venue has no rule
  that loses an order; unit tests cover it.
* **`feed_down`, `foreign_activity`, `credentials`, `game_start_orders`, `session_key_expiring`**
  notifications need a feed outage over 30 s, outside trading or venue state the fake venue does
  not produce on demand; checklist §23.
* **axe and the hygiene checks** run in Chromium only; the DOM is the same in every engine.
* **Not automated:** real iOS keyboard and safe areas, 200% zoom, screen-reader output, long
  sessions and performance with 50+ games, background tabs, the glossary and British spelling;
  these are in the checklist.
* **Venue error states** (425, 429, cancel-only, post-only mode, no credentials) cannot be produced
  by the fake venue, so they are checklist-only.
* **Long-press cancel on phones** is not automated; the desktop right-click path is. Playwright
  has no long-press gesture; `support/alerts.ts` `longPress` dispatches the touch pointer events
  and holds until the action sheet shows, and is used for the ladder's `Set alert at …`.
* **Structure without test ids:** the spots below are located by structure rather than role. They
  are the first to break when markup changes.

## Test ids that would help

The suite uses roles and accessible names everywhere else. These are located by structure today:

| Element | Located by | Suggested hook |
| - | - | - |
| Top-bar account figures block | parent of the `Cash` link | `role="group"` with a name |
| Portfolio summary strip | grandparent of the `Value (liq)` label | `role="group"` `aria-label="Account summary"` |
| Positions table header row | parent of `Sort by Size (sh)` | `role="row"` / `columnheader`s |
| Orders list scroll container | nearest scrolling ancestor of a market group | `data-testid="orders-list"` |
| Ladder order chip | the `data-chip` span inside a level's row, reached through the row's screen-reader label | a named, focusable chip (`Your buy 31.25 at 16¢`), which would also give keyboard users the chip |
| Essential values (scores, clocks, prices, sizes, P\&L) | text patterns in `support/clipping.ts` | `data-essential` on each value element (requested from FE-Trading); the check reads it first and falls back to the patterns |
| Toasts | sonner's `[data-sonner-toast]`, filtered by text | fine as is; a `data-notification-kind` on notification toasts would tell them from the click's own toasts (NT-01) |
| Notification history list | `listitem`s under the `Alerts` region | `aria-label="Notification history"` on the list |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.