Browser Tests
The template has opt-in browser tests, in e2e/. They take the php and app images this branch just built, start them in a throwaway stack, and use the site in Chromium the way visitors and editors do. When they're on, every deploy waits for them, so a build that breaks logging in or publishing never reaches a site (#157, 3fd2c01, 8c36f8c). Since template 2.0.0-alpha.34 · release notes
The template's e2e/README.md is the short version of this page.
What they check
The template's tests cover:
- every page in the sitemap renders and hydrates;
- a public page is served from the cache on its second request;
- a redirecting route answers with a
308; - the loading bar shows while a page loads after a client-side navigation;
- a page's OG image renders;
- an admin can log in and out;
- an admin can edit and publish a text component, and an anonymous visitor then sees it;
- an admin can upload and publish an image, and a visitor then sees it;
- a password reset email links to a working reset page.
A run also fails when:
- an API write gets a
405,406or415, which means the front end and the API disagree about a method or a content type; - the page throws an uncaught error;
- the API sent an error mail, meaning it answered a request with a
5xxthat the UI may have hidden; - not every listed test ran and passed (
check-all-ran.mjs), so a skipped or filtered-out test can't pass a run.
Every timeout is bounded: 90 seconds a test, 30 seconds a navigation, 15 seconds an action and 10 seconds an assertion.
browser tests job, with the list of tests.Turning them on
Set the CI variable CWA_CI_E2E_ENABLED to "true" (see CI/CD Variables → Pipeline flags). Nothing runs until you do. CWA_CI_TEST_DISABLED="true" skips them along with the other tests.
- GitLab: a
browser testsjob in theteststage, afterbuild apiandbuild app. The review, staging, canary and production jobsneeds:it, so none of them runs when it fails. It runs Docker in Docker (docker:27-dind). - GitHub: the reusable workflow
.github/workflows/browser-tests.yml.ci.ymlcalls it after the builds, before review and staging, andproduction.ymlcalls it onmain's images before the deploy. A failed run stops those deploys. It has a 30-minute limit.
The Playwright report, with traces, screenshots and videos of failed tests, is kept as a job artifact for 14 days. GitLab also reads its JUnit file, so the results show in the pipeline's test report.
The test stack
e2e/run.sh starts e2e/compose.yaml, which has:
- the branch's php and app production images (
frankenphp_prodandprod); - PostgreSQL on
tmpfs, so every run starts empty; - a Cloud Storage emulator for uploads, reached through php's
GCLOUD_API_ENDPOINT; - Mailpit, which catches every email, such as the password reset;
- the Playwright runner, built from
e2e/Dockerfile.
It makes new secrets and a new JWT key pair for each run, waits for the API (migrations run on boot), loads the fixtures, flushes the HTTP cache and waits for the site. Then it runs the tests one at a time, and stops and deletes the stack.
The stack publishes no ports, so it can run beside your dev stack.
GCLOUD_API_ENDPOINT is the only change the tests need in the API: GoogleCloudStorageFactory uses an emulator, with no credentials, when it's set. Leave it unset on every deploy, where empty means Google.
Running them locally
You need Docker and openssl. Build the two production images, then run the tests against them:
docker build --target frankenphp_prod -t cwa-e2e-php:local api
docker build --target prod -t cwa-e2e-app:local app
E2E_PHP_IMAGE=cwa-e2e-php:local E2E_APP_IMAGE=cwa-e2e-app:local sh e2e/run.sh
| Variable | What it does |
|---|---|
E2E_PHP_IMAGE / E2E_APP_IMAGE | Required. The php and app images to test. CI passes this branch's tags |
E2E_ARGS | Runs part of the suite: a file, such as E2E_ARGS="tests/admin.spec.ts", or --grep …. The every-test-ran check is skipped then |
E2E_KEEP_STACK | true leaves the stack running afterwards and prints the admin password. Stop it with docker compose -f e2e/compose.yaml down -v |
The reports land in e2e/playwright-report/ and e2e/test-results/. When a run fails, the script prints the last 80 lines of the php and app logs.
DOCKER_DEFAULT_PLATFORM=linux/amd64 to test images from your CI registry, which builds them for amd64 only. On main after template 2.0.0-alpha.35, not yet released (8949efe)Writing your own tests
Add your project's own flows under e2e/tests/, as *.spec.ts files.
import { expect, logIn, test } from './fixtures'
test('an admin sees the contact page, and so does a visitor', async ({ page, visitor }) => {
await logIn(page)
await page.goto('/contact')
await expect(page.getByRole('button', { name: 'Edit' })).toBeVisible()
await visitor.goto('/contact')
await expect(visitor.getByRole('heading', { name: 'Contact us', exact: true })).toBeVisible()
})
- Import
testandexpectfromtests/fixtures.ts, never from@playwright/test. Itspagefixture is what fails a test on a refused API write or an uncaught error. Importing types from@playwright/testis fine. logIn(page)signs in as the admin. The password is made for each run and loaded with the fixtures.- Use the
visitorfixture to see a page as an anonymous visitor, beside the signed-inpage. It has its own browser context, is checked the same way aspage, and is closed after the test. Onmainafter template 2.0.0-alpha.35, not yet released (43ece66) - Tests share one database and run one at a time. A test that edits content should use its own unique text, such as one with
Date.now()in it, so it can't match another test's.
The template's editing test publishes straight after the last keystroke. Since @cwa/nuxt 2.0.0-alpha.16, Publish sends the waiting save first (cwa-nuxt-module#394), so the test checks that this still works.
Type checks, lint and conventions
The tests are type-checked and linted before they run, and every rule is an error, so a type or lint error fails the browser tests job. Playwright only strips types, so nothing else would catch them. On main after template 2.0.0-alpha.35, not yet released (43ece66, 08b40db)
tsc, with the stricte2e/tsconfig.json;- ESLint, with
e2e/eslint.config.mjs:@nuxt/eslint-config's stylistic rules (the module's style),eslint-plugin-playwright, and typescript-eslint'sno-floating-promises, which also catches a missingawaiton your own helpers.
Run them yourself in e2e/. npm ci also gives your editor the types:
cd e2e
npm ci
npm run typecheck
npm run lint
npm run lint:fix
The rules come with these conventions, from the template's e2e/README.md:
- Await every Playwright call and every helper that returns a promise.
- Use retrying assertions:
await expect(locator).toBeVisible(), notexpect(await locator.isVisible()). - No fixed waits and no forced clicks.
waitForTimeoutandforce: trueare errors. Wait for the response, URL or element the test is about. - Find elements the way a person does:
getByRole,getByLabelorgetByText, with exact names. Use a CSS selector only where the markup gives nothing better, with a comment that says why.
e2e/Dockerfile and @playwright/test in e2e/package.json on the same version.Reading Lighthouse Scores
Why a fast CWA page can score anywhere from 60 to 96 on mobile Lighthouse, how to read the result, and what a project should check first — third-party scripts, icons, images and its own front-end bundle.
Faster Functional Tests
When a project's functional test suite slows CI down — measure where the time goes, reset the database without rebuilding the schema, and shard the job by test.