--- title: Testing, Validation & Codegen type: reference audience: human + agent status: current --- # Testing, Validation & Codegen The cross-cutting story of how the framework stays correct: the test suite, the contract validators, what is generated vs authored, and the SSR posture. The commands are the `scripts` in `package.json`; this groups them by what they are *for*. ## The verification loop (the short version) ```bash npm run check # types — svelte-kit sync && svelte-check (expect 0 errors) npm run test # the vitest suite (one run) npm run lint # prettier --check · npm run format to fix ``` For a component you also run the contract validators (below). A change is not done until `check` is clean and the relevant validators pass. ## Tests — two projects `vite.config.ts` defines **two vitest projects** (see CLAUDE.md → "Vitest Two-Project Structure"): - **client** — browser tests via Playwright, for `*.svelte.{test,spec}.{js,ts}`. This is where component providers are exercised with real runes + DOM. - **server** — Node environment for `*.{test,spec}.{js,ts}` (excludes the svelte tests). Pure engines, libs, and server logic. ```bash npm run test:unit # watch mode npm run test # one run npx vitest run src/uix/morfo/compile.test.ts # a single file npx vitest run -t "describe name" # by test name ``` Component providers carry their own `{name}-provider.svelte.test.ts` under the client project (a guard fails when an active provider ships without one — the test tree IS the coverage inventory); the reusable engines they consume live in `$libs/datagrid`, `$libs/forms`, `$libs/strings` and are tested there, not inside soma. See [`soma-architecture.md`](./architecture/soma-architecture.md) §6. ## Validation — what each script catches These catch classes of bug that `check` (types) and an HTTP 200 (SSR) miss. | Command | Catches | | --- | --- | | `npm run morfo:check` | the **real DOM vs the morfo contract** — navigates each demo and validates emitted `data-*` against the declaration. | | `npm run morfo:vocabulary` | **canonical-verb / vocabulary drift** in morfo events (e.g. `open\|closed`, declared verbs not in `SEMA_VERBS`). Hard-fails CI on declared-verb drift. | | `npm run component:audit` | the **acceptance matrix** of [`completion-checklist.md`](./guides/completion-checklist.md) — per-rule severity/applicability across all four layers + recipe + demo. | | `npm run perm:check` | **state-transition** bugs — cycles a component through its declared states (via `data-perm-step` annotations) and re-validates morfo after each. Catches reactivity loops and transition-time drift `morfo:check` can't. | | `npm run smoke` | **runtime / hydration** errors — walks every `+page.svelte` with Playwright and surfaces `pageerror`, `console.error`, missing translation keys, `Context "X" not found`, and `__uix_lang_missing__` markers. Needs `npm run dev` running. HTTP 200 is SSR only; smoke exercises client hydration. | | `npm run translations:check` | missing / malformed translation keys. | | `npm run docs:check` | **doc-corpus drift** — copied vocabulary counts vs the source consts, phantom fields (the legacy morfo text field; rejected API shapes), dependency claims vs `package.json`, the variant-vocab mirror in `component-audit.ts`, checklist↔audit rule-ID sync, and relative links (warn severity). Guards the "link the canon, never copy it" law of [`docs/authoring.md`](./authoring.md). | | `scripts/eidos-lint.ts` · `eidos-lint-all.ts` | classifies every `[data-*]` selector in eidos CSS as **morfo-backed / eidos-only / invalid** — drift between the morfo contract and the CSS. (Opt-in safety net; the architectural defense is the typed `semaSelector` builder — see the "Eidos drift defense" rule in CLAUDE.md.) | | `src/uix/contracts.test.ts` (vitest, via `npm run test`) | **catalogue invariants** that keep declarations / recipes coherent as the framework grows. Each fails on drift naming the offender, and excludes the active-dev-track set so it stays green for the maintained catalogue: **VG-8** every morfo is `as const satisfies Morfo`, never `: Morfo` (a `: Morfo` annotation widens the literal so the schema can't check it); **SYS-1 scope-drift** a component shipping an `eidos/components/{c}/` recipe declares `'eidos'` in `scope`; **A31** no per-item membership predicate (`isSelected` / `isItemPressed` / …) doing `.current.includes` (O(N²) — lift a `Set`, use `.has()`); **A30** `inputId` registered with the parent Field in the constructor, not wrapped in a `$effect`; **THEME-SYS-1** overlay z-index references the named `--z-index-overlay-*` scale, never a raw integer. | ## Codegen — what is generated vs authored Some surfaces are produced from a source of truth, not hand-maintained. Don't edit the output; edit the source and regenerate. | Command | Generates from | | --- | --- | | `npm run generate:eidos-css` | the eidos foundation/recipe CSS, from `EidosConfig.recipes`. (`generated/base.css` is output — regenerate, don't hand-edit.) | The `data-*` contract is **not** a generated doc — it is the morfo itself, validated live by `npm run morfo:check`. (A legacy `generate:contracts-docs` script and its output `DATA_ATTRS.md`, both fossils of the removed `terra` layer, were deleted.) The **morfo compiler** itself is the central codegen primitive: `compileMorfo(morfo)` turns a declaration into a `CompiledMorfo` (resolved attr / keyboard / action plans + the closed set of CSS selectors eidos may use), cached by morfo identity (WeakMap). Every consumer reads the compiled morfo, never the raw declaration. ```bash npm run eidos:purge # production: drop unreached foundation CSS (−46 to −55%) ``` ## SSR posture Components are **headless-functional without a DOM**. The architecture keeps SSR and headless tests working: - **`dom:false`** injects a shared `disabledDom` no-op consumed by Soma / Sema / Eidos — there is no silent fallback to direct DOM writes (see [`arts/adom/README`](../src/arts/adom/README.md) and `src/uix/contracts.ts`). - **`ActiveDom`** resolves the *owner* `document` / `window` (`getDocument(node)` / `getWindow(node)`), so it is correct under iframes, popups and happy-dom — not bound to the global `document`. - **Sema is ornamental** — `ActiveUix.events` (the perceptual engine) is optional; with no engine, `SomaRuntime.trigger()` skips the emit and the component stays functional. So sound/haptic-free, audio-disabled and server environments work. - **Hydration is what HTTP 200 misses** — server render succeeds long before a hydration-time `Context not found` or effect loop would; that is exactly what `npm run smoke` exists to catch. ## See also - [`docs/getting-started.md`](./getting-started.md) — the short verification loop in context. - [`component-guide.md`](./guides/component-guide.md) — the build checklist that calls these. - [`completion-checklist.md`](./guides/completion-checklist.md) — what `component:audit` enforces.