You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/docs/testing-and-tooling.md

18 KiB

title type audience status
Testing, Validation & Codegen reference human + agent 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)

npm run check:gate  # types WITH the policy: src/ owes ZERO; web/ measured
                    # against the shrinking ledger scripts/check-debt.ts
npm run check       # raw svelte-check (src/ expect 0; web/ carries the
                    # frozen ledger errors, which may only shrink)
npm run test        # the vitest suite (one run)
npm run gate        # everything the pre-push hook runs (see §The gate)
npm run lint        # prettier --check  ·  npm run format to fix
                    # (NOT in `gate` yet — pre-existing repo-wide format debt;
                    # the one-shot is `npm run format` on a quiet tree)

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.
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 §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 — 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 layer:check a shared visual layer losing a cascade fight — for every element carrying a layer hook (data-viewport-placement), asserts the computed position, that the stacking token resolved, and that no override slot declared inline computes to nothing (the signature of a custom-property CYCLE). Needs npm run dev. Its consumer list is DERIVED from who imports the layer, so a component joins the day it migrates. Deliberate hole, stated in the script: geometry. getComputedStyle reports the USED value, so an inset: auto reads back as pixels (measured: -1976.7px) — there is no property-level way to tell a dead calc() from an intended value.
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.
npm run eidos:lint classifies every [data-*] selector in eidos CSS as morfo-backed / eidos-only / invalid — drift between the morfo contract and the CSS. A gate member since P0 fase B (audit 2026-08-26); the architectural defense is still the typed semaSelector builder — see the "Eidos drift defense" rule in CLAUDE.md.
src/uix/eidos/shared-layer-contract.test.ts (vitest) the text half a browser cannot cover for a shared layer: every zone has a rule, stretch stays on the axes it was scoped to, no axis is read without its override slot, no geometry rule keys on the component identity, and each consumer imports the layer / stamps the hook / mints no parallel token / re-asserts position when the primitive it composes declares one. It exists for ONE thing the computed check is blind to: getComputedStyle of a safe-area slot returns "0px" on desktop, so a :dir(rtl) remap with one half flipped reads identical to a correct one on every CI machine and only surfaces on a notched phone, sideways, in RTL.
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.

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 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.
  • The morfo contract server-renders — since the render bag became the single attr pipeline (P0 fase C, audit 2026-08-26), a part's role / aria-* / data-state / literals resolve at render time, server included. The mechanism is SHARED (partPropsForRegistration) and pinned by src/uix/soma/ssr-contract.test.ts (server project, node — no window) with two representatives, Toggle and RadioGroup — a CANARY, not a per-provider census; the per-component SSR snapshot census is still owed (informe P1). It was born red against the old client-only effect.

The gate

The drift-defense scripts stopped being advisory in P0 fase B (audit 2026-08-26). Three additions:

  • npm run check:gate — svelte-check with policy: any ERROR under src/ fails (framework source owes zero); errors in the demo tree are measured against the per-file ledger in scripts/check-debt.ts, which only SHRINKS (the theming-census-debt discipline — a file above its entry, or an erroring file the ledger does not name, fails). The run must end with the COMPLETED marker and the parsed count must match it: a gate that inspected nothing fails, never passes.
  • npm run gate — the chain the pre-push hook runs: check:gate → the fast validators (morfo:vocabulary, docs:check, arts:check, blocks:check, packs:check, rtl:check, translations:check, agent:check, eidos:lint) → the full vitest suite last. lint is deliberately NOT a member yet: the tree carries pre-existing repo-wide format debt; the pending one-shot is npm run format on a quiet tree, then add npm run lint && back.
  • The pre-push hook — source of truth in scripts/hooks/pre-push; arm it with cp scripts/hooks/pre-push .git/hooks/pre-push. Browser-dependent validators (morfo:check, perm:check, smoke, layer:check) stay manual — they need a dev server.

See also

Powered by TurnKey Linux.