Testing
Vitest and Playwright layout, conventions, dogfooding and known gaps.
Unit and behavior tests (Vitest)
Live in __tests__/ directories beside the code they cover; most are in
apps/web/src/server/__tests__/.
Behavior tests use real infrastructure, not module mocks. The server functions run for real against:
- pglite (
apps/web/src/test/pglite-db.ts'screateTestDb) — an in-memory Postgres with the real schema pushed into it, so every query, constraint and the repo lock's lease SQL are real. - the in-memory git store — R2 isn't configured in tests, so
git-fs.tsbacks the same storage stack production uses with an in-memory object store. - a faked session —
apps/web/src/test/app-harness.ts'ssessionModule, withsignIn(user)/signOut()choosing the caller,createUsers(...)for fixtures andrejectsNotFound(promise)for 404 assertions.
The setup at the top of such a file:
vi.mock("#/db", async () => ({
db: await (await import("../../test/pglite-db")).createTestDb(),
}));
vi.mock("@tanstack/react-start", () => import("../../test/server-test-utils"));
vi.mock("#/server/session", async () => (await import("../../test/app-harness")).sessionModule);vi.mock factories run before aliases resolve, so they import helpers by
relative path. Write new server tests this way rather than mocking modules:
a mock silently drifts from the code it replaces.
git-user-lifecycle.test.ts goes one layer further: git-fs-s3's real fetch
store talks to a fake S3 endpoint (helpers/fake-s3-fetch.ts), so request
signing, ListObjectsV2 parsing and server-side copy run too; it clones and
pushes with isomorphic-git over a loopback server running serveGitRequest.
Native git (pnpm smoke:git) — apps/web/scripts/smoke/git-cli.smoke.test.ts
drives the real git CLI against the real /api/git/$ route on a loopback
server: clone, push, incremental fetch, repack, web commits, force-push,
branch/tag create and delete, default-branch protection, merges, bad tokens,
and "no loose objects ever written". Every git call runs with a scratch
HOME/GIT_CONFIG_GLOBAL and GIT_CONFIG_NOSYSTEM=1 — it never reads or
writes your own git configuration. Kept out of pnpm test.
Notable files:
repositories.test.ts,collaboration.test.ts— the server functions end to end: access rules, privacy (no emails leave the server), unsafe names and paths, stars, collaborators, rename/delete, issues/PRs/comments, the shared#Nsequence, merging, search, profiles, PAT auth.git-http.test.ts— URL routing, the streamed body cap, auth challenges.git-integration.test.ts,git-repack.test.ts— git reads and writes throughgit-read.ts/git-write.ts, and pack consolidation.git-repo-storage-ops.test.ts—withRepositoryLock(serialization, expiry takeover, timeout, heartbeat renewal),afterRepositoryPush's cache invalidation, storage rename/delete.git-ref-name.test.ts,git-storage-naming.test.ts— the validators and key construction that stop path traversal (see security.md).repo-access.test.ts—RepositoryAccessand its handler helpers.rate-limit.test.ts— the shared rate-limit counters against real SQL (the atomic upsert's allowance, per-key isolation, window rollover), plus an assertion that the auth config uses that storage rather than the in-memory default.csp.test.ts— the Content-Security-Policy's directives, the theme script's hash being the hash of the constant the document renders, and the nonce being unguessable and distinct per call.repo-path.test.ts— resolving a relative markdown target against its containing file, including refusal to climb above the repository root.
Run every workspace suite with pnpm test; run one web file with
pnpm --filter @pushstack/web test src/server/__tests__/repo-access.test.ts.
Use pnpm --filter @pushstack/web test:watch or test:coverage for web watch/coverage runs.
Documentation integrity tests run with pnpm --filter @pushstack/docs test;
they check page links, navigation targets, reference examples and searchable metadata.
Config: apps/web/vitest.config.ts — jsdom by default (server suites opt into
@vitest-environment node), typecheck-on-test, v8 coverage.
What's not covered yet
Few component render tests: coverage is server logic plus pure client
helpers (e.g. MarkdownRenderer's isSafeHref/isSafeImageSrc). The
high-churn routes (PR pages, tree/blob/commit viewers, upload, comment forms)
are covered only by e2e.
End-to-end tests (Playwright)
Live in apps/web/e2e/: auth.spec.ts, navigation.spec.ts, repositories.spec.ts,
and git-cli.spec.ts (that same smart-HTTP path, driven by the real git
binary rather than isomorphic-git).
pnpm test:e2e # headless
pnpm test:e2e:ui # Playwright's UI mode
pnpm test:e2e:headed # headed browserConfig: apps/web/playwright.config.ts. Two things worth knowing before touching this
suite:
- It loads
.env.localdirectly (viadotenv) so both the dev server it spins up and the test files themselves (which need direct DB access to verify things like test-user email state) see the same environmentpnpm devwould. It creates and deletes users, so point.env.localat a dev database before running it — never production. The helpers refuse to run against a non-localDATABASE_URLunlessE2E_ALLOW_REMOTE_DB=1is set, because.env.localis the file most likely to be pointed at a real one. - It runs serially, one worker (
fullyParallel: false, workers: 1) — deliberately, not for lack of trying parallelism elsewhere in this codebase. All specs share one dev server, one real database, and one auth rate limiter (20 requests/60s, configured inapps/web/src/lib/auth.ts). Running workers in parallel caused cross-file request contention that looked like flaky rate-limit false positives, and let client hydration lag behind test input under load. If you're adding e2e coverage and tempted to speed things up with more workers, this is why that's currently off.
Dogfooding the issue and pull-request flow
The unit and e2e suites cover the pieces; this is the manual pass that walks the flow the way a person does, on a real deployment:
- Open an issue, comment on it, close it, reopen it.
- Open a pull request, comment on it, close it, reopen it, merge it.
- Put
Closes #3in a pull request body and confirm merging it closes issue 3 in the same repository.
Step 3 is the one worth doing by hand: closing keywords, the shared #N
sequence and the merge all have to agree, and a passing unit test for each of
them does not prove they agree with each other.
Lint / format / typecheck
pnpm check # Biome lint + format check (CI gate)
pnpm format # Biome format, writes changes
pnpm typecheck # tsc --noEmitBiome (not ESLint/Prettier) — config in biome.json.