Skip to content

Documentation

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.

9/27/2026
5 min read Edit on GitHub

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:

  1. 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 under tests/unit/.
  2. Mock the message barrel, never the behaviour. Widget index.ts files import exactly one widget_*_description() from the Paraglide barrel (1 400+ generated modules, ~5 s of Vite transform per file). Suites replace it with WIDGET_MESSAGES from the shared fixture โ€” a widget that starts calling a new message fails at import instead of silently rendering undefined.

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 @widgets alias 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 with validationStore.
  • 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
Note

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-builder still lives under tests/unit/widgets/custom/ โ€” move it when the widget folder is next touched

Related

testingwidgetscoveragevitest
Was this page helpful?