Files
cf6966112b chore(e2e): check every page for accessibility violations with axe (#4951)
* chore(e2e): Update playwright #4772, smaller focus

* chore(e2e) address greptile feedback

* chore(e2e): Add Lighthouse and Axe

* chore(e2e): replace the Lighthouse and axe projects with an axe check per page

- Move PAGES and the page-opening wait into support/pages.ts, so loads.spec.ts
  and the new a11y.spec.ts share one list of every route.
- a11y.spec.ts runs axe on each page as the least privileged role allowed on
  it, and login.spec.ts on the login page. Critical violations fail; every
  violation is attached to the report as JSON. Serious ones (mostly color
  contrast) still fire on several pages, so they stay report-only for now.
- Drop the Lighthouse suite and its dependency tree, the separate axe project,
  the @smoke tag, the sitemap, and build:e2e.

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

* fix(e2e): wait for skeletons to clear before checking a page

Addresses Greptile's review: openPage returned once the view mounted, so axe
(and the load check) ran while seven data-backed pages still showed
RSkeletonBlock placeholders. Wait until none are in the viewport; virtualized
grids keep them below the fold.

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

---------

Co-authored-by: Georges-Antoine Assi <[email protected]>
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-05 07:16:25 -04:00
..

End-to-end tests (Playwright)

These tests drive the app in a real browser against a real backend, for behaviour that only shows up once everything is assembled. Vitest covers components in isolation.

Run it

You need a backend with games in its library and two accounts: an admin, and a non-admin in the Viewer group. On a throwaway dev backend, the seed script creates both (run it from the repo root). Never run it against a real server; it resets those accounts' passwords.

uv run python .github/scripts/seed_e2e_users.py

Then, from frontend/:

npm run test:e2e

That starts npm run dev (or reuses the one already running on 3000), which proxies to the backend named by DEV_PORT or DEV_PROXY_TARGET in .env. To point the suite at another site or other accounts, set the variables from e2e/.env.example in your shell or in e2e/.env. For more or fewer parallel workers than the default 2, pass --workers N.

Install the recommended VS Code extension, Playwright Test for VS Code. Most recipes below start from its panel in the Testing sidebar.

Recipes

Write a new test by clicking

Create the file, leave the cursor inside the test, and click Record at cursor in the Playwright panel:

// e2e/specs/home/favorite-a-game.spec.ts
import { STORAGE_STATE } from "../../support/auth";
import { expect, test } from "../../support/test";

test.describe("Favorites", { tag: "@page:home" }, () => {
  test.use({ storageState: STORAGE_STATE.admin });

  test("favorites a game", async ({ page }) => {
    await page.goto("/");
    // cursor here
  });
});

A browser opens, already signed in, and every click is written into the file. Use the recorder's assert buttons for what should be true afterwards, then replace any nth() or CSS-path selectors with roles and labels.

Watch a test run

Tick Show browser in the Playwright panel and run any test. From a terminal:

npx playwright test --headed --workers 1

Find out why a test failed

npx playwright test --ui    # click a step to see the page, network and console at that moment
npm run test:e2e:report    # the last run's failures, with traces

For a CI failure, download the playwright-report artifact from the workflow run, unzip it, and run npx playwright show-report <unzipped>/report.

Stop on a line

Set a breakpoint, right-click the test's gutter arrow, and choose Debug Test. Timeouts switch off while a debugger is attached, so take your time.

Poke at the page mid-test

await page.pause();
npx playwright test --debug e2e/specs/rom/actions-menu.spec.ts

The Inspector steps one action at a time, tries locators live, and records more steps. ESLint refuses a committed page.pause().

Find a selector

Pick locator in the Playwright panel, then click the element. The locator is copied for you.

Debug the Vue app itself

Run headed, press F12 in the test browser, and put debugger; in any .vue file. It pauses there, with the real source files.

Test as the viewer

test.use({ storageState: STORAGE_STATE.viewer });

Need the credentials themselves? Take them from ACCOUNTS:

const { username } = ACCOUNTS.viewer;

Test Explorer shows no tests

The extension lists tests by loading playwright.config.ts. If that fails, the panel stays empty. See why:

npx playwright test --list

Fix what it reports, then click Refresh Tests in the Testing sidebar.

Run one file, one test, or one page

npm run test:e2e -- e2e/specs/auth/login.spec.ts
npm run test:e2e -- -g "rejects a wrong password"
npm run test:e2e -- --grep "@page:rom( |$)"   # every test for one page, by its route name

Test another site

Set E2E_BASE_URL (in the shell or e2e/.env), and use accounts that exist there:

E2E_BASE_URL=https://romm.example.com

The specs follow this branch's UI, so a site on another version fails where the two differ. A released site (5.3.1, say) lacks endpoints added on master since, and loads.spec.ts reports each as a 404. For a full pass, run a backend from this checkout (uv run main.py) and leave E2E_BASE_URL unset.

To test a production build of this branch, serve it first: npm run build && npm run preview (preview also serves frontend/assets, as the Docker image does), then set E2E_BASE_URL=http://localhost:4173.

Check accessibility

a11y.spec.ts runs axe on every page in PAGES, and login.spec.ts on the login page. A critical violation fails the test; every violation, at any impact, is attached to the report as JSON.

npm run test:e2e -- -g "a11y"

The axe DevTools browser extension runs the same rules on the page you're looking at, which is faster for fixing one.

How it's wired

e2e/
  specs/      the tests, and only tests, one folder per page
    loads.spec.ts   every page opens with every response 2xx
    a11y.spec.ts    every page has no critical axe violations
  setup/      sign-in and the first game, run before the specs
  support/    fixtures, helpers, the page list, environment and output paths
  .output/    generated and gitignored; delete it to reset
    auth/       saved sessions
    specs/      results/ (traces, screenshots) and report/ (HTML)
  • Environment: every E2E_* variable comes from the shell, then e2e/.env, then a default that matches the seed script. CI uses the defaults.
  • Server: with E2E_BASE_URL unset, the config's webServer starts one: npm run dev locally (reusing one already running), the static build under vite preview in CI. Otherwise it tests that URL as served.
  • Sign-in: setup/auth.setup.ts signs each account in at the start of every run and saves the session for the specs. login.spec.ts is the only spec that drives the login form.
  • Library: setup/library.setup.ts then finds the first game once, and specs open it by URL with gotoFirstRom().
  • Timeouts: the same locally and in CI (45s per test), so a test passes or fails the same way in both. They switch off while debugging.
  • App errors: if an /api call returns 5xx or the app throws, the test fails at once and names the request (for example GET /api/roms returned 500) instead of timing out on an element.
  • Output: everything the suite writes goes under .output/, through the paths in support/output.ts.
  • Checks: npm run typecheck covers the suite through tsconfig.node.json (typecheck:scripts checks just that project), and lint rules live in eslint.e2e.config.js.
  • Changing the suite itself: see AGENTS.md.