Testing & CI
Unit tests
Section titled “Unit tests”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.
pnpm vitest run # run all tests oncepnpm vitest run --watch # watch modepnpm vitest run tests/lib/auth # specific directoryUnit 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.
Build smoke tests
Section titled “Build smoke tests”scripts/smoke-build-check.mjs scans the SSR output directory (.netlify/build) for two regressions:
- Registry singleton split —
createRegistryappearing 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). - Registration tree-shaken away — if
registerSection(...)orregisterSchema(...)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:
pnpm buildnode scripts/smoke-build-check.mjsThree 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.
| Gate | Runs in | What it catches |
|---|---|---|
node scripts/check-admin-isolation.mjs | CI, pnpm check | A 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.mjs | CI, pnpm check | Module-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.mjs | CI, pnpm check, sync-template.yml | Structural 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) | CI | Layer 2 of admin isolation: builds with no dist/ present — the exact Netlify failure mode. Type-only imports erase and pass. |
pnpm --filter portal-admin test | CI | The admin unit suite under its own node-environment config. |
pnpm run build:packages | CI, publish.sh | All six packages build in dependency order; each emits declarations with tsc, so a package type error fails here too. |
pnpm typecheck | CI, publish.sh, pnpm check | Type 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 smoke | CI | The MCP function’s Netlify v2 emission, and that the bundle imports under plain Node. |
npx vitest run | CI, publish.sh, pnpm check | The unit suite. |
pnpm build + node scripts/smoke-build-check.mjs | CI, publish.sh | Registry 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-card | CI | The custom-section channel end to end: the ProductCard section is registered and its markup survives into SSR output. |
pnpm --filter portal-docs build | CI | Broken internal links and malformed frontmatter on this docs site (Starlight/Pagefind fail the build). |
pnpm --filter portal-marketing build | CI | The marketing site (drawn.guide) still builds — its only gate. |
Branch is main, working tree clean, workspace:* deps queued in order | publish.sh only | Publishing from a feature branch, from uncommitted state, or a package ahead of a dependency it needs on npm. |
e2e/*.spec.ts (Playwright) | local-only | Browser behaviour against a live portal — see below. |
Local-only e2e
Section titled “Local-only e2e”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.
| Spec | Needs | Recorded run status |
|---|---|---|
collab.spec.ts | Live portal + two seeded editor accounts | No recorded status. |
page-groups.spec.ts | Live portal + editor account | Never run against a live portal (CLAUDE.md); the only automated coverage for nav drag-and-drop, since jsdom cannot run atlaskit. |
document-viewer.spec.ts | Live portal | Run 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 Supabase | No recorded status. |
What the smoke test does NOT yet catch
Section titled “What the smoke test does NOT yet catch”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 acontainersection at multiple widths would cover this).