Widget Test Coverage
How SveltyCMS tests its widget system: core suites, portable custom-widget suites, the shared fixture boundary, and the commands to run them.
On this page
Executive Summary
Every widget that ships with SveltyCMS has at least one dedicated validation suite. Widget tests are black-box: they call the createWidget factory and assert on the returned validationSchema, so they need no DOM and no Svelte mount, and they stay fast enough for the pre-commit gate.
Two rules keep the suite honest:
- One home per widget suite. Custom-widget suites live with the widget (
src/widgets/custom/<name>/tests/) so a widget folder stays portable and marketplace-ready. A suite for the same widget must not be duplicated undertests/unit/. - Mock the message barrel, never the behaviour. Widget
index.tsfiles import exactly onewidget_*_description()from the Paraglide barrel (1 400+ generated modules, ~5 s of Vite transform per file). Suites replace it withWIDGET_MESSAGESfrom the shared fixture โ a widget that starts calling a new message fails at import instead of silently renderingundefined.
Test Statistics (self-assessed, 2026-09-27)
| Category | Suites | Location | What they cover |
|---|---|---|---|
| Core widgets | 13 | tests/unit/widgets/core/ |
Validation schemas, defaults, filter/sort fragments, i18n shapes |
| Portable custom | 16 | src/widgets/custom/<name>/tests/ |
Validation schemas + accept/reject cases, co-located with the widget |
| Custom (non-portable) | 1 | tests/unit/widgets/custom/ |
block-builder โ its suite is not co-located yet |
| Widget system | 13 | tests/unit/widgets/ |
Factory contract, governance, desugaring, relation integrity, chaos boundary |
| Shared fixture | 1 | tests/unit/widgets/test-utils.ts |
getSchema, CHAOS_CASES, testChaos, WIDGET_MESSAGES |
Run everything with bun run test:unit; the counts above are structure, not pass/fail โ CI is the source of truth for a green tree.
๐๏ธ Testing Architecture
1. Core Widget Suites (tests/unit/widgets/core/)
The backbone of the CMS โ every core widget has a dedicated file asserting its data contract:
- โ
checkbox.test.tsยทdate-time.test.tsยทemail.test.tsยทgroup.test.ts - โ
input.test.tsยทnumber.test.tsยทradio.test.tsยทselect.test.ts - โ
relation.test.tsยทrich-text.test.tsยทslug.test.ts - โ
media-upload.test.ts+media-upload-process.test.ts
2. Portable Custom Widget Suites (src/widgets/custom/<name>/tests/)
Inspired by the Drupal module pattern, custom widgets are self-contained: definition, input, display and tests travel together in one folder.
- Co-located today (16):
address,ai-enrichment,color-picker,currency,date-range,geolocation,json-editor,markdown,mega-menu,phone-number,price,rating,remote-video,repeater,seo,tags - Verified in
vitest.config.ts(src/widgets/custom/**/tests/**/*.test.ts), so a new widget folder is picked up without config changes. - Each suite imports the widget through the
@widgetsalias and the fixture through@tests/unit/widgets/test-utils, so the file stays valid wherever the widget folder is moved.
3. Client DOM Component Interaction (tests/unit/components/dom/)
Complementing headless schema validation, client DOM interaction tests mount the actual Svelte 5 widget components (input.svelte) in the jsdom runtime via vitest.dom.config.ts:
- โ
widget-inputs-interaction.test.ts: exercises user typing, number min/max range attributes, checkbox toggles, email validation, and two-way reactivity withvalidationStore. - Run with
bun run test:unit:components.
๐งช Testing Patterns
Validation Schema Testing (Black-Box)
We test widgets without mounting Svelte components, which keeps the gate fast: the assertion target is the validationSchema returned by the factory.
import { safeParse } from "valibot";
import { getSchema } from "@tests/unit/widgets/test-utils";
import EmailWidget from "@widgets/core/email";
const schema = getSchema(EmailWidget({ label: "Email", required: true }));
expect(safeParse(schema, "test@example.com").success).toBe(true);
expect(safeParse(schema, "invalid-email").success).toBe(false);
getSchema handles both factory-shaped and static schemas, so suites do not cast to any and do not need to know which shape a widget chose.
Table-Driven Case Sets
Payload variants belong in one it.each table (ACCEPTED_CASES / REJECTED_CASES) instead of one it per case: the schema is built once per field configuration and each row isolates one rule. price and seo are the reference implementations.
Paraglide Boundary Mock
vi.mock(
"@src/paraglide/messages",
async () => (await import("@tests/unit/widgets/test-utils")).WIDGET_MESSAGES,
);
The vi.mock factory must be async for the mock to stay hoisted. WIDGET_MESSAGES is exhaustive for the widget graph on purpose: a missing entry throws at import rather than hiding a regression.
Multilingual Fields
Suites assert that translated fields accept Record<string, string> structures as well as primitive values.
Database Fragments
filters and sorts returned by widget aggregations are asserted to be valid MongoDB/SQL-compatible fragments.
๐ Execution Guide
Run All Widget Tests
bun run test:unit
Run a Specific Core Suite
bun x vitest run tests/unit/widgets/core/input.test.ts
Run a Specific Portable Suite
bun x vitest run src/widgets/custom/address/tests/address.test.ts
Vitest is the canonical runner (never bun:test). The test:unit script applies the project aliases and the SQLite test setup, so always go through it or bun x vitest run <path>.
Status Report
- โ Core logic coverage: every core widget has a dedicated suite
- โ Schema validation: asserted through the factory output, not internals
- โ Portable pattern: 16 custom widgets co-located with their suites
- โ
Runner: Vitest (worker pool sized from CPU cores; see
vitest.config.ts) - โณ Open:
block-builderstill lives undertests/unit/widgets/custom/โ move it when the widget folder is next touched