From 412fd5605629d4180f2175b0eadb97c44652e209 Mon Sep 17 00:00:00 2001 From: Fabian Hiller Date: Sat, 17 Jan 2026 13:23:27 -0400 Subject: [PATCH] Add tests via Vitest to React framework package --- frameworks/react/package.json | 9 +- .../react/src/components/Field/Field.test.tsx | 252 ++++++++++++++ .../components/FieldArray/FieldArray.test.tsx | 310 ++++++++++++++++++ .../react/src/components/Form/Form.test.tsx | 226 +++++++++++++ .../src/hooks/useField/useField.test-d.ts | 56 ++++ .../src/hooks/useField/useField.test.tsx | 283 ++++++++++++++++ .../useFieldArray/useFieldArray.test-d.ts | 60 ++++ .../useFieldArray/useFieldArray.test.tsx | 236 +++++++++++++ .../react/src/hooks/useForm/useForm.test-d.ts | 45 +++ .../react/src/hooks/useForm/useForm.test.tsx | 136 ++++++++ .../src/hooks/useSignals/useSignals.test.tsx | 121 +++++++ frameworks/react/src/vitest/index.ts | 1 + frameworks/react/src/vitest/setup.ts | 9 + frameworks/react/src/vitest/utils.ts | 66 ++++ frameworks/react/vitest.config.ts | 23 ++ pnpm-lock.yaml | 178 +++++++++- prompts/write-unit-tests.md | 10 +- 17 files changed, 2002 insertions(+), 19 deletions(-) create mode 100644 frameworks/react/src/components/Field/Field.test.tsx create mode 100644 frameworks/react/src/components/FieldArray/FieldArray.test.tsx create mode 100644 frameworks/react/src/components/Form/Form.test.tsx create mode 100644 frameworks/react/src/hooks/useField/useField.test-d.ts create mode 100644 frameworks/react/src/hooks/useField/useField.test.tsx create mode 100644 frameworks/react/src/hooks/useFieldArray/useFieldArray.test-d.ts create mode 100644 frameworks/react/src/hooks/useFieldArray/useFieldArray.test.tsx create mode 100644 frameworks/react/src/hooks/useForm/useForm.test-d.ts create mode 100644 frameworks/react/src/hooks/useForm/useForm.test.tsx create mode 100644 frameworks/react/src/hooks/useSignals/useSignals.test.tsx create mode 100644 frameworks/react/src/vitest/index.ts create mode 100644 frameworks/react/src/vitest/setup.ts create mode 100644 frameworks/react/src/vitest/utils.ts create mode 100644 frameworks/react/vitest.config.ts diff --git a/frameworks/react/package.json b/frameworks/react/package.json index ee8db9d..2a80833 100644 --- a/frameworks/react/package.json +++ b/frameworks/react/package.json @@ -34,6 +34,7 @@ }, "scripts": { "build": "tsdown", + "test": "vitest run --typecheck", "lint": "eslint \"src/**/*.ts*\" && tsc --noEmit", "lint.fix": "eslint \"src/**/*.ts*\" --fix", "format": "prettier --write ./src", @@ -43,20 +44,26 @@ "@eslint/js": "^9.39.1", "@formisch/core": "workspace:*", "@formisch/methods": "workspace:*", + "@testing-library/dom": "^10.4.0", + "@testing-library/jest-dom": "^6.6.0", + "@testing-library/react": "^16.3.0", "@types/node": "^24.10.1", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", + "@vitest/coverage-v8": "^3.2.4", "eslint": "^9.39.1", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", + "jsdom": "^26.1.0", "react": "^19.2.1", "react-dom": "^19.2.1", "tsdown": "^0.16.8", "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", - "vite": "^7.2.4" + "vite": "^7.2.4", + "vitest": "^3.2.4" }, "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", diff --git a/frameworks/react/src/components/Field/Field.test.tsx b/frameworks/react/src/components/Field/Field.test.tsx new file mode 100644 index 0000000..de67a7b --- /dev/null +++ b/frameworks/react/src/components/Field/Field.test.tsx @@ -0,0 +1,252 @@ +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import type { ReactElement } from 'react'; +import * as v from 'valibot'; +import { describe, expect, test } from 'vitest'; +import { useForm } from '../../hooks/index.ts'; +import { Field } from './Field.tsx'; + +describe('Field', () => { + describe('rendering', () => { + test('should render children with field store', () => { + function TestField(): ReactElement { + const form = useForm({ + schema: v.object({ name: v.string() }), + }); + + return ( + + {(field) => ( + + )} + + ); + } + + render(); + + expect(screen.getByTestId('input')).toBeInTheDocument(); + }); + + test('should provide field input value', () => { + function TestFieldWithValue(): ReactElement { + const form = useForm({ + schema: v.object({ name: v.string() }), + initialInput: { name: 'John' }, + }); + + return ( + + {(field) => ( + + )} + + ); + } + + render(); + + const input = screen.getByTestId('input') as HTMLInputElement; + expect(input.value).toBe('John'); + }); + }); + + describe('field state', () => { + test('should provide field state to children', () => { + function StateField(): ReactElement { + const form = useForm({ + schema: v.object({ email: v.string() }), + }); + + return ( + + {(field) => ( +
+ + {String(field.isTouched)} + {String(field.isDirty)} + {String(field.isValid)} +
+ )} +
+ ); + } + + render(); + + expect(screen.getByTestId('touched')).toHaveTextContent('false'); + expect(screen.getByTestId('dirty')).toHaveTextContent('false'); + expect(screen.getByTestId('valid')).toHaveTextContent('true'); + }); + + test('should update isTouched on focus', async () => { + function TouchField(): ReactElement { + const form = useForm({ + schema: v.object({ name: v.string() }), + }); + + return ( + + {(field) => ( +
+ + {String(field.isTouched)} +
+ )} +
+ ); + } + + render(); + + expect(screen.getByTestId('touched')).toHaveTextContent('false'); + + fireEvent.focus(screen.getByTestId('input')); + + await waitFor(() => { + expect(screen.getByTestId('touched')).toHaveTextContent('true'); + }); + }); + }); + + describe('error handling', () => { + test('should display errors from validation', async () => { + function ErrorField(): ReactElement { + const form = useForm({ + schema: v.object({ + email: v.pipe(v.string(), v.email('Invalid email')), + }), + validate: 'initial', + initialInput: { email: 'invalid' }, + }); + + return ( + + {(field) => ( +
+ + {field.errors && ( + {field.errors[0]} + )} +
+ )} +
+ ); + } + + render(); + + await waitFor(() => { + expect(screen.getByTestId('error')).toHaveTextContent('Invalid email'); + }); + }); + }); + + describe('nested paths', () => { + test('should handle nested object paths', () => { + function NestedField(): ReactElement { + const form = useForm({ + schema: v.object({ + user: v.object({ + profile: v.object({ + name: v.string(), + }), + }), + }), + initialInput: { + user: { profile: { name: 'John' } }, + }, + }); + + return ( + + {(field) => ( + + )} + + ); + } + + render(); + + const input = screen.getByTestId('input') as HTMLInputElement; + expect(input.value).toBe('John'); + }); + + test('should handle array index paths', () => { + function ArrayField(): ReactElement { + const form = useForm({ + schema: v.object({ + items: v.array(v.string()), + }), + initialInput: { items: ['first', 'second'] }, + }); + + return ( +
+ + {(field) => ( + + )} + + + {(field) => ( + + )} + +
+ ); + } + + render(); + + expect((screen.getByTestId('input-0') as HTMLInputElement).value).toBe( + 'first' + ); + expect((screen.getByTestId('input-1') as HTMLInputElement).value).toBe( + 'second' + ); + }); + }); + + describe('props integration', () => { + test('should provide props with correct name', () => { + function PropsField(): ReactElement { + const form = useForm({ + schema: v.object({ username: v.string() }), + }); + + return ( + + {(field) => } + + ); + } + + render(); + + const input = screen.getByTestId('input'); + // The name is the stringified path + expect(input).toHaveAttribute('name', '["username"]'); + }); + }); +}); diff --git a/frameworks/react/src/components/FieldArray/FieldArray.test.tsx b/frameworks/react/src/components/FieldArray/FieldArray.test.tsx new file mode 100644 index 0000000..ac4d221 --- /dev/null +++ b/frameworks/react/src/components/FieldArray/FieldArray.test.tsx @@ -0,0 +1,310 @@ +import { insert, remove } from '@formisch/methods/vanilla'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import type { ReactElement } from 'react'; +import * as v from 'valibot'; +import { describe, expect, test } from 'vitest'; +import { useForm } from '../../hooks/index.ts'; +import { FieldArray } from './FieldArray.tsx'; + +describe('FieldArray', () => { + describe('rendering', () => { + test('should render children with field array store', () => { + function TestFieldArray(): ReactElement { + const form = useForm({ + schema: v.object({ items: v.array(v.string()) }), + }); + + return ( + + {(field) => ( +
+ {field.items.length} +
+ )} +
+ ); + } + + render(); + + expect(screen.getByTestId('container')).toBeInTheDocument(); + expect(screen.getByTestId('count')).toHaveTextContent('0'); + }); + + test('should display items when initial input is provided', () => { + function TestFieldArrayWithItems(): ReactElement { + const form = useForm({ + schema: v.object({ tags: v.array(v.string()) }), + initialInput: { tags: ['react', 'typescript', 'vite'] }, + }); + + return ( + + {(field) => ( +
+ {field.items.length} +
    + {field.items.map((id) => ( +
  • + {id} +
  • + ))} +
+
+ )} +
+ ); + } + + render(); + + expect(screen.getByTestId('count')).toHaveTextContent('3'); + }); + }); + + describe('field array state', () => { + test('should provide field array state to children', () => { + function StateFieldArray(): ReactElement { + const form = useForm({ + schema: v.object({ items: v.array(v.string()) }), + }); + + return ( + + {(field) => ( +
+ {String(field.isTouched)} + {String(field.isDirty)} + {String(field.isValid)} +
+ )} +
+ ); + } + + render(); + + expect(screen.getByTestId('touched')).toHaveTextContent('false'); + expect(screen.getByTestId('dirty')).toHaveTextContent('false'); + expect(screen.getByTestId('valid')).toHaveTextContent('true'); + }); + }); + + describe('error handling', () => { + test('should display errors from validation', async () => { + function ErrorFieldArray(): ReactElement { + const form = useForm({ + schema: v.object({ + items: v.pipe( + v.array(v.string()), + v.minLength(2, 'Need at least 2 items') + ), + }), + validate: 'initial', + initialInput: { items: ['one'] }, + }); + + return ( + + {(field) => ( +
+ {String(field.isValid)} + {field.errors && ( + {field.errors[0]} + )} +
+ )} +
+ ); + } + + render(); + + await waitFor(() => { + expect(screen.getByTestId('valid')).toHaveTextContent('false'); + expect(screen.getByTestId('error')).toHaveTextContent( + 'Need at least 2 items' + ); + }); + }); + }); + + describe('nested arrays', () => { + test('should handle nested array paths', () => { + function NestedArrayField(): ReactElement { + const form = useForm({ + schema: v.object({ + groups: v.array( + v.object({ + items: v.array(v.string()), + }) + ), + }), + initialInput: { + groups: [{ items: ['a', 'b'] }, { items: ['c'] }], + }, + }); + + return ( +
+ + {(field) => ( + {field.items.length} + )} + + + {(field) => ( + {field.items.length} + )} + + + {(field) => ( + {field.items.length} + )} + +
+ ); + } + + render(); + + expect(screen.getByTestId('groups-count')).toHaveTextContent('2'); + expect(screen.getByTestId('items-0-count')).toHaveTextContent('2'); + expect(screen.getByTestId('items-1-count')).toHaveTextContent('1'); + }); + }); + + describe('array of objects', () => { + test('should handle array of objects', () => { + function ObjectArrayField(): ReactElement { + const form = useForm({ + schema: v.object({ + users: v.array( + v.object({ + name: v.string(), + email: v.string(), + }) + ), + }), + initialInput: { + users: [ + { name: 'John', email: 'john@example.com' }, + { name: 'Jane', email: 'jane@example.com' }, + ], + }, + }); + + return ( + + {(field) => ( +
+ {field.items.length} +
    + {field.items.map((id, index) => ( +
  • + User {index} +
  • + ))} +
+
+ )} +
+ ); + } + + render(); + + expect(screen.getByTestId('count')).toHaveTextContent('2'); + expect(screen.getByTestId('user-0')).toBeInTheDocument(); + expect(screen.getByTestId('user-1')).toBeInTheDocument(); + }); + }); + + describe('path property', () => { + test('should provide correct path in field store', () => { + function PathFieldArray(): ReactElement { + const form = useForm({ + schema: v.object({ items: v.array(v.string()) }), + }); + + return ( + + {(field) => ( + {JSON.stringify(field.path)} + )} + + ); + } + + render(); + + expect(screen.getByTestId('path')).toHaveTextContent('["items"]'); + }); + }); + + describe('array operations reactivity', () => { + test('should re-render when items are inserted', async () => { + function InsertFieldArray(): ReactElement { + const form = useForm({ + schema: v.object({ items: v.array(v.string()) }), + initialInput: { items: ['a', 'b'] }, + }); + + return ( +
+ + + {(field) => {field.items.length}} + +
+ ); + } + + render(); + + expect(screen.getByTestId('count')).toHaveTextContent('2'); + + fireEvent.click(screen.getByText('Add')); + + await waitFor(() => { + expect(screen.getByTestId('count')).toHaveTextContent('3'); + }); + }); + + test('should re-render when items are removed', async () => { + function RemoveFieldArray(): ReactElement { + const form = useForm({ + schema: v.object({ items: v.array(v.string()) }), + initialInput: { items: ['a', 'b', 'c'] }, + }); + + return ( +
+ + + {(field) => {field.items.length}} + +
+ ); + } + + render(); + + expect(screen.getByTestId('count')).toHaveTextContent('3'); + + fireEvent.click(screen.getByText('Remove')); + + await waitFor(() => { + expect(screen.getByTestId('count')).toHaveTextContent('2'); + }); + }); + }); +}); diff --git a/frameworks/react/src/components/Form/Form.test.tsx b/frameworks/react/src/components/Form/Form.test.tsx new file mode 100644 index 0000000..dca7ae9 --- /dev/null +++ b/frameworks/react/src/components/Form/Form.test.tsx @@ -0,0 +1,226 @@ +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import type { ReactElement } from 'react'; +import * as v from 'valibot'; +import { describe, expect, test, vi } from 'vitest'; +import { useForm } from '../../hooks/index.ts'; +import { Form } from './Form.tsx'; + +describe('Form', () => { + describe('rendering', () => { + function TestForm(): ReactElement { + const form = useForm({ schema: v.object({ name: v.string() }) }); + return ( +
{}}> + + +
+ ); + } + + test('should render form element with children', () => { + render(); + + expect(screen.getByRole('textbox', { name: 'Name' })).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'Submit' }) + ).toBeInTheDocument(); + }); + + test('should have noValidate attribute', () => { + render(); + + // Form elements without accessible name don't have role="form" + // so we use querySelector instead + const form = document.querySelector('form'); + expect(form).toHaveAttribute('noValidate'); + }); + }); + + describe('props forwarding', () => { + test('should forward standard form attributes', () => { + function TestFormWithProps(): ReactElement { + const form = useForm({ schema: v.object({ name: v.string() }) }); + return ( +
{}} + className="test-class" + id="test-form" + aria-label="Test Form" + > + +
+ ); + } + + render(); + + const formElement = screen.getByRole('form', { name: 'Test Form' }); + expect(formElement).toHaveClass('test-class'); + expect(formElement).toHaveAttribute('id', 'test-form'); + }); + }); + + describe('submission', () => { + test('should call onSubmit when form is submitted with valid data', async () => { + const handleSubmit = vi.fn(); + const schema = v.object({ name: v.string() }); + + function SubmitTestForm(): ReactElement { + const form = useForm({ + schema, + initialInput: { name: 'John' }, + }); + + return ( +
+ +
+ ); + } + + render(); + + const form = screen.getByRole('form', { name: 'Submit Test' }); + fireEvent.submit(form); + + await waitFor(() => { + expect(handleSubmit).toHaveBeenCalled(); + }); + }); + + test('should prevent default form submission', async () => { + const handleSubmit = vi.fn(); + const schema = v.object({ name: v.string() }); + + function PreventDefaultForm(): ReactElement { + const form = useForm({ + schema, + initialInput: { name: 'John' }, + }); + + return ( +
+ +
+ ); + } + + render(); + + const form = screen.getByRole('form', { name: 'Prevent Default' }); + const submitEvent = new Event('submit', { + bubbles: true, + cancelable: true, + }); + + form.dispatchEvent(submitEvent); + + // The form should prevent default + expect(submitEvent.defaultPrevented).toBe(true); + }); + }); + + describe('validation on submit', () => { + test('should not call onSubmit when validation fails', async () => { + const handleSubmit = vi.fn(); + const schema = v.object({ + email: v.pipe(v.string(), v.nonEmpty('Email is required')), + }); + + function ValidationForm(): ReactElement { + const form = useForm({ + schema, + initialInput: { email: '' }, + }); + + return ( +
+ +
+ ); + } + + render(); + + const form = screen.getByRole('form', { name: 'Validation Form' }); + fireEvent.submit(form); + + // Use waitFor to ensure validation has completed + await waitFor(() => { + expect(handleSubmit).not.toHaveBeenCalled(); + }); + }); + + test('should call onSubmit with valid output', async () => { + const handleSubmit = vi.fn(); + const schema = v.object({ + email: v.pipe(v.string(), v.email()), + }); + + function ValidForm(): ReactElement { + const form = useForm({ + schema, + initialInput: { email: 'test@example.com' }, + }); + + return ( +
+ +
+ ); + } + + render(); + + const form = screen.getByRole('form', { name: 'Valid Form' }); + fireEvent.submit(form); + + await waitFor(() => { + expect(handleSubmit).toHaveBeenCalledWith( + { email: 'test@example.com' }, + expect.any(Object) + ); + }); + }); + }); + + describe('form state', () => { + test('should set isSubmitting during submission', async () => { + const schema = v.object({ name: v.string() }); + let capturedIsSubmitting = false; + + function StateForm(): ReactElement { + const form = useForm({ + schema, + initialInput: { name: 'John' }, + }); + + return ( +
{ + capturedIsSubmitting = form.isSubmitting; + await new Promise((resolve) => setTimeout(resolve, 50)); + }} + aria-label="State Form" + > + {String(form.isSubmitting)} + +
+ ); + } + + render(); + + expect(screen.getByTestId('submitting')).toHaveTextContent('false'); + + const form = screen.getByRole('form', { name: 'State Form' }); + fireEvent.submit(form); + + await waitFor(() => { + expect(capturedIsSubmitting).toBe(true); + }); + }); + }); +}); diff --git a/frameworks/react/src/hooks/useField/useField.test-d.ts b/frameworks/react/src/hooks/useField/useField.test-d.ts new file mode 100644 index 0000000..0a339b4 --- /dev/null +++ b/frameworks/react/src/hooks/useField/useField.test-d.ts @@ -0,0 +1,56 @@ +import * as v from 'valibot'; +import { describe, expectTypeOf, test } from 'vitest'; +import type { FieldStore } from '../../types/index.ts'; +import { useForm } from '../useForm/index.ts'; +import { useField } from './useField.ts'; + +describe('useField types', () => { + test('should infer field type from path', () => { + const schema = v.object({ + name: v.string(), + age: v.number(), + }); + const form = useForm({ schema }); + const field = useField(form, { path: ['name'] }); + + expectTypeOf(field).toMatchTypeOf>(); + expectTypeOf(field.input).toEqualTypeOf(); + }); + + test('should infer nested field type', () => { + const schema = v.object({ + user: v.object({ + email: v.string(), + }), + }); + const form = useForm({ schema }); + const field = useField(form, { path: ['user', 'email'] }); + + expectTypeOf(field.input).toEqualTypeOf(); + }); + + test('should have correct property types', () => { + const schema = v.object({ name: v.string() }); + const form = useForm({ schema }); + const field = useField(form, { path: ['name'] }); + + expectTypeOf(field.path).toEqualTypeOf<['name']>(); + expectTypeOf(field.errors).toEqualTypeOf<[string, ...string[]] | null>(); + expectTypeOf(field.isTouched).toBeBoolean(); + expectTypeOf(field.isDirty).toBeBoolean(); + expectTypeOf(field.isValid).toBeBoolean(); + }); + + test('should have correct props types', () => { + const schema = v.object({ name: v.string() }); + const form = useForm({ schema }); + const field = useField(form, { path: ['name'] }); + + expectTypeOf(field.props.name).toBeString(); + expectTypeOf(field.props.autoFocus).toBeBoolean(); + expectTypeOf(field.props.ref).toBeFunction(); + expectTypeOf(field.props.onFocus).toBeFunction(); + expectTypeOf(field.props.onChange).toBeFunction(); + expectTypeOf(field.props.onBlur).toBeFunction(); + }); +}); diff --git a/frameworks/react/src/hooks/useField/useField.test.tsx b/frameworks/react/src/hooks/useField/useField.test.tsx new file mode 100644 index 0000000..0e95c84 --- /dev/null +++ b/frameworks/react/src/hooks/useField/useField.test.tsx @@ -0,0 +1,283 @@ +import { + fireEvent, + render, + renderHook, + screen, + waitFor, +} from '@testing-library/react'; +import type { ReactElement } from 'react'; +import * as v from 'valibot'; +import { describe, expect, test } from 'vitest'; +import { useForm } from '../useForm/index.ts'; +import { useField } from './useField.ts'; + +describe('useField', () => { + describe('initialization', () => { + test('should create field store with correct path', () => { + const schema = v.object({ name: v.string() }); + + const { result } = renderHook(() => { + const form = useForm({ schema }); + return useField(form, { path: ['name'] }); + }); + + expect(result.current.path).toEqual(['name']); + }); + + test('should have undefined input for uninitialized field', () => { + const schema = v.object({ name: v.string() }); + + const { result } = renderHook(() => { + const form = useForm({ schema }); + return useField(form, { path: ['name'] }); + }); + + expect(result.current.input).toBe(undefined); + }); + + test('should have initial input value when provided', () => { + const schema = v.object({ name: v.string() }); + + const { result } = renderHook(() => { + const form = useForm({ + schema, + initialInput: { name: 'John' }, + }); + return useField(form, { path: ['name'] }); + }); + + expect(result.current.input).toBe('John'); + }); + }); + + describe('field state', () => { + test('should have default state values', () => { + const schema = v.object({ email: v.string() }); + + const { result } = renderHook(() => { + const form = useForm({ schema }); + return useField(form, { path: ['email'] }); + }); + + expect(result.current.errors).toBe(null); + expect(result.current.isTouched).toBe(false); + expect(result.current.isDirty).toBe(false); + expect(result.current.isValid).toBe(true); + }); + + test('should show errors after validation', async () => { + const schema = v.object({ + email: v.pipe(v.string(), v.email('Invalid email')), + }); + + const { result } = renderHook(() => { + const form = useForm({ + schema, + validate: 'initial', + initialInput: { email: 'invalid' }, + }); + return useField(form, { path: ['email'] }); + }); + + await waitFor(() => { + expect(result.current.errors).not.toBe(null); + expect(result.current.isValid).toBe(false); + }); + }); + }); + + describe('props', () => { + test('should have correct name prop', () => { + const schema = v.object({ username: v.string() }); + + const { result } = renderHook(() => { + const form = useForm({ schema }); + return useField(form, { path: ['username'] }); + }); + + // The name is the stringified path + expect(result.current.props.name).toBe('["username"]'); + }); + + test('should have autoFocus false when no errors', () => { + const schema = v.object({ name: v.string() }); + + const { result } = renderHook(() => { + const form = useForm({ schema }); + return useField(form, { path: ['name'] }); + }); + + expect(result.current.props.autoFocus).toBe(false); + }); + + test('should have ref callback', () => { + const schema = v.object({ name: v.string() }); + + const { result } = renderHook(() => { + const form = useForm({ schema }); + return useField(form, { path: ['name'] }); + }); + + expect(typeof result.current.props.ref).toBe('function'); + }); + + test('should have event handlers', () => { + const schema = v.object({ name: v.string() }); + + const { result } = renderHook(() => { + const form = useForm({ schema }); + return useField(form, { path: ['name'] }); + }); + + expect(typeof result.current.props.onFocus).toBe('function'); + expect(typeof result.current.props.onChange).toBe('function'); + expect(typeof result.current.props.onBlur).toBe('function'); + }); + }); + + describe('event handlers', () => { + function TestComponent(): ReactElement { + const form = useForm({ + schema: v.object({ name: v.string() }), + validate: 'touch', + }); + const field = useField(form, { path: ['name'] }); + + return ( +
+ + {String(field.isTouched)} + {String(field.isDirty)} +
+ ); + } + + test('should set isTouched on focus', async () => { + render(); + + const input = screen.getByTestId('input'); + expect(screen.getByTestId('touched')).toHaveTextContent('false'); + + fireEvent.focus(input); + + await waitFor(() => { + expect(screen.getByTestId('touched')).toHaveTextContent('true'); + }); + }); + + test('should update input value on change', async () => { + render(); + + const input = screen.getByTestId('input') as HTMLInputElement; + fireEvent.change(input, { target: { value: 'test' } }); + + await waitFor(() => { + expect(input.value).toBe('test'); + }); + }); + + test('should trigger validation on blur when validate is blur', async () => { + function BlurValidateComponent(): ReactElement { + const form = useForm({ + schema: v.object({ + email: v.pipe(v.string(), v.email('Invalid email')), + }), + validate: 'blur', + initialInput: { email: 'invalid' }, + }); + const field = useField(form, { path: ['email'] }); + + return ( +
+ + {String(field.isValid)} + {field.errors && {field.errors[0]}} +
+ ); + } + + render(); + + // Initially valid (no validation run yet) + expect(screen.getByTestId('valid')).toHaveTextContent('true'); + + const input = screen.getByTestId('input'); + fireEvent.blur(input); + + await waitFor(() => { + expect(screen.getByTestId('valid')).toHaveTextContent('false'); + expect(screen.getByTestId('error')).toHaveTextContent('Invalid email'); + }); + }); + }); + + describe('nested fields', () => { + test('should handle nested object path', () => { + const schema = v.object({ + user: v.object({ + profile: v.object({ + name: v.string(), + }), + }), + }); + + const { result } = renderHook(() => { + const form = useForm({ + schema, + initialInput: { user: { profile: { name: 'John' } } }, + }); + return useField(form, { path: ['user', 'profile', 'name'] }); + }); + + expect(result.current.path).toEqual(['user', 'profile', 'name']); + expect(result.current.input).toBe('John'); + }); + + test('should handle array index path', () => { + const schema = v.object({ + items: v.array(v.string()), + }); + + const { result } = renderHook(() => { + const form = useForm({ + schema, + initialInput: { items: ['first', 'second'] }, + }); + return useField(form, { path: ['items', 0] }); + }); + + expect(result.current.path).toEqual(['items', 0]); + expect(result.current.input).toBe('first'); + }); + }); + + describe('element registration', () => { + function TestInputComponent(): ReactElement { + const form = useForm({ + schema: v.object({ name: v.string() }), + }); + const field = useField(form, { path: ['name'] }); + + return ; + } + + test('should register element via ref callback', () => { + const { unmount } = render(); + + // Element should be registered after render + const input = screen.getByTestId('field-input'); + expect(input).toBeInTheDocument(); + + // Cleanup on unmount + unmount(); + }); + }); +}); diff --git a/frameworks/react/src/hooks/useFieldArray/useFieldArray.test-d.ts b/frameworks/react/src/hooks/useFieldArray/useFieldArray.test-d.ts new file mode 100644 index 0000000..c19625e --- /dev/null +++ b/frameworks/react/src/hooks/useFieldArray/useFieldArray.test-d.ts @@ -0,0 +1,60 @@ +import * as v from 'valibot'; +import { describe, expectTypeOf, test } from 'vitest'; +import type { FieldArrayStore } from '../../types/index.ts'; +import { useForm } from '../useForm/index.ts'; +import { useFieldArray } from './useFieldArray.ts'; + +describe('useFieldArray types', () => { + test('should infer array type from path', () => { + const schema = v.object({ + tags: v.array(v.string()), + }); + const form = useForm({ schema }); + const fieldArray = useFieldArray(form, { path: ['tags'] }); + + expectTypeOf(fieldArray).toMatchTypeOf< + FieldArrayStore + >(); + }); + + test('should infer nested array type', () => { + const schema = v.object({ + user: v.object({ + hobbies: v.array(v.string()), + }), + }); + const form = useForm({ schema }); + const fieldArray = useFieldArray(form, { path: ['user', 'hobbies'] }); + + expectTypeOf(fieldArray.path).toEqualTypeOf<['user', 'hobbies']>(); + }); + + test('should have correct property types', () => { + const schema = v.object({ items: v.array(v.string()) }); + const form = useForm({ schema }); + const fieldArray = useFieldArray(form, { path: ['items'] }); + + expectTypeOf(fieldArray.items).toEqualTypeOf(); + expectTypeOf(fieldArray.errors).toEqualTypeOf< + [string, ...string[]] | null + >(); + expectTypeOf(fieldArray.isTouched).toBeBoolean(); + expectTypeOf(fieldArray.isDirty).toBeBoolean(); + expectTypeOf(fieldArray.isValid).toBeBoolean(); + }); + + test('should work with array of objects', () => { + const schema = v.object({ + users: v.array( + v.object({ + name: v.string(), + age: v.number(), + }) + ), + }); + const form = useForm({ schema }); + const fieldArray = useFieldArray(form, { path: ['users'] }); + + expectTypeOf(fieldArray.items).toEqualTypeOf(); + }); +}); diff --git a/frameworks/react/src/hooks/useFieldArray/useFieldArray.test.tsx b/frameworks/react/src/hooks/useFieldArray/useFieldArray.test.tsx new file mode 100644 index 0000000..f21a866 --- /dev/null +++ b/frameworks/react/src/hooks/useFieldArray/useFieldArray.test.tsx @@ -0,0 +1,236 @@ +import { insert, remove, swap } from '@formisch/methods/vanilla'; +import { act, renderHook, waitFor } from '@testing-library/react'; +import * as v from 'valibot'; +import { describe, expect, test } from 'vitest'; +import { useForm } from '../useForm/index.ts'; +import { useFieldArray } from './useFieldArray.ts'; + +describe('useFieldArray', () => { + describe('initialization', () => { + test('should create field array store with correct path', () => { + const schema = v.object({ items: v.array(v.string()) }); + + const { result } = renderHook(() => { + const form = useForm({ schema }); + return useFieldArray(form, { path: ['items'] }); + }); + + expect(result.current.path).toEqual(['items']); + }); + + test('should have empty items for uninitialized array', () => { + const schema = v.object({ items: v.array(v.string()) }); + + const { result } = renderHook(() => { + const form = useForm({ schema }); + return useFieldArray(form, { path: ['items'] }); + }); + + expect(result.current.items).toEqual([]); + }); + + test('should have items when initial input is provided', () => { + const schema = v.object({ items: v.array(v.string()) }); + + const { result } = renderHook(() => { + const form = useForm({ + schema, + initialInput: { items: ['a', 'b', 'c'] }, + }); + return useFieldArray(form, { path: ['items'] }); + }); + + expect(result.current.items).toHaveLength(3); + }); + }); + + describe('field array state', () => { + test('should have default state values', () => { + const schema = v.object({ tags: v.array(v.string()) }); + + const { result } = renderHook(() => { + const form = useForm({ schema }); + return useFieldArray(form, { path: ['tags'] }); + }); + + expect(result.current.errors).toBe(null); + expect(result.current.isTouched).toBe(false); + expect(result.current.isDirty).toBe(false); + expect(result.current.isValid).toBe(true); + }); + + test('should show errors after validation', async () => { + const schema = v.object({ + items: v.pipe( + v.array(v.string()), + v.minLength(2, 'Need at least 2 items') + ), + }); + + const { result } = renderHook(() => { + const form = useForm({ + schema, + validate: 'initial', + initialInput: { items: ['one'] }, + }); + return useFieldArray(form, { path: ['items'] }); + }); + + await waitFor(() => { + expect(result.current.errors).not.toBe(null); + expect(result.current.isValid).toBe(false); + }); + }); + }); + + describe('nested arrays', () => { + test('should handle nested array path', () => { + const schema = v.object({ + users: v.array( + v.object({ + tags: v.array(v.string()), + }) + ), + }); + + const { result } = renderHook(() => { + const form = useForm({ + schema, + initialInput: { + users: [{ tags: ['tag1', 'tag2'] }], + }, + }); + return useFieldArray(form, { path: ['users', 0, 'tags'] }); + }); + + expect(result.current.path).toEqual(['users', 0, 'tags']); + expect(result.current.items).toHaveLength(2); + }); + }); + + describe('array of objects', () => { + test('should handle array of objects', () => { + const schema = v.object({ + contacts: v.array( + v.object({ + name: v.string(), + email: v.string(), + }) + ), + }); + + const { result } = renderHook(() => { + const form = useForm({ + schema, + initialInput: { + contacts: [ + { name: 'John', email: 'john@example.com' }, + { name: 'Jane', email: 'jane@example.com' }, + ], + }, + }); + return useFieldArray(form, { path: ['contacts'] }); + }); + + expect(result.current.items).toHaveLength(2); + }); + }); + + describe('store stability', () => { + test('should return stable store reference across renders', () => { + const schema = v.object({ items: v.array(v.string()) }); + + const { result, rerender } = renderHook(() => { + const form = useForm({ schema }); + return useFieldArray(form, { path: ['items'] }); + }); + + const firstStore = result.current; + rerender(); + const secondStore = result.current; + + // Path should be the same reference + expect(firstStore.path).toEqual(secondStore.path); + }); + }); + + describe('array operations reactivity', () => { + test('should update items when insert is called', () => { + const schema = v.object({ items: v.array(v.string()) }); + + const { result } = renderHook(() => { + const form = useForm({ + schema, + initialInput: { items: ['a', 'b'] }, + }); + const fieldArray = useFieldArray(form, { path: ['items'] }); + return { form, fieldArray }; + }); + + expect(result.current.fieldArray.items).toHaveLength(2); + + act(() => { + insert(result.current.form, { + path: ['items'], + initialInput: 'c', + }); + }); + + expect(result.current.fieldArray.items).toHaveLength(3); + }); + + test('should update items when remove is called', () => { + const schema = v.object({ items: v.array(v.string()) }); + + const { result } = renderHook(() => { + const form = useForm({ + schema, + initialInput: { items: ['a', 'b', 'c'] }, + }); + const fieldArray = useFieldArray(form, { path: ['items'] }); + return { form, fieldArray }; + }); + + expect(result.current.fieldArray.items).toHaveLength(3); + + act(() => { + remove(result.current.form, { + path: ['items'], + at: 1, + }); + }); + + expect(result.current.fieldArray.items).toHaveLength(2); + }); + + test('should maintain stable item keys after swap', () => { + const schema = v.object({ items: v.array(v.string()) }); + + const { result } = renderHook(() => { + const form = useForm({ + schema, + initialInput: { items: ['a', 'b', 'c'] }, + }); + const fieldArray = useFieldArray(form, { path: ['items'] }); + return { form, fieldArray }; + }); + + const keysBefore = result.current.fieldArray.items.map((item) => item.id); + + act(() => { + swap(result.current.form, { + path: ['items'], + at: 0, + and: 2, + }); + }); + + const keysAfter = result.current.fieldArray.items.map((item) => item.id); + + // Keys should be swapped, not regenerated + expect(keysAfter[0]).toBe(keysBefore[2]); + expect(keysAfter[2]).toBe(keysBefore[0]); + expect(keysAfter[1]).toBe(keysBefore[1]); + }); + }); +}); diff --git a/frameworks/react/src/hooks/useForm/useForm.test-d.ts b/frameworks/react/src/hooks/useForm/useForm.test-d.ts new file mode 100644 index 0000000..800ff62 --- /dev/null +++ b/frameworks/react/src/hooks/useForm/useForm.test-d.ts @@ -0,0 +1,45 @@ +import * as v from 'valibot'; +import { describe, expectTypeOf, test } from 'vitest'; +import type { FormStore } from '../../types/index.ts'; +import { useForm } from './useForm.ts'; + +describe('useForm types', () => { + test('should infer schema type from config', () => { + const schema = v.object({ name: v.string() }); + const form = useForm({ schema }); + + expectTypeOf(form).toMatchTypeOf>(); + }); + + test('should have correct property types', () => { + const schema = v.object({ name: v.string() }); + const form = useForm({ schema }); + + expectTypeOf(form.isSubmitting).toBeBoolean(); + expectTypeOf(form.isSubmitted).toBeBoolean(); + expectTypeOf(form.isValidating).toBeBoolean(); + expectTypeOf(form.isTouched).toBeBoolean(); + expectTypeOf(form.isDirty).toBeBoolean(); + expectTypeOf(form.isValid).toBeBoolean(); + expectTypeOf(form.errors).toEqualTypeOf<[string, ...string[]] | null>(); + }); + + test('should type check initial input', () => { + const schema = v.object({ + name: v.string(), + age: v.number(), + }); + + // This should compile + useForm({ + schema, + initialInput: { name: 'John', age: 30 }, + }); + + // Partial input should also be allowed + useForm({ + schema, + initialInput: { name: 'John' }, + }); + }); +}); diff --git a/frameworks/react/src/hooks/useForm/useForm.test.tsx b/frameworks/react/src/hooks/useForm/useForm.test.tsx new file mode 100644 index 0000000..5c3976c --- /dev/null +++ b/frameworks/react/src/hooks/useForm/useForm.test.tsx @@ -0,0 +1,136 @@ +import { renderHook, waitFor } from '@testing-library/react'; +import * as v from 'valibot'; +import { describe, expect, test } from 'vitest'; +import { useForm } from './useForm.ts'; + +describe('useForm', () => { + describe('initialization', () => { + test('should create form store with default values', () => { + const schema = v.object({ name: v.string() }); + const { result } = renderHook(() => useForm({ schema })); + + expect(result.current.isSubmitting).toBe(false); + expect(result.current.isSubmitted).toBe(false); + expect(result.current.isValidating).toBe(false); + expect(result.current.isTouched).toBe(false); + expect(result.current.isDirty).toBe(false); + expect(result.current.isValid).toBe(true); + expect(result.current.errors).toBe(null); + }); + + test('should accept initial input values', () => { + const schema = v.object({ email: v.string() }); + const { result } = renderHook(() => + useForm({ + schema, + initialInput: { email: 'test@example.com' }, + }) + ); + + expect(result.current.isSubmitting).toBe(false); + expect(result.current.errors).toBe(null); + }); + + test('should accept validation mode configuration', () => { + const schema = v.object({ name: v.string() }); + const { result } = renderHook(() => + useForm({ + schema, + validate: 'blur', + revalidate: 'input', + }) + ); + + expect(result.current.isSubmitting).toBe(false); + }); + }); + + describe('initial validation', () => { + test('should run initial validation when validate is set to initial', async () => { + const schema = v.object({ + email: v.pipe(v.string(), v.email('Invalid email')), + }); + + const { result } = renderHook(() => + useForm({ + schema, + validate: 'initial', + initialInput: { email: 'invalid' }, + }) + ); + + await waitFor(() => { + expect(result.current.isValid).toBe(false); + }); + }); + + test('should not run validation on initial render when validate is not initial', () => { + const schema = v.object({ + email: v.pipe(v.string(), v.email('Invalid email')), + }); + + const { result } = renderHook(() => + useForm({ + schema, + validate: 'blur', + initialInput: { email: 'invalid' }, + }) + ); + + expect(result.current.isValidating).toBe(false); + expect(result.current.isValid).toBe(true); + }); + }); + + describe('store stability', () => { + test('should return stable store reference across renders', () => { + const schema = v.object({ name: v.string() }); + const { result, rerender } = renderHook(() => useForm({ schema })); + + const firstStore = result.current; + rerender(); + const secondStore = result.current; + + expect(firstStore).toBe(secondStore); + }); + }); + + describe('nested schema', () => { + test('should handle nested object schema', () => { + const schema = v.object({ + user: v.object({ + name: v.string(), + email: v.string(), + }), + }); + + const { result } = renderHook(() => + useForm({ + schema, + initialInput: { + user: { name: 'John', email: 'john@example.com' }, + }, + }) + ); + + expect(result.current.isSubmitting).toBe(false); + expect(result.current.errors).toBe(null); + }); + + test('should handle array schema', () => { + const schema = v.object({ + items: v.array(v.string()), + }); + + const { result } = renderHook(() => + useForm({ + schema, + initialInput: { items: ['a', 'b', 'c'] }, + }) + ); + + expect(result.current.isSubmitting).toBe(false); + expect(result.current.errors).toBe(null); + }); + }); +}); diff --git a/frameworks/react/src/hooks/useSignals/useSignals.test.tsx b/frameworks/react/src/hooks/useSignals/useSignals.test.tsx new file mode 100644 index 0000000..1d4e5a4 --- /dev/null +++ b/frameworks/react/src/hooks/useSignals/useSignals.test.tsx @@ -0,0 +1,121 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import { type ReactElement, useEffect, useState } from 'react'; +import { describe, expect, test } from 'vitest'; +import { useSignals } from './useSignals.ts'; + +describe('useSignals', () => { + describe('basic functionality', () => { + function TestComponent(): ReactElement { + useSignals(); + const [count, setCount] = useState(0); + + return ( +
+ {count} + +
+ ); + } + + test('should render without errors', () => { + render(); + expect(screen.getByTestId('count')).toHaveTextContent('0'); + }); + + test('should allow re-renders', async () => { + render(); + const button = screen.getByText('Increment'); + + button.click(); + + await waitFor(() => { + expect(screen.getByTestId('count')).toHaveTextContent('1'); + }); + }); + }); + + describe('cleanup', () => { + function MountUnmountComponent({ + onMount, + onUnmount, + }: { + onMount: () => void; + onUnmount: () => void; + }): ReactElement { + useSignals(); + + useEffect(() => { + onMount(); + return () => { + onUnmount(); + }; + }, [onMount, onUnmount]); + + return
Mounted
; + } + + test('should cleanup on unmount', async () => { + let mounted = false; + let unmounted = false; + + const { unmount } = render( + { + mounted = true; + }} + onUnmount={() => { + unmounted = true; + }} + /> + ); + + expect(mounted).toBe(true); + expect(unmounted).toBe(false); + + unmount(); + + // Give time for the timeout-based cleanup + await waitFor(() => { + expect(unmounted).toBe(true); + }); + }); + }); + + describe('multiple components', () => { + function Counter({ id }: { id: string }): ReactElement { + useSignals(); + const [count, setCount] = useState(0); + + return ( +
+ {count} + +
+ ); + } + + test('should work with multiple components', async () => { + render( +
+ + +
+ ); + + expect(screen.getByTestId('count-a')).toHaveTextContent('0'); + expect(screen.getByTestId('count-b')).toHaveTextContent('0'); + + screen.getByTestId('button-a').click(); + + await waitFor(() => { + expect(screen.getByTestId('count-a')).toHaveTextContent('1'); + expect(screen.getByTestId('count-b')).toHaveTextContent('0'); + }); + }); + }); +}); diff --git a/frameworks/react/src/vitest/index.ts b/frameworks/react/src/vitest/index.ts new file mode 100644 index 0000000..02db0f7 --- /dev/null +++ b/frameworks/react/src/vitest/index.ts @@ -0,0 +1 @@ +export * from './utils.ts'; diff --git a/frameworks/react/src/vitest/setup.ts b/frameworks/react/src/vitest/setup.ts new file mode 100644 index 0000000..2627cac --- /dev/null +++ b/frameworks/react/src/vitest/setup.ts @@ -0,0 +1,9 @@ +import '@testing-library/dom'; +import '@testing-library/jest-dom/vitest'; +import { cleanup } from '@testing-library/react'; +import { afterEach } from 'vitest'; + +// Cleanup after each test +afterEach(() => { + cleanup(); +}); diff --git a/frameworks/react/src/vitest/utils.ts b/frameworks/react/src/vitest/utils.ts new file mode 100644 index 0000000..3cdd694 --- /dev/null +++ b/frameworks/react/src/vitest/utils.ts @@ -0,0 +1,66 @@ +import type * as v from 'valibot'; + +/** + * Creates an object path item for testing validation issues. + * + * @param key The object key. + * @param value The value at the key. + * + * @returns An object path item. + */ +export function objectPath(key: string, value: unknown = ''): v.ObjectPathItem { + return { type: 'object', origin: 'value', input: {}, key, value }; +} + +/** + * Creates an array path item for testing validation issues. + * + * @param key The array index. + * @param value The value at the index. + * + * @returns An array path item. + */ +export function arrayPath(key: number, value: unknown = ''): v.ArrayPathItem { + return { type: 'array', origin: 'value', input: [], key, value }; +} + +/** + * Creates a validation issue for testing. + * + * @param message The error message. + * @param path The path to the field. + * + * @returns A base issue object. + */ +export function validationIssue( + message: string, + path?: [v.IssuePathItem, ...v.IssuePathItem[]] +): v.BaseIssue { + return { + kind: 'validation', + type: 'check', + input: '', + expected: null, + received: 'unknown', + message, + path, + }; +} + +/** + * Creates a schema-level issue for testing. + * + * @param message The error message. + * + * @returns A base issue object. + */ +export function schemaIssue(message: string): v.BaseIssue { + return { + kind: 'schema', + type: 'object', + input: null, + expected: 'Object', + received: 'null', + message, + }; +} diff --git a/frameworks/react/vitest.config.ts b/frameworks/react/vitest.config.ts new file mode 100644 index 0000000..e5bf671 --- /dev/null +++ b/frameworks/react/vitest.config.ts @@ -0,0 +1,23 @@ +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [react()], + test: { + environment: 'jsdom', + isolate: false, + setupFiles: ['./src/vitest/setup.ts'], + coverage: { + include: ['src'], + exclude: [ + 'src/types', + 'src/vitest', + '**/index.ts', + '**/index.tsx', + '**/*.test.ts', + '**/*.test.tsx', + '**/*.test-d.ts', + ], + }, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ec2a16c..805db95 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -140,6 +140,15 @@ importers: '@formisch/methods': specifier: workspace:* version: link:../../packages/methods + '@testing-library/dom': + specifier: ^10.4.0 + version: 10.4.1 + '@testing-library/jest-dom': + specifier: ^6.6.0 + version: 6.9.1 + '@testing-library/react': + specifier: ^16.3.0 + version: 16.3.1(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) '@types/node': specifier: ^24.10.1 version: 24.10.1 @@ -152,6 +161,9 @@ importers: '@vitejs/plugin-react': specifier: ^5.1.1 version: 5.1.1(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + '@vitest/coverage-v8': + specifier: ^3.2.4 + version: 3.2.4(vitest@3.2.4(@edge-runtime/vm@3.2.0)(@types/debug@4.1.12)(@types/node@24.10.1)(jiti@2.6.1)(jsdom@26.1.0)(less@4.4.2)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) eslint: specifier: ^9.39.1 version: 9.39.1(jiti@2.6.1) @@ -164,6 +176,9 @@ importers: globals: specifier: ^16.5.0 version: 16.5.0 + jsdom: + specifier: ^26.1.0 + version: 26.1.0 react: specifier: ^19.2.1 version: 19.2.1 @@ -182,6 +197,9 @@ importers: vite: specifier: ^7.2.4 version: 7.2.6(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vitest: + specifier: ^3.2.4 + version: 3.2.4(@edge-runtime/vm@3.2.0)(@types/debug@4.1.12)(@types/node@24.10.1)(jiti@2.6.1)(jsdom@26.1.0)(less@4.4.2)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) frameworks/solid: devDependencies: @@ -202,7 +220,7 @@ importers: version: 9.38.0(jiti@2.6.1) eslint-plugin-import: specifier: ^2.32.0 - version: 2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.8.3))(eslint@9.38.0(jiti@2.6.1)) + version: 2.32.0(eslint@9.38.0(jiti@2.6.1)) eslint-plugin-jsdoc: specifier: ^51.3.4 version: 51.4.1(eslint@9.38.0(jiti@2.6.1)) @@ -364,7 +382,7 @@ importers: version: 9.38.0(jiti@2.6.1) eslint-plugin-import: specifier: ^2.32.0 - version: 2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.8.3))(eslint@9.38.0(jiti@2.6.1)) + version: 2.32.0(eslint@9.38.0(jiti@2.6.1)) eslint-plugin-jsdoc: specifier: ^51.3.4 version: 51.4.1(eslint@9.38.0(jiti@2.6.1)) @@ -424,7 +442,7 @@ importers: version: 9.38.0(jiti@2.6.1) eslint-plugin-import: specifier: ^2.32.0 - version: 2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.8.3))(eslint@9.38.0(jiti@2.6.1)) + version: 2.32.0(eslint@9.38.0(jiti@2.6.1)) eslint-plugin-jsdoc: specifier: ^51.3.4 version: 51.4.1(eslint@9.38.0(jiti@2.6.1)) @@ -652,7 +670,7 @@ importers: version: 0.15.3(solid-js@1.9.9) '@solidjs/start': specifier: ^1.1.6 - version: 1.2.0(solid-js@1.9.9)(vinxi@0.5.8(@types/node@24.10.1)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(rolldown@1.0.0-beta.52)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 1.2.0(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vinxi@0.5.8(@types/node@24.10.1)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(rolldown@1.0.0-beta.52)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) '@tailwindcss/vite': specifier: ^4.1.11 version: 4.1.16(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) @@ -914,6 +932,9 @@ packages: '@adobe/css-tools@4.3.3': resolution: {integrity: sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==} + '@adobe/css-tools@4.4.4': + resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} + '@ampproject/remapping@2.3.0': resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} @@ -3631,6 +3652,29 @@ packages: resolution: {integrity: sha512-a05fzK+jBGacsSAc1vE8an7lpBh4H0PyIEcivtEyHLomgSeElAJxm9E2It/0nYRZ5Lh23m0okbhzJNaYWZpAOg==} engines: {node: '>=12'} + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@6.9.1': + resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/react@16.3.1': + resolution: {integrity: sha512-gr4KtAWqIOQoucWYD/f6ki+j5chXfcPc74Col/6poTyqTmn7zRmodWahWRCp8tYd+GMqBonw6hstNzqjbs6gjw==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@tootallnate/once@1.1.2': resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==} engines: {node: '>= 6'} @@ -3679,6 +3723,9 @@ packages: '@types/acorn@4.0.6': resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -4327,6 +4374,10 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + ansi-styles@6.2.3: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} @@ -4374,6 +4425,9 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + aria-query@5.3.2: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} @@ -4974,6 +5028,9 @@ packages: resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} engines: {node: '>= 6'} + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -5217,6 +5274,12 @@ packages: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + dom-serializer@2.0.0: resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} @@ -7143,6 +7206,10 @@ packages: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + magic-string@0.25.9: resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} @@ -7506,6 +7573,10 @@ packages: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + miniflare@3.20250718.2: resolution: {integrity: sha512-cW/NQPBKc+fb0FwcEu+z/v93DZd+/6q/AF0iR0VFELtNPOsCvLalq6ndO743A7wfZtFxMxvuDQUXNx3aKQhOwA==} engines: {node: '>=16.13'} @@ -8204,6 +8275,10 @@ packages: resolution: {integrity: sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw==} engines: {node: '>=20'} + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + pretty-ms@7.0.1: resolution: {integrity: sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==} engines: {node: '>=10'} @@ -8306,6 +8381,9 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-refresh@0.14.2: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} @@ -8390,6 +8468,10 @@ packages: recma-stringify@1.0.0: resolution: {integrity: sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==} + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + redis-errors@1.2.0: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} @@ -9027,6 +9109,10 @@ packages: resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} engines: {node: '>=12'} + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -10399,6 +10485,8 @@ snapshots: '@adobe/css-tools@4.3.3': optional: true + '@adobe/css-tools@4.4.4': {} + '@ampproject/remapping@2.3.0': dependencies: '@jridgewell/gen-mapping': 0.3.13 @@ -12672,7 +12760,7 @@ snapshots: dependencies: solid-js: 1.9.9 - '@solidjs/start@1.2.0(solid-js@1.9.9)(vinxi@0.5.8(@types/node@24.10.1)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(rolldown@1.0.0-beta.52)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + '@solidjs/start@1.2.0(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vinxi@0.5.8(@types/node@24.10.1)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(rolldown@1.0.0-beta.52)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@tanstack/server-functions-plugin': 1.121.21(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.8(@types/node@24.10.1)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(rolldown@1.0.0-beta.52)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) @@ -12689,7 +12777,7 @@ snapshots: terracotta: 1.0.6(solid-js@1.9.9) tinyglobby: 0.2.15 vinxi: 0.5.8(@types/node@24.10.1)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(rolldown@1.0.0-beta.52)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) - vite-plugin-solid: 2.11.10(solid-js@1.9.9)(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + vite-plugin-solid: 2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) transitivePeerDependencies: - '@testing-library/jest-dom' - solid-js @@ -12976,6 +13064,36 @@ snapshots: - supports-color - vite + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/runtime': 7.28.4 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.9.1': + dependencies: + '@adobe/css-tools': 4.4.4 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + + '@testing-library/react@16.3.1(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': + dependencies: + '@babel/runtime': 7.28.4 + '@testing-library/dom': 10.4.1 + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) + optionalDependencies: + '@types/react': 19.2.7 + '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@tootallnate/once@1.1.2': {} '@trivago/prettier-plugin-sort-imports@5.2.2(@vue/compiler-sfc@3.5.22)(prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.42.1))(prettier@3.6.2)(svelte@5.42.1)': @@ -13022,6 +13140,8 @@ snapshots: dependencies: '@types/estree': 1.0.8 + '@types/aria-query@5.0.4': {} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.28.5 @@ -14372,6 +14492,8 @@ snapshots: dependencies: color-convert: 2.0.1 + ansi-styles@5.2.0: {} + ansi-styles@6.2.3: {} ansis@4.2.0: {} @@ -14425,6 +14547,10 @@ snapshots: argparse@2.0.1: {} + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + aria-query@5.3.2: {} array-buffer-byte-length@1.0.2: @@ -15053,6 +15179,8 @@ snapshots: css-what@6.2.2: {} + css.escape@1.5.1: {} + cssesc@3.0.0: {} csso@5.0.5: @@ -15226,6 +15354,10 @@ snapshots: dependencies: esutils: 2.0.3 + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + dom-serializer@2.0.0: dependencies: domelementtype: 2.3.0 @@ -15777,11 +15909,10 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.38.0(jiti@2.6.1)): + eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.9)(eslint@9.38.0(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.8.3) eslint: 9.38.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: @@ -15799,7 +15930,7 @@ snapshots: lodash.memoize: 4.1.2 semver: 7.7.3 - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.8.3))(eslint@9.38.0(jiti@2.6.1)): + eslint-plugin-import@2.32.0(eslint@9.38.0(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -15810,7 +15941,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.38.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.38.0(jiti@2.6.1)) + eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.9)(eslint@9.38.0(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -15821,8 +15952,6 @@ snapshots: semver: 6.3.1 string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 - optionalDependencies: - '@typescript-eslint/parser': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.8.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -17557,6 +17686,8 @@ snapshots: lru-cache@7.18.3: {} + lz-string@1.5.0: {} + magic-string@0.25.9: dependencies: sourcemap-codec: 1.4.8 @@ -18278,6 +18409,8 @@ snapshots: mimic-response@3.1.0: {} + min-indent@1.0.1: {} + miniflare@3.20250718.2: dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -18992,6 +19125,12 @@ snapshots: pretty-bytes@7.1.0: {} + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + pretty-ms@7.0.1: dependencies: parse-ms: 2.1.0 @@ -19099,6 +19238,8 @@ snapshots: react-is@16.13.1: {} + react-is@17.0.2: {} + react-refresh@0.14.2: {} react-refresh@0.18.0: {} @@ -19211,6 +19352,11 @@ snapshots: unified: 11.0.5 vfile: 6.0.2 + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + redis-errors@1.2.0: {} redis-parser@3.0.0: @@ -20115,6 +20261,10 @@ snapshots: strip-final-newline@3.0.0: {} + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + strip-json-comments@3.1.1: {} strip-literal@3.1.0: @@ -21336,7 +21486,7 @@ snapshots: transitivePeerDependencies: - supports-color - vite-plugin-solid@2.11.10(solid-js@1.9.9)(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): + vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): dependencies: '@babel/core': 7.28.5 '@types/babel__core': 7.20.5 @@ -21346,6 +21496,8 @@ snapshots: solid-refresh: 0.6.3(solid-js@1.9.9) vite: 7.2.6(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) vitefu: 1.1.1(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + optionalDependencies: + '@testing-library/jest-dom': 6.9.1 transitivePeerDependencies: - supports-color diff --git a/prompts/write-unit-tests.md b/prompts/write-unit-tests.md index 35b765f..7b5d04d 100644 --- a/prompts/write-unit-tests.md +++ b/prompts/write-unit-tests.md @@ -191,16 +191,16 @@ function arrayPath(key: number, value: unknown = ''): v.ArrayPathItem { return { type: 'array', origin: 'value', input: [], key, value }; } -function issue( +function validationIssue( message: string, path?: [v.IssuePathItem, ...v.IssuePathItem[]] ): v.BaseIssue { return { kind: 'validation', - type: 'custom', + type: 'check', input: '', - expected: 'valid', - received: 'invalid', + expected: null, + received: 'unknown', message, path, }; @@ -347,5 +347,5 @@ store.children.name.elements = [input]; ### Issue Helper Pattern ```typescript -issue('Error message', [objectPath('field'), arrayPath(0)]); +validationIssue('Error message', [objectPath('field'), arrayPath(0)]); ```