| name | repo-source-code-test-packages |
| description | Write unit tests for Formisch packages (packages/core and packages/methods) with proper TypeScript types. Use when creating new tests, fixing type errors in tests, or adding test coverage for core/methods functions. |
| metadata | {"author":"formisch","version":"2.0"} |
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/ or packages/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
as assertions
- Type guards only when needed ā Use
if blocks 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:
import * 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('should handle initial input', () => {
const store = createTestStore(v.object({ name: v.string() }), {
initialInput: { name: 'John' },
});
});
});
The createTestStore helper accepts a schema and an optional config object:
createTestStore(schema, {
initialInput?: unknown,
validate?: ValidationMode,
revalidate?: ValidationMode,
issues?: [...],
});
JSDOM Environment
For tests that need DOM APIs (focus, createElement, etc.), add the directive:
import { describe, expect, test } from 'vitest';
Type-Safe Patterns
Accessing Union Type Properties
InternalFieldStore is a union type:
type InternalFieldStore =
| InternalArrayStore
| InternalObjectStore
| InternalValueStore;
When to use type guards: Only use if blocks for type narrowing when TypeScript reports an error.
ā Bad ā Type cast:
expect(store.children.items.children[0].input.value).toBe('a');
expect(
(store.children.items as InternalArrayStore).children[0].input.value
).toBe('a');
ā
Good ā Type guard with assertion:
const 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
import type {
InternalArrayStore,
InternalFormStore,
InternalObjectStore,
} from '../../types/index.ts';
Complete Example
test('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:
const mockFocus = vi.fn();
store.children.name.elements = [{ focus: mockFocus } as HTMLElement];
ā
Good ā Real DOM element with spy:
const 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:
function 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:
describe('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
test('should focus first error field when shouldFocus is true', () => {});
test('focus test', () => {});
Running Tests
Replace <pkg> with core or methods:
pnpm -C packages/<pkg> test
pnpm -C packages/<pkg> test --watch
pnpm -C packages/<pkg> test validateFormInput
pnpm -C packages/<pkg> test --coverage
Checklist
Before committing tests:
Quick Reference
Type Guard Pattern
const fieldStore = store.children.fieldName;
expect(fieldStore.kind).toBe('array');
if (fieldStore.kind === 'array') {
expect(fieldStore.children).toHaveLength(2);
}
DOM Element Pattern
const input = document.createElement('input');
const spy = vi.spyOn(input, 'focus');
store.children.name.elements = [input];
Issue Helper Pattern
validationIssue('Error message', [objectPath('field'), arrayPath(0)]);