End-to-end and integration tests for the Languages Learner web app.
In this repo, "integration tests" are full-page screenshot (visual regression) tests: each spec opens a page and compares it against committed baselines in both light and dark themes.
- Live app
- Storybook
- Package docs: docs/packages/web-e2e.md
integration/playwright.config.ts— Playwright config (testDir=integration/tests).integration/tests/**— specs and their*-snapshots/baselines (landing,office,dictionary).pages/**— Page Object Models.core/**— shared login/setup helpers.integration/.env.example— template for the local env.
| Script | What it does |
|---|---|
test:integration |
Runs the Playwright screenshot suite against BASE_URL (native). |
test:integration:update |
Same, but regenerates the screenshot baselines (--update-snapshots). |
test:integration:docker |
Runs the suite inside the CI-matched Linux Docker image. |
test:integration:update:docker |
Regenerates the baselines inside that Docker image (see below). |
typecheck |
tsgo --noEmit. |
lint / lint:fix |
ESLint (check / autofix). |
Run a single script with a filter, e.g. pnpm --filter web-e2e test:integration.
- Copy
integration/.env.example→integration/.envand fill in:BASE_URL— a running server, e.g.http://localhost:5173(startapps/webfirst withpnpm --filter app-web dev) or a staging URL.AUTH_LOGIN/AUTH_PASSWORD— a seeded, confirmed test user.global_setupperforms a real UI login and persists the session as storage state.
pnpm --filter web-e2e test:integration.
Baselines are Linux-rendered (
…-chromium-linux.png, hardcoded in the config). On Windows/macOS a native:updatewould overwrite them with locally-rendered images that then mismatch on the CI Linux runner — so never update baselines natively. Use the Docker flow below instead.
This mirrors how uikit component tests are updated: baselines are (re)generated inside a Docker
image that matches the CI runner (Ubuntu + the same fonts + pinned Playwright, see
Dockerfile.tests.component), while CI itself runs the comparison directly on ubuntu-latest
without Docker.
- Make sure Docker Desktop is running.
- Put
AUTH_LOGIN/AUTH_PASSWORD(a seeded, confirmed test user) inintegration/.env.BASE_URLthere is only for native runs — the Docker flow overrides it. - Start the app natively on the host (it must not be containerised):
pnpm --filter app-web dev(serveshttp://localhost:5173). - Regenerate the baselines in the container:
pnpm --filter web-e2e test:integration:update:docker.
The container reaches the host dev server via host.docker.internal:5173 (set as BASE_URL by the
playwright-ct service in docker-compose.yml; override with the E2E_BASE_URL env var to point at
a different URL, e.g. staging). The updated *-snapshots/*.png are written back to the working tree
— review and commit them. Run test:integration:docker (without :update) to verify against the
committed baselines the same way.
Backend traffic (<baseURL>/api/**) is mocked via committed …-linux.har files next to each
snapshot (the mockNetwork fixture from @languages-learner/app-integration-tests-utils, which
also sanitizes the HAR). The :update scripts refresh the HAR together with the screenshots —
test:integration:update sets UPDATE=1 (via cross-env), so test:integration:update:docker
re-records and sanitizes the HAR while regenerating the PNGs. Commit the refreshed *.har/*.dat
alongside the screenshots. Plain test:integration / :docker (and CI) only replay the
committed HAR.
Because Playwright matches HAR entries by exact request URL (host included) and the backend is
served under the app's own origin — which differs between local recording and the dynamic CI stage
domain — the committed HAR is host-normalized: the recording origin is rewritten to the stable
placeholder http://mocked.local, and on replay a throwaway *.replay.har copy is rewritten back
to the current baseURL. Login itself is not mocked (it goes to Supabase auth directly).
After this change the previously committed HARs are stale (they targeted the old direct-Supabase URLs). Re-run
test:integration:update:dockeronce to record the/apitraffic, then commit the regenerated*.har/*.dat— until then CI has nothing to replay.
Committed HARs must carry no secrets (session cookies, bearer tokens, API keys). The mockNetwork
fixture sanitizes them automatically on record; pnpm sanitize-har re-sanitizes every HAR in the
repo on demand, and CI enforces it via pnpm har:check (re-runs the sanitizer and scans for tokens
— see scripts/check-har-sanitized.ts). Extend
@languages-learner/har-sanitizer if a new field ever leaks.
The reusable workflow .github/workflows/e2e-tests.yml runs test:integration on pull requests,
after the staging deploy, only when the web project is affected (gated on the
check-deploy-affected job). It passes the freshly-deployed stage URL as BASE_URL and feeds the
test account from the TEST_USER_AUTH_LOGIN / TEST_USER_AUTH_PASSWORD repository secrets (mapped
to AUTH_LOGIN / AUTH_PASSWORD). On failure the Playwright HTML report is uploaded as an
artifact for inspecting screenshot diffs.
When the UI changes, regenerate the baselines with the Docker flow above
(test:integration:update:docker) and commit the updated *-snapshots/*.png in the same PR.