TurboJet
Recipes

E2E testing

Run Playwright end-to-end tests against the SaaS app.

TurboJet integrates Playwright for end-to-end tests against the SaaS app (@repo/saas).

Full monorepo required

Run these commands in the full TurboJet monorepo that contains apps/saas. This docs site is published from a marketing/docs host that does not include the SaaS app or Playwright suite.

Marketing has no Playwright suite

There is no apps/marketing/e2e (or tests/) Playwright setup. E2E covers SaaS browser flows (auth, dashboard) only.

Why Playwright

Playwright is a mature open-source e2e tool with a strong developer experience. It is fast and reliable, and can also be used for component testing. TurboJet uses it for full browser flows with Mailpit for email OTP and magic links.

Unit tests (Vitest) are covered in Testing.

Prerequisites (local)

  1. Start Docker services (Postgres + Mailpit):
pnpm docker:up
  1. Ensure root .env includes local SMTP / Mailpit (see .env.example):
SMTP_HOST=localhost
SMTP_PORT=1025
MAILPIT_URL=http://localhost:8025
SEED_EMAIL=[email protected]
E2E_USER_EMAIL=[email protected]

When SMTP_HOST is set, @repo/mail sends via SMTP (Mailpit) instead of Resend.

  1. First-time browser install:
pnpm --filter @repo/saas exec playwright install chromium

Running e2e tests

From the monorepo root:

pnpm test:e2e
# equivalent to:
pnpm --filter @repo/saas test:e2e

Open the Playwright UI:

pnpm --filter @repo/saas test:e2e:ui

Show the last HTML report:

pnpm --filter @repo/saas test:e2e:report

How the server starts

By default, Playwright’s webServer builds the SaaS app in production mode and runs pnpm start on port 3000, waiting on /api/status. Locally it can reuse an already-running server when not in CI.

To point at an existing server and skip the build/start step, set:

E2E_BASE_URL=http://localhost:3000

(NEXT_PUBLIC_SAAS_URL is also accepted as a base URL fallback; default is http://localhost:3000.)

Global setup

Before specs run, e2e/setup/global-setup.ts:

  1. Runs pnpm --filter @repo/database db:migrate:deploy
  2. Runs pnpm --filter @repo/database db:seed (seeds SEED_EMAIL and E2E_USER_EMAIL)

Auth storage state

The chromium-setup project runs e2e/setup/auth.setup.ts, which signs in via OTP and writes storage state to e2e/.playwright/auth/user.json. Specs matching *.authenticated.spec.ts load that state automatically.

Running e2e tests in CI

Workflow: .github/workflows/e2e-saas.yml

TriggerWhen it runs
push to mainAlways
workflow_dispatchAlways
Pull requestOnly if labeled e2e or e2e-saas

CI does not require GitHub Action secrets for the default suite. It copies committed .env.ci to .env (placeholders for auth, payments, AI, S3, etc., plus DATABASE_URL for the CI Postgres service).

Workflow services:

  • Postgres 15 — ci / ci / ci on 5432
  • Mailpit — SMTP 1025, UI 8025

Steps install Chromium with deps, then run pnpm --filter @repo/saas test:e2e. Artifacts:

  • HTML report: apps/saas/e2e/playwright-report (14 days)
  • Traces / test results on failure: apps/saas/e2e/test-results (7 days)

Main CI (.github/workflows/ci.yml) does not run e2e.

If you add integrations that need real credentials in CI, put them in GitHub Settings → Secrets and variables → Actions and wire them into the workflow / .env.ci the same way you would for local .env.

Where tests live

E2E is colocated with the SaaS app:

apps/saas/
├── playwright.config.ts
└── e2e/
    ├── constants.ts          # baseURL, paths, e2e user, Mailpit URL, auth state path
    ├── env.ts                # loads root ../../.env
    ├── helpers/
    │   └── mailbox.ts        # Mailpit API: clear, wait, extract OTP / magic link
    ├── pages/
    │   ├── sign-in.page.ts
    │   └── dashboard.page.ts
    ├── setup/
    │   ├── global-setup.ts   # migrate + seed
    │   └── auth.setup.ts     # OTP login → storage state
    ├── specs/
    │   ├── auth.sign-in.spec.ts
    │   └── dashboard.authenticated.spec.ts
    ├── .playwright/          # gitignored (auth state)
    ├── playwright-report/    # gitignored
    └── test-results/         # gitignored

Config highlights (apps/saas/playwright.config.ts):

  • testDir: ./e2e
  • workers: 1, fullyParallel: false
  • Projects: chromium-setup → chromium (unauthenticated specs) and chromium-authenticated (*.authenticated.spec.ts + storageState)
  • Reporter: list + HTML under e2e/playwright-report

Writing e2e tests

Add a new suite under apps/saas/e2e/specs/ as a *.spec.ts file. Playwright picks it up automatically.

Unauthenticated / public flow:

import { expect, test } from "@playwright/test";

test.describe("some feature", () => {
	test("should do something", async ({ page }) => {
		await page.goto("/sign-in");

		await expect(
			page.getByRole("heading", { name: /sign in/i }),
		).toBeVisible();
	});
});

Authenticated flow — name the file *.authenticated.spec.ts so it runs in the chromium-authenticated project with saved session:

import { expect, test } from "@playwright/test";
import { paths } from "../constants";
import { DashboardPage } from "../pages/dashboard.page";

test.describe("dashboard", () => {
	test("loads for signed-in user", async ({ page }) => {
		const dashboard = new DashboardPage(page);
		await page.goto(paths.dashboard);
		await dashboard.waitForReady();
		await expect(page).toHaveURL(/dashboard/);
	});
});

Prefer page objects under e2e/pages/ and Mailpit helpers under e2e/helpers/mailbox.ts for email-driven auth.

Learn more in the Playwright documentation.

Current coverage

SpecWhat it covers
auth.sign-in.spec.tsSign-in tabs; email OTP sign-in; magic-link sign-in (via Mailpit)
dashboard.authenticated.spec.tsAuthenticated dashboard smoke load (storage state)

No billing, admin, onboarding, or marketing e2e specs yet.

Quick command cheat sheet

CommandPurpose
pnpm docker:upPostgres + Mailpit
pnpm test:e2eRun SaaS e2e (root)
pnpm --filter @repo/saas test:e2e:uiPlaywright UI
pnpm --filter @repo/saas test:e2e:reportOpen HTML report
pnpm --filter @repo/saas exec playwright install chromiumInstall browser (once)
PR label e2e or e2e-saasEnable e2e workflow on a pull request

On this page