Zeno

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.ts

Create the package.json:

packages/e2e/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/test
npx playwright install --with-deps

3. List the apps you test

Add every app you write tests for as a workspace dependency:

packages/e2e/package.json
{
  "devDependencies": {
    "@yourorg/webapp": "workspace:*"
  }
}

4. Configure Playwright

Spread baseConfig and add a webServer entry per app:

packages/e2e/playwright.config.ts
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:

packages/e2e/package.json
{
  "scripts": {
    "verify-deps": "zeno-e2e verify-deps"
  }
}
turbo.json
{
  "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:

FieldLocallyIn CI
testDir./tests./tests
projectsone chromium project (Desktop Chrome)same
fullyParalleltruetrue
forbidOnlyoffon (a stray test.only fails the build)
retries03
timeout120s30s
reporterlisthtml
use.traceon-first-retryon-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:

packages/e2e/tests/webapp/home.spec.ts
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-deps

It 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:

FlagDefaultWhat it does
--apps-dir../../appsDirectory that contains the apps
--tests-dir./testsDirectory with one folder per tested app
--package-json./package.jsonpackage.json whose deps must list the apps

Common mistakes

  • Skipping npx playwright install. npm install brings 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-deps catches this.