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)
- Start Docker services (Postgres + Mailpit):
pnpm docker:up- Ensure root
.envincludes 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.
- First-time browser install:
pnpm --filter @repo/saas exec playwright install chromiumRunning e2e tests
From the monorepo root:
pnpm test:e2e
# equivalent to:
pnpm --filter @repo/saas test:e2eOpen the Playwright UI:
pnpm --filter @repo/saas test:e2e:uiShow the last HTML report:
pnpm --filter @repo/saas test:e2e:reportHow 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:
- Runs
pnpm --filter @repo/database db:migrate:deploy - Runs
pnpm --filter @repo/database db:seed(seedsSEED_EMAILandE2E_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
| Trigger | When it runs |
|---|---|
push to main | Always |
workflow_dispatch | Always |
| Pull request | Only 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/cion5432 - Mailpit — SMTP
1025, UI8025
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/ # gitignoredConfig highlights (apps/saas/playwright.config.ts):
testDir:./e2eworkers:1,fullyParallel:false- Projects:
chromium-setup→chromium(unauthenticated specs) andchromium-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
| Spec | What it covers |
|---|---|
auth.sign-in.spec.ts | Sign-in tabs; email OTP sign-in; magic-link sign-in (via Mailpit) |
dashboard.authenticated.spec.ts | Authenticated dashboard smoke load (storage state) |
No billing, admin, onboarding, or marketing e2e specs yet.
Quick command cheat sheet
| Command | Purpose |
|---|---|
pnpm docker:up | Postgres + Mailpit |
pnpm test:e2e | Run SaaS e2e (root) |
pnpm --filter @repo/saas test:e2e:ui | Playwright UI |
pnpm --filter @repo/saas test:e2e:report | Open HTML report |
pnpm --filter @repo/saas exec playwright install chromium | Install browser (once) |
PR label e2e or e2e-saas | Enable e2e workflow on a pull request |
Related
- Testing — Vitest unit tests