// @vitest-environment jsdom import { cleanup, fireEvent, render, screen } from '@testing-library/react' import { afterEach, describe, expect, it, vi } from 'vitest' import Schema from 'schemastery' import { SchemaForm } from '../src/index.ts' afterEach(cleanup) const Wire = (schema: Schema): unknown => JSON.parse(JSON.stringify(schema.toJSON())) const Profile = Schema.object({ apiKey: Schema.string().role('secret'), apiKeyEnv: Schema.string().role('credential-ref'), baseURL: Schema.string().description('Endpoint override'), reasoning: Schema.union(['off', 'high', 'max']), timeoutMs: Schema.number().min(0).max(1000).step(1), verbose: Schema.boolean(), name: Schema.string().required(), }) function lastDraft(onChange: ReturnType): Record { return onChange.mock.calls.at(-1)?.[0] as Record } describe('leaf controls', () => { it('renders strings with inherited placeholders, writes on input, clears on empty', () => { const onChange = vi.fn() render() const input = screen.getByDisplayValue('https://mine') fireEvent.change(input, { target: { value: 'https://next' } }) expect(lastDraft(onChange)).toEqual({ baseURL: 'https://next' }) fireEvent.change(input, { target: { value: '' } }) expect(lastDraft(onChange)).toEqual({}) const inherited = screen.getByPlaceholderText('Default: https://base') expect(inherited).toBeTruthy() }) it('renders numbers with bounds and parses edits', () => { const onChange = vi.fn() const { container } = render() const input = container.querySelector('input[type="number"]') as HTMLInputElement expect(input.placeholder).toBe('Default: 500') expect(input.min).toBe('0') expect(input.max).toBe('1000') fireEvent.change(input, { target: { value: '250' } }) expect(lastDraft(onChange)).toEqual({ timeoutMs: 250 }) }) it('clears a number override back to inherited on empty input', () => { const onChange = vi.fn() const { container } = render() const input = container.querySelector('input[type="number"]') as HTMLInputElement expect(input.value).toBe('250') fireEvent.change(input, { target: { value: '' } }) expect(lastDraft(onChange)).toEqual({}) }) it('prefers an overridden boolean over the fallback', () => { const { container } = render() const box = container.querySelector('input[type="checkbox"]') as HTMLInputElement expect(box.checked).toBe(false) }) it('reflects booleans from the fallback until overridden', () => { const onChange = vi.fn() const { container } = render() const box = container.querySelector('input[type="checkbox"]') as HTMLInputElement expect(box.checked).toBe(true) fireEvent.click(box) expect(lastDraft(onChange)).toEqual({ verbose: false }) }) it('renders literal unions as selects with an inherit option', () => { const onChange = vi.fn() const { container } = render() const select = container.querySelector('select') as HTMLSelectElement expect([...select.options].map(option => option.text)).toEqual(['Default: high', 'off', 'high', 'max']) fireEvent.change(select, { target: { value: 'max' } }) expect(lastDraft(onChange)).toEqual({ reasoning: 'max' }) }) it('clears a union override back to inherit', () => { const onChange = vi.fn() const { container } = render() const select = container.querySelector('select') as HTMLSelectElement expect(select.value).toBe('max') fireEvent.change(select, { target: { value: '' } }) expect(lastDraft(onChange)).toEqual({}) }) it('marks required fields and surfaces descriptions', () => { render() expect(screen.getByText('Endpoint override')).toBeTruthy() expect(screen.getByText('name').textContent).toContain('name') expect(screen.getByText('*')).toBeTruthy() }) it('shows the per-field reset only for overridden fields and deletes on click', () => { const onChange = vi.fn() render() const resets = screen.getAllByText('Reset') expect(resets).toHaveLength(1) fireEvent.click(resets[0] as HTMLElement) expect(lastDraft(onChange)).toEqual({}) }) }) describe('secrets and custom renderers', () => { it('renders secrets write-only with the stored-state placeholder', () => { const onChange = vi.fn() const { container } = render() const input = container.querySelector('input[type="password"]') as HTMLInputElement expect(input.placeholder).toBe('Configured — enter a new value to replace') expect(input.value).toBe('') fireEvent.change(input, { target: { value: 'sk-new' } }) expect(lastDraft(onChange)).toEqual({ apiKey: 'sk-new' }) }) it('clears a typed-but-unsaved secret back to unset', () => { const onChange = vi.fn() const { container } = render() const input = container.querySelector('input[type="password"]') as HTMLInputElement expect(input.value).toBe('sk-draft') fireEvent.change(input, { target: { value: '' } }) expect(lastDraft(onChange)).toEqual({}) }) it('reports an unset secret slot', () => { const { container } = render() const input = container.querySelector('input[type="password"]') as HTMLInputElement expect(input.placeholder).toBe('Not configured') }) it('lets renderField replace a role-tagged control', () => { render( { if (context.role !== 'credential-ref') return undefined return
{String(context.draftValue)}
}} />) expect(screen.getByTestId('credential-control').textContent).toBe('OPENAI_API_KEY') }) it('disables every control under disabled', () => { const { container } = render() for (const input of container.querySelectorAll('input, select, button')) { expect((input as HTMLInputElement).disabled).toBe(true) } }) }) describe('containers', () => { const Catalog = Schema.object({ models: Schema.array(Schema.object({ id: Schema.string().required() })), retryPolicy: Schema.object({ maxRetries: Schema.number() }), }) it('renders nested object groups', () => { render() expect(screen.getByText('retryPolicy')).toBeTruthy() expect(screen.getByText('maxRetries')).toBeTruthy() }) it('materializes fallback rows into the draft on add and edit', () => { const onChange = vi.fn() render() fireEvent.click(screen.getByText('Add')) expect(lastDraft(onChange)).toEqual({ models: [{ id: 'flash' }, {}] }) fireEvent.change(screen.getByPlaceholderText('Default: flash'), { target: { value: 'pro' } }) expect(lastDraft(onChange)).toEqual({ models: [{ id: 'pro' }] }) }) it('removes draft array rows wholesale', () => { const onChange = vi.fn() render() fireEvent.click(screen.getAllByText('Remove')[0] as HTMLElement) expect(lastDraft(onChange)).toEqual({ models: [{ id: 'pro' }] }) }) it('renders dict rows from both layers with removal only for draft keys', () => { const Providers = Schema.object({ providers: Schema.dict(Schema.object({ baseURL: Schema.string() })) }) const onChange = vi.fn() render() expect(screen.getByText('anthropic')).toBeTruthy() expect(screen.getByText('openai')).toBeTruthy() const removes = screen.getAllByText('Remove') expect(removes.map(button => button.disabled)).toEqual([true, false]) fireEvent.click(removes[1] as HTMLElement) expect(lastDraft(onChange)).toEqual({ providers: {} }) }) it('adds dict entries through a free-text key input', () => { const Providers = Schema.object({ providers: Schema.dict(Schema.object({ baseURL: Schema.string() })) }) const onChange = vi.fn() render() const add = screen.getByLabelText('Add') fireEvent.keyDown(add, { key: 'a' }) expect(onChange).not.toHaveBeenCalled() add.value = 'openai' fireEvent.keyDown(add, { key: 'Enter' }) expect(lastDraft(onChange)).toEqual({ providers: { openai: {} } }) add.value = '' fireEvent.keyDown(add, { key: 'Enter' }) expect(onChange).toHaveBeenCalledTimes(1) }) it('offers remaining sKey vocabulary as the add select', () => { const Providers = Schema.object({ providers: Schema.dict(Schema.object({ baseURL: Schema.string() }), Schema.union(['openai', 'anthropic'])), }) const onChange = vi.fn() render() const add = screen.getByLabelText('Add') expect([...add.options].map(option => option.value)).toEqual(['', 'anthropic']) fireEvent.change(add, { target: { value: 'anthropic' } }) expect(lastDraft(onChange)).toEqual({ providers: { openai: {}, anthropic: {} } }) }) it('materializes type-shaped empty values for every array inner kind', () => { const Kinds = Schema.object({ tags: Schema.array(Schema.string()), nums: Schema.array(Schema.number()), flags: Schema.array(Schema.boolean()), lists: Schema.array(Schema.array(Schema.string())), dicts: Schema.array(Schema.dict(Schema.string())), }) const onChange = vi.fn() render() const adds = screen.getAllByText('Add') const expected: Record = { tags: [''], nums: [0], flags: [false], lists: [[]], dicts: [{}], } Object.entries(expected).forEach(([key, value], index) => { fireEvent.click(adds[index] as HTMLElement) expect(lastDraft(onChange)).toEqual({ [key]: value }) }) }) it('falls back to a read-only view for unsupported nodes instead of dropping them', () => { const Mixed = Schema.object({ weird: Schema.union([Schema.string(), Schema.number()]) }) render() expect(screen.getByText('42')).toBeTruthy() expect(screen.getByText(/no form control/)).toBeTruthy() }) it('shows the draft value in the read-only fallback view, and nothing when both layers are empty', () => { const Mixed = Schema.object({ weird: Schema.union([Schema.string(), Schema.number()]) }) const { container } = render() expect(screen.getByText('"overridden"')).toBeTruthy() cleanup() const empty = render().container expect((empty.querySelector('pre') as HTMLElement).textContent).toBe('') expect(container).toBeTruthy() }) it('renders a structural object node without declared properties as an empty group', () => { const { container } = render() expect(container.querySelectorAll('input')).toHaveLength(0) }) })