Skip to content

Testing & CI

Tests use Vitest + @testing-library/react. They live in tests/ mirroring the src/ structure. Coverage targets: schemas, the registry, the loader (mergeSiteContent), nav generation, and key components.

Terminal window
pnpm vitest run # run all tests once
pnpm vitest run --watch # watch mode
pnpm vitest run tests/lib/auth # specific directory

Unit tests run in jsdom. They mock modules via vi.mock("@/...") and are structurally unable to catch SSR bundling issues (registry singleton splits, tree-shaken registration). That gap is covered by the build smoke tests.

apps/admin has its own Vitest config (node environment, src/**/*.test.ts); the root config excludes apps/admin/** so those files run exactly once, via pnpm --filter portal-admin test.

scripts/smoke-build-check.mjs scans the SSR output directory (.netlify/build) for two regressions:

  1. Registry singleton split — createRegistry appearing in more than one SSR chunk means Rollup produced two independent registry instances. Sections registered in one are invisible to the other at runtime (causes “At least 2 section schemas must be registered” in production).
  2. Registration tree-shaken away — if registerSection(...) or registerSchema(...) calls are absent from the SSR output entirely, the registry is empty at runtime and no sections will render.

The script also accepts --expect <token> flags to assert that specific strings appear in the SSR output (used by the apps/dev custom-section acceptance test).

Run manually after building:

Terminal window
pnpm build
node scripts/smoke-build-check.mjs

Three things run gates: CI (.github/workflows/ci.yml, every push and pull request, in the order below), scripts/publish.sh (before any package reaches npm), and pnpm check (the local pre-commit composite: the three static checks, pnpm typecheck, then vitest run). Anything that needs a live portal is local-only.

pnpm typecheck and the unit suite both resolve sibling packages through their built dist/, so run pnpm build:packages first — CI and publish.sh both do.

GateRuns inWhat it catches
node scripts/check-admin-isolation.mjsCI, pnpm checkA runtime (value) import of @drawnagency/* in apps/admin/src/ — admin deploys without the package dist, so it would fail module resolution on Netlify.
node scripts/check-config-graph-browser-safety.mjsCI, pnpm checkModule-scope Node APIs or unguarded env access anywhere in portal.config.mjs’s import graph, which is hydrated in the browser.
node scripts/validate-template.mjsCI, pnpm check, sync-template.ymlStructural invariants of template/ (required files incl. AGENTS.md/CLAUDE.md, config import path, .npmrc/.gitignore entries, canonical index.json, empty nav.json) before the sync force-pushes it downstream.
pnpm --filter portal-admin build (before build:packages)CILayer 2 of admin isolation: builds with no dist/ present — the exact Netlify failure mode. Type-only imports erase and pass.
pnpm --filter portal-admin testCIThe admin unit suite under its own node-environment config.
pnpm run build:packagesCI, publish.shAll six packages build in dependency order; each emits declarations with tsc, so a package type error fails here too.
pnpm typecheckCI, publish.sh, pnpm checkType errors in primitives, github, auth-supabase, assets-r2, authoring (--noEmit on their build programs), platform, apps/mcp, core’s islands and routes programs, e2e/, and apps/admin (astro check).
pnpm --filter portal-mcp build + bundle-load smokeCIThe MCP function’s Netlify v2 emission, and that the bundle imports under plain Node.
npx vitest runCI, publish.sh, pnpm checkThe unit suite.
pnpm build + node scripts/smoke-build-check.mjsCI, publish.shRegistry singleton split, tree-shaken registration, deck runtime leaking onto viewer chunks.
pnpm --filter portal-dev build + smoke-build-check.mjs --expect product_card --expect data-portal-product-cardCIThe custom-section channel end to end: the ProductCard section is registered and its markup survives into SSR output.
pnpm --filter portal-docs buildCIBroken internal links and malformed frontmatter on this docs site (Starlight/Pagefind fail the build).
pnpm --filter portal-marketing buildCIThe marketing site (drawn.guide) still builds — its only gate.
Branch is main, working tree clean, workspace:* deps queued in orderpublish.sh onlyPublishing from a feature branch, from uncommitted state, or a package ahead of a dependency it needs on npm.
e2e/*.spec.ts (Playwright)local-onlyBrowser behaviour against a live portal — see below.

e2e/playwright.config.ts hosts four specs, none wired into CI. pnpm e2e runs everything except the @colors-tagged spec; pnpm e2e:colors runs only that one. Every spec file must be named in the config’s testMatch — an unlisted spec is silently skipped.

SpecNeedsRecorded run status
collab.spec.tsLive portal + two seeded editor accountsNo recorded status.
page-groups.spec.tsLive portal + editor accountNever run against a live portal (CLAUDE.md); the only automated coverage for nav drag-and-drop, since jsdom cannot run atlaskit.
document-viewer.spec.tsLive portalRun only against a Vite harness, not a real site (docs/backlog.md § Document viewer follow-ups).
colors-island.spec.ts (@colors)apps/dev dev server only, no SupabaseNo recorded status.

The following gaps are documented in scripts/smoke-build-check.mjs and .claude/rules/ssr-netlify-build.md § Build Smoke Tests as known TODOs:

  • Empty import.meta.env.* values for required env vars at build time.
  • Container-query layout collapse at specific @-breakpoints (jsdom cannot compute container queries; a Playwright check rendering a container section at multiple widths would cover this).