Files
d8b024bddc chore(e2e): Update playwright with smaller focus (#4932)
* chore(e2e): Update playwright #4772, smaller focus

* chore(e2e) address greptile feedback

* chore(e2e): keep static analysis out of e2e/.output

Everything the suites write (reports, traces, saved sessions) lands in
e2e/.output. It was already gitignored and ESLint skipped it, but the
e2e tsconfig included it through ./**/*.ts, so any .ts file written
there was type-checked, and Trunk skipped it only through the
.gitignore default. Exclude it from the tsconfig and list it in
Trunk's ignore paths explicitly.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* chore(e2e): run with zero config locally and typecheck the suite in CI

- Every E2E_* variable now falls back to a default matching
  seed_e2e_users.py, and the shell wins over e2e/.env, which is optional.
- With E2E_BASE_URL unset, the config starts (or reuses) `npm run dev`
  through webServer; CI still sets the URL and serves the static build.
- Drop the migration table for removed variables, the unknown-key checks,
  and the gitignore entries for suite versions that never shipped.
- Add e2e/tsconfig.json to the root project references, so
  `npm run typecheck` (run by Frontend Checks) covers specs/ and setup/.
- Restore `test:e2e:ui`.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix(e2e): make seedUiState win over the user's stored preferences

Preferences are now stored per user (#5098), and the app only adopts an
unscoped value when the user has no `user:<id>:` copy. Saved sessions
carry one, so seedUiState(page, "light") was silently ignored. Drop the
scoped copies before seeding, and assert the light theme in the spec
that relies on it.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* build(frontend): add build:preview, which copies frontend/assets into dist

Vite leaves frontend/assets out of the build and only the Docker image
copies it in, so a bare `vite preview` serves the SPA fallback for every
icon, logo, font and default cover. Give that copy one home: the e2e
workflow and the local production-build recipe both use the script. It
copies with fs.cpSync so it also runs from cmd.exe and PowerShell.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* ci(e2e): let the Playwright webServer serve CI again

The suite starts its own server once more, so the workflow no longer
needs its own build, serve, wait and teardown steps, nor the E2E_*
variables that now default to the seeded accounts. In CI the webServer
runs `build:preview` and `vite preview` as it did before this PR; locally
it runs (or reuses) `npm run dev`. The default URL is 127.0.0.1, which
the preview binds, rather than localhost, which can resolve to ::1.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor(e2e): simplify the setup files

- auth.setup.ts signs each account in afresh every run, instead of
  validating saved sessions in a separate browser context. Two sign-ins
  cost about 3s.
- The hand-rolled login retry loop and its CI-specific attempt counts
  give way to `retries: 2` on the setup project, and the timeout
  arithmetic to `setup.slow()`.
- global-setup.ts folds its four check functions into one, and reads the
  library total from the admin's account check instead of fetching it
  twice.
- Drop isSessionValid(), login() and the ESLint exception that only the
  old setup needed.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor(e2e): drop the global-setup preflight

The suite already reports what it checked: a downed backend fails
sign-in through the app-error guard (`GET /api/heartbeat returned 502`),
and a wrong E2E_BASE_URL fails page.goto with the URL. The one case it
explained better, a rejected account, is now an assertion on the
`POST /api/login` status in auth.setup.ts. With no preflight, the suite
never calls the API directly, so its ESLint exception goes too.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* test(e2e): cover every route for both roles, and stop the theme leaking

loads.spec.ts:
- Key PAGES by RouteName from src/plugins/routeNames.ts, with a `skip`
  reason for routes it can't open, so a new route fails the typecheck
  until it is listed. That adds devices and conversion-settings, which
  had drifted out.
- Load each page as every role allowed on it, not only as admin, so a
  viewer-only 403 shows up.
- Drop the wait for in-flight requests, which timed out on pages that
  load slowly on a large library.

Tags and folders now use route names (@page:rom, specs/rom/,
specs/user-profile/).

The app syncs UI settings to the account and reapplies them on sign-in,
so a theme a spec wrote leaked into later sessions: admin ended up on
"auto", and every "dark" spec rendered light. seedUiState now always
stores "auto", the config sets `colorScheme: "dark"`, and the light
spec uses emulateMedia. The saved sessions carry the seeded settings, so
specs no longer call seedUiState themselves.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor(e2e): keep support/auth.ts to accounts and sign-in

- Navigation helpers (gotoHydrated, gotoFirstRom, gotoOwnProfile) move
  to support/navigation.ts, with a gotoFirstPlatform that loads.spec.ts
  and gotoFirstRom now share.
- openMoreMenu and menuLabels move into rom/actions-menu.spec.ts, their
  only user.
- Role derives from ROLES, and accountFor's return type is inferred, so
  the Account interface goes.
- watchAppErrors folds into the app-error guard, its last caller.
- output.ts exports the results and report paths instead of a
  suiteOutput() that only ever took "specs".

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix(e2e): don't count the page load against gotoHydrated's waits

The /api/permissions/me and login-form waits are armed before page.goto,
so their 5s local action timeout also covered the navigation, and a slow
page timed out before the request was sent. The test's own timeout now
bounds them.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor(e2e): replace the environment module with plain reads

With every variable defaulted, the parser, its aggregated error and the
e2eEnv fixture guarded against mistakes that can barely happen, and
Playwright reports the rest itself.

- playwright.config.ts loads e2e/.env with process.loadEnvFile (the
  shell still wins) and reads E2E_BASE_URL; support/auth.ts reads the
  accounts into ACCOUNTS. Specs that need the URL use the baseURL
  fixture.
- E2E_WORKERS goes: `--workers N` already does that.
- Drop the e2eEnv fixture, its ESLint import ban and the empty-pattern
  exception it needed, and trim .env.example.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* build(e2e): typecheck the suite with tsconfig.node.json

e2e/tsconfig.json was a looser copy of the Node config: it lacked
noUncheckedIndexedAccess and exactOptionalPropertyTypes, so specs/ and
setup/ escaped flags the rest of the frontend, and the e2e support files
through playwright.config.ts, already obey. The suite runs in Node, so
it joins tsconfig.node.json, and typecheck:e2e goes with its config.
The specs needed no changes under the stricter flags.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor(e2e): trim the ESLint config and the app-error guard's opt-out

- Drop the Node globals block: typescript-eslint turns no-undef off for
  .ts files, so it changed nothing.
- Drop no-skipped-test's allowConditional: nothing in the suite skips.
- Fold the type-info, floating-promise and no-API blocks into one, and
  move the e2e/.output ignore to the base config's generated-output list.
- Remove failOnAppErrors and E2EOptions: nothing opts out of the guard.

Every remaining rule still fires on a probe spec.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* docs(frontend): shorten the test:e2e command note

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* build(frontend): copy assets with cp -R in build:preview

Also puts a comma in the test:e2e command note.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix(e2e): one set of timeouts, and treat empty variables as unset

- Local runs used 3-10s timeouts against CI's 10-45s, so slow pages on
  a large dev library failed locally while CI passed. Both now use the
  CI values; a debugger still turns them off.
- An empty E2E_BASE_URL left baseURL as "" while still starting the
  server, and an empty account variable signed in as "". Both fall back
  to their defaults now.
- Set the Desktop Chrome device once, point the slow-test comment at
  test.slow(), and anchor the setup project's testMatch.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix(e2e): apply code-review findings

pr-ready step 2 (code-review xhigh --fix):
- fall back to E2E_PASSWORD, which the seed script uses, for both accounts
- name the cause when the library has no platform or game to click
- read PWDEBUG=0/false as off, as Playwright does
- widen the API-call lint ban to any uncalled `.request` and its import
- compare the signed-in username case-insensitively
- only allow adminOnly with a URL, so the viewer 404 check always runs
- fix the README grep recipe and the recorded-spec location
- Object.hasOwn in seedUiState, ROLES in role.spec, shorter comments

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor(e2e): apply simplify findings

pr-ready step 3 (simplify):
- one home for the login-form and signed-in-user locators
- derive ROLES and STORAGE_STATE from ACCOUNTS
- openMediaSubtab returns the panel; upload helpers take a scope
- menuLabels reads the panel openMoreMenu returns
- seed the UI state in login.spec's beforeEach
- one no-restricted-syntax entry for the API-call ban
- one port constant in playwright.config.ts
- drop .gitignore entries for paths nothing writes any more

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* docs(e2e): trim two long comments

pr-ready step 4 (review-polish).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* docs(e2e): plainer README opener and comments

pr-ready step 5 (humanizer).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor(e2e): role selectors, one first-game lookup, shared route scopes

- Select by role and label: the account menu's name ("Account menu for
  <user>") replaces the user-name span, the Login button the login form's
  class, the "Page not found" heading the 404 class, and the subtab rail's
  tablist role the subtab button class.
- library.setup.ts clicks through to the first game once per run and saves
  its URL; specs open it with gotoFirstRom() instead of clicking through
  /platforms in every test. loads.spec keeps the click-through.
- Move the router's route scope table to routeNames.ts as ROUTE_SCOPES, so
  loads.spec derives admin-only pages from it: a newly gated route gets the
  viewer 404 check unless it is listed in VIEWER_OPENS.
- `vite preview` serves frontend/assets through a sirv middleware, as the
  dev server and the Docker image do, so build:preview goes away.
- CI builds the frontend in the background while the backend is set up,
  and the Playwright webServer only runs `vite preview` on that build.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix(v2): menuitem role on button menu items, tabpanel role on Media panels

- RMenuItem sets role="menuitem" on its button form too, not only on links,
  so every item in an RMenu is a menuitem. Stories give the items a
  role="menu" parent.
- SubtabNav takes an optional idPrefix that gives each rail tab an id and
  aria-controls. MediaTab uses it to make its sections tabpanels labelled
  by their tab; the small-screen sheet variant has no tabs, so its sections
  stay plain.
- The e2e specs read menu labels by the menuitem role and find a Media
  panel by its tabpanel name.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Georges-Antoine Assi <[email protected]>
2026-10-04 20:28:07 -04:00
..