E2E Testing
End-to-end testing based on Playwright
In Zeno, E2E tests live in a single package at packages/e2e. They're centralised because e2e tests often span several apps at once, so the setup in packages/e2e/playwright.config.ts waits for every app to be running and ready before the suite starts.
Setup
The examples below use two placeholders: @yourorg for your monorepo's npm scope, and webapp for one of the apps you test. Swap in your own names.
1. Create the e2e package
packages/e2e/
├── playwright.config.ts # extends @zeno-lib/e2e's baseConfig
├── package.json # depends on @zeno-lib/e2e and each tested app
└── tests/
└── webapp/ # tests for apps/webapp
└── home.spec.tsCreate the package.json:
{
"name": "@yourorg/e2e",
"type": "module",
"scripts": {
"e2e": "playwright test",
"e2e:watch": "playwright test --ui"
}
}2. Install Playwright and the preset
From inside packages/e2e, add @zeno-lib/e2e with its @playwright/test peer dependency, then download the browsers:
npm install --save-dev @zeno-lib/e2e @playwright/testnpx playwright install --with-deps3. List the apps you test
Add every app you write tests for as a workspace dependency:
{
"devDependencies": {
"@yourorg/webapp": "workspace:*"
}
}4. Configure Playwright
Spread baseConfig and add a webServer entry per app:
import { defineConfig } from "@playwright/test"
import { baseConfig } from "@zeno-lib/e2e/config"
export default defineConfig({
...baseConfig,
webServer: [
{
// Command that starts the app you want to test
command: "pnpm --filter @yourorg/webapp start -- --port 5002",
url: "http://localhost:5002",
reuseExistingServer: !process.env.CI,
},
],
})Each webServer entry starts one app and waits for its url. Add one entry per app you test, each on the port that app serves.
5. Configure Turborepo
Add a verify-deps script, then make the e2e task build the tested apps and run that check before the tests:
{
"scripts": {
"verify-deps": "zeno-e2e verify-deps"
}
}{
"tasks": {
"e2e": {
"dependsOn": ["^build", "@yourorg/e2e#verify-deps"]
},
"verify-deps": {
"cache": false
}
}
}^build builds every dependency, and verify-deps fails if one is missing. Now turbo run e2e builds the apps, verifies the wiring, and runs the tests.
The config preset
@zeno-lib/e2e/config exports baseConfig, a plain Playwright config object you spread and override. It sets:
| Field | Locally | In CI |
|---|---|---|
testDir | ./tests | ./tests |
projects | one chromium project (Desktop Chrome) | same |
fullyParallel | true | true |
forbidOnly | off | on (a stray test.only fails the build) |
retries | 0 | 3 |
timeout | 120s | 30s |
reporter | list | html |
use.trace | on-first-retry | on-first-retry |
It intentionally leaves webServer unset, because only you know how to start your apps. Everything else you need (defineConfig, devices, test, expect) comes straight from @playwright/test.
Writing tests
Put test files under tests/<app>/, where <app> matches the app's folder under apps/. Import test and expect from @playwright/test:
import { expect, test } from "@playwright/test"
test("home page loads", async ({ page }) => {
const response = await page.goto("http://localhost:5002")
expect(response?.status()).toBe(200)
})Run them with pnpm e2e inside the package, or turbo run e2e from the root to build the apps first.
Keeping app dependencies in sync
Every app with a tests/<app>/ folder must be a dependency of the e2e package. Otherwise Turborepo has no reason to rebuild it, and the tests run against a stale build. zeno-e2e verify-deps (installed with @zeno-lib/e2e) enforces this.
See step 5 of Setup to wire it automatically. It also works on its own:
zeno-e2e verify-depsIt matches each tests/<app>/ folder to apps/<app>, and if an app is missing from the e2e package's dependencies it prints the devDependencies to add and exits. It can accept different configurations:
| Flag | Default | What it does |
|---|---|---|
--apps-dir | ../../apps | Directory that contains the apps |
--tests-dir | ./tests | Directory with one folder per tested app |
--package-json | ./package.json | package.json whose deps must list the apps |
Common mistakes
- Skipping
npx playwright install.npm installbrings the test runner but not the browsers, so every run fails at launch with "Executable doesn't exist". - Forgetting to list a tested app as a dependency. Turborepo then has no edge to rebuild it, and tests hit a stale build.
zeno-e2e verify-depscatches this.