Writing Unit Tests
Guide for writing high-quality unit tests in packages/core/ and packages/methods/ with proper TypeScript types. Both packages share the same conventions: createTestStore helper, objectPath / arrayPath / validationIssue helpers, and the type-guard pattern for narrowing union store types.
For tests in frameworks/<framework>/ (hooks, composables, runes, components), use the repo-source-code-test-frameworks skill instead — those have framework-specific concerns (DOM testing, signal/rune reactivity, snippets/slots, cross-framework consistency) that are out of scope here.
When to Use This Guide
- Creating new unit tests for functions in
packages/core/src/orpackages/methods/src/ - Fixing type errors in existing tests in those packages
- Adding test coverage for new core or method features
Core Principles
- No type casts — Get types right, don't use
asassertions - Type guards only when needed — Use
ifblocks to narrow types only when TypeScript reports an error - Real DOM elements — Use
document.createElement(), not mock objects
Test File Structure
File Location
Tests live next to their implementation in either package:
packages/core/src/ ├── form/ │ └── validateFormInput/ │ ├── validateFormInput.ts │ └── validateFormInput.test.ts ├── field/ │ └── getFieldInput/ │ ├── getFieldInput.ts │ └── getFieldInput.test.ts packages/methods/src/ ├── insert/ │ ├── insert.ts │ └── insert.test.ts ├── validate/ │ ├── validate.ts │ └── validate.test.ts
Basic Template
The global setup file (src/vitest/setup.ts) automatically calls mockFramework() and sets up beforeEach with resetIdCounter(). Use the shared createTestStore helper:
typescriptimport * as v from 'valibot'; import { describe, expect, test } from 'vitest'; import { createTestStore } from '../../vitest/index.ts'; describe('functionName', () => { test('should do something', () => { const store = createTestStore(v.object({ name: v.string() })); // Test logic }); test('should handle initial input', () => { const store = createTestStore(v.object({ name: v.string() }), { initialInput: { name: 'John' }, }); // Test logic }); });
The createTestStore helper accepts a schema and an optional config object:
typescriptcreateTestStore(schema, { initialInput?: unknown, // Initial form values validate?: ValidationMode, // 'initial' | 'touch' | 'input' | 'change' | 'blur' | 'submit' revalidate?: ValidationMode, // Same options except 'initial' issues?: [...], // Mock validation issues });
JSDOM Environment
For tests that need DOM APIs (focus, createElement, etc.), add the directive:
typescript// @vitest-environment jsdom import { describe, expect, test } from 'vitest';
Type-Safe Patterns
Accessing Union Type Properties
InternalFieldStore is a union type:
typescripttype InternalFieldStore = | InternalArrayStore // has: kind: 'array', children: InternalFieldStore[] | InternalObjectStore // has: kind: 'object', children: Record<string, InternalFieldStore> | InternalValueStore; // has: kind: 'value', NO children property
When to use type guards: Only use if blocks for type narrowing when TypeScript reports an error.
❌ Bad — Type cast:
typescriptexpect(store.children.items.children[0].input.value).toBe('a'); // Error: Property 'children' does not exist on type 'InternalFieldStore' // Wrong fix: expect( (store.children.items as InternalArrayStore).children[0].input.value ).toBe('a');
✅ Good — Type guard with assertion:
typescriptconst itemsStore = store.children.items; expect(itemsStore.kind).toBe('array'); if (itemsStore.kind === 'array') { expect(itemsStore.children[0].input.value).toBe('a'); }
The pattern:
- Extract to variable —
const itemsStore = store.children.items; - Assert the kind —
expect(itemsStore.kind).toBe('array');(test fails if wrong) - Narrow with if —
if (itemsStore.kind === 'array') { ... }(TypeScript narrows type)
Required Imports
typescriptimport type { InternalArrayStore, InternalFormStore, InternalObjectStore, } from '../../types/index.ts';
Complete Example
typescripttest('should initialize array schema', () => { const store = createTestStore(v.object({ items: v.array(v.string()) }), { initialInput: { items: ['a', 'b'] }, }); const itemsStore = store.children.items; expect(itemsStore.kind).toBe('array'); if (itemsStore.kind === 'array') { expect(itemsStore.children).toHaveLength(2); expect(itemsStore.children[0].input.value).toBe('a'); expect(itemsStore.children[1].input.value).toBe('b'); } });
DOM Element Mocking
❌ Bad — Mock object with cast:
typescriptconst mockFocus = vi.fn(); store.children.name.elements = [{ focus: mockFocus } as HTMLElement]; // Error: Type '{ focus: Mock }' is not assignable to type 'FieldElement'
✅ Good — Real DOM element with spy:
typescriptconst inputElement = document.createElement('input'); const mockFocus = vi.spyOn(inputElement, 'focus'); store.children.name.elements = [inputElement]; await validateFormInput(store, { shouldFocus: true }); expect(mockFocus).toHaveBeenCalledOnce();
Note: Requires // @vitest-environment jsdom at file top.
Valibot Issue Helpers
When testing validation, create properly typed issue helpers:
typescriptfunction objectPath(key: string, value: unknown = ''): v.ObjectPathItem { return { type: 'object', origin: 'value', input: {}, key, value }; } function arrayPath(key: number, value: unknown = ''): v.ArrayPathItem { return { type: 'array', origin: 'value', input: [], key, value }; } function validationIssue( message: string, path?: [v.IssuePathItem, ...v.IssuePathItem[]] ): v.BaseIssue<unknown> { return { kind: 'validation', type: 'check', input: '', expected: null, received: 'unknown', message, path, }; }
Note: The path type is [v.IssuePathItem, ...v.IssuePathItem[]] (tuple with at least one item).
Common Type Errors and Fixes
Error: Property 'children' does not exist
Property 'children' does not exist on type 'InternalFieldStore'.
Fix: Use type guard pattern (see above).
Error: Type is not assignable to 'FieldElement'
Type '{ focus: Mock }' is not assignable to type 'FieldElement'.
Fix: Use real DOM elements with document.createElement().
Error: Type not assignable with exactOptionalPropertyTypes
Type 'IssuePathItem[]' is not assignable to type '[IssuePathItem, ...IssuePathItem[]]'.
Fix: Use tuple type [v.IssuePathItem, ...v.IssuePathItem[]] for path arrays.
Test Organization
Describe Blocks
Group tests by functionality:
typescriptdescribe('functionName', () => { describe('basic behavior', () => { test('should handle simple case', () => {}); }); describe('error handling', () => { test('should return errors for invalid input', () => {}); }); describe('nested fields', () => { test('should handle nested objects', () => {}); }); });
Test Naming
- Start with "should"
- Describe the expected behavior
- Be specific about the scenario
typescript// ✅ Good test('should focus first error field when shouldFocus is true', () => {}); // ❌ Bad test('focus test', () => {});
Running Tests
Replace <pkg> with core or methods:
bash# Run all tests pnpm -C packages/<pkg> test # Run tests in watch mode pnpm -C packages/<pkg> test --watch # Run specific test file pnpm -C packages/<pkg> test validateFormInput # Run with coverage pnpm -C packages/<pkg> test --coverage
Checklist
Before committing tests:
- No type casts (
as SomeType) — use type guards instead - All imports use
.tsextension - Type imports use
import type { ... } - DOM tests have
// @vitest-environment jsdomdirective - DOM elements created with
document.createElement() - Union types narrowed with
expect(...).toBe(...)+ifguards - All tests pass:
pnpm test - No lint errors:
pnpm lint
Quick Reference
Type Guard Pattern
typescriptconst fieldStore = store.children.fieldName; expect(fieldStore.kind).toBe('array'); if (fieldStore.kind === 'array') { expect(fieldStore.children).toHaveLength(2); }
DOM Element Pattern
typescriptconst input = document.createElement('input'); const spy = vi.spyOn(input, 'focus'); store.children.name.elements = [input];
Issue Helper Pattern
typescriptvalidationIssue('Error message', [objectPath('field'), arrayPath(0)]);


