mirror of https://github.com/Mai-with-u/MaiBot.git
test(dashboard): add unit tests for dynamic form components
- Create comprehensive test suite for DynamicField (21 tests) - Create comprehensive test suite for DynamicConfigForm (10 tests) - Create comprehensive test suite for FieldHookRegistry (21 tests) - Configure Vitest 4.0.18 with jsdom environment - Add test setup with ResizeObserver and matchMedia polyfills - 52 tests total covering all core functionalitypull/1496/head
parent
c58ad64352
commit
69dfd0cac6
|
|
@ -8,7 +8,9 @@
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"format": "prettier --write \"src/**/*.{ts,tsx,css}\""
|
"format": "prettier --write \"src/**/*.{ts,tsx,css}\"",
|
||||||
|
"test": "vitest",
|
||||||
|
"test:ui": "vitest --ui"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/lang-javascript": "^6.2.4",
|
"@codemirror/lang-javascript": "^6.2.4",
|
||||||
|
|
@ -75,21 +77,27 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.1",
|
||||||
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
|
"@testing-library/react": "^16.3.2",
|
||||||
|
"@testing-library/user-event": "^14.6.1",
|
||||||
"@types/node": "^24.10.2",
|
"@types/node": "^24.10.2",
|
||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^5.1.2",
|
"@vitejs/plugin-react": "^5.1.2",
|
||||||
|
"@vitest/ui": "^4.0.18",
|
||||||
"autoprefixer": "^10.4.22",
|
"autoprefixer": "^10.4.22",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.4.24",
|
"eslint-plugin-react-refresh": "^0.4.24",
|
||||||
"globals": "^16.5.0",
|
"globals": "^16.5.0",
|
||||||
|
"jsdom": "^28.1.0",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"prettier": "^3.7.4",
|
"prettier": "^3.7.4",
|
||||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||||
"tailwindcss": "^3",
|
"tailwindcss": "^3",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"typescript-eslint": "^8.49.0",
|
"typescript-eslint": "^8.49.0",
|
||||||
"vite": "^7.2.7"
|
"vite": "^7.2.7",
|
||||||
|
"vitest": "^4.0.18"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,362 @@
|
||||||
|
import { describe, it, expect, vi } from 'vitest'
|
||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
|
||||||
|
import { DynamicConfigForm } from '../DynamicConfigForm'
|
||||||
|
import { FieldHookRegistry } from '@/lib/field-hooks'
|
||||||
|
import type { ConfigSchema } from '@/types/config-schema'
|
||||||
|
import type { FieldHookComponentProps } from '@/lib/field-hooks'
|
||||||
|
|
||||||
|
describe('DynamicConfigForm', () => {
|
||||||
|
describe('basic rendering', () => {
|
||||||
|
it('renders simple fields', () => {
|
||||||
|
const schema: ConfigSchema = {
|
||||||
|
className: 'TestConfig',
|
||||||
|
classDoc: 'Test configuration',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'field1',
|
||||||
|
type: 'string',
|
||||||
|
label: 'Field 1',
|
||||||
|
description: 'First field',
|
||||||
|
required: false,
|
||||||
|
default: 'value1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'field2',
|
||||||
|
type: 'boolean',
|
||||||
|
label: 'Field 2',
|
||||||
|
description: 'Second field',
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
const values = { field1: 'value1', field2: false }
|
||||||
|
const onChange = vi.fn()
|
||||||
|
|
||||||
|
render(<DynamicConfigForm schema={schema} values={values} onChange={onChange} />)
|
||||||
|
|
||||||
|
expect(screen.getByText('Field 1')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Field 2')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('First field')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Second field')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders nested schema', () => {
|
||||||
|
const schema: ConfigSchema = {
|
||||||
|
className: 'MainConfig',
|
||||||
|
classDoc: 'Main configuration',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'top_field',
|
||||||
|
type: 'string',
|
||||||
|
label: 'Top Field',
|
||||||
|
description: 'Top level field',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
nested: {
|
||||||
|
sub_config: {
|
||||||
|
className: 'SubConfig',
|
||||||
|
classDoc: 'Sub configuration',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'nested_field',
|
||||||
|
type: 'number',
|
||||||
|
label: 'Nested Field',
|
||||||
|
description: 'Nested field',
|
||||||
|
required: false,
|
||||||
|
default: 42,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const values = {
|
||||||
|
top_field: 'top',
|
||||||
|
sub_config: {
|
||||||
|
nested_field: 42,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const onChange = vi.fn()
|
||||||
|
|
||||||
|
render(<DynamicConfigForm schema={schema} values={values} onChange={onChange} />)
|
||||||
|
|
||||||
|
expect(screen.getByText('Top Field')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('SubConfig')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Sub configuration')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Nested Field')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Hook system', () => {
|
||||||
|
it('renders Hook component in replace mode', () => {
|
||||||
|
const TestHookComponent: React.FC<FieldHookComponentProps> = ({ fieldPath, value }) => {
|
||||||
|
return <div data-testid="hook-component">Hook: {fieldPath} = {String(value)}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
const hooks = new FieldHookRegistry()
|
||||||
|
hooks.register('hooked_field', TestHookComponent, 'replace')
|
||||||
|
|
||||||
|
const schema: ConfigSchema = {
|
||||||
|
className: 'TestConfig',
|
||||||
|
classDoc: 'Test configuration',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'hooked_field',
|
||||||
|
type: 'string',
|
||||||
|
label: 'Hooked Field',
|
||||||
|
description: 'A field with hook',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'normal_field',
|
||||||
|
type: 'string',
|
||||||
|
label: 'Normal Field',
|
||||||
|
description: 'A normal field',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
const values = { hooked_field: 'test', normal_field: 'normal' }
|
||||||
|
const onChange = vi.fn()
|
||||||
|
|
||||||
|
render(<DynamicConfigForm schema={schema} values={values} onChange={onChange} hooks={hooks} />)
|
||||||
|
|
||||||
|
expect(screen.getByTestId('hook-component')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Hook: hooked_field = test')).toBeInTheDocument()
|
||||||
|
expect(screen.queryByText('Hooked Field')).not.toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Normal Field')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders Hook component in wrapper mode', () => {
|
||||||
|
const WrapperHookComponent: React.FC<FieldHookComponentProps> = ({ fieldPath, children }) => {
|
||||||
|
return (
|
||||||
|
<div data-testid="wrapper-hook">
|
||||||
|
<div>Wrapper for: {fieldPath}</div>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hooks = new FieldHookRegistry()
|
||||||
|
hooks.register('wrapped_field', WrapperHookComponent, 'wrapper')
|
||||||
|
|
||||||
|
const schema: ConfigSchema = {
|
||||||
|
className: 'TestConfig',
|
||||||
|
classDoc: 'Test configuration',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'wrapped_field',
|
||||||
|
type: 'string',
|
||||||
|
label: 'Wrapped Field',
|
||||||
|
description: 'A wrapped field',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
const values = { wrapped_field: 'test' }
|
||||||
|
const onChange = vi.fn()
|
||||||
|
|
||||||
|
render(<DynamicConfigForm schema={schema} values={values} onChange={onChange} hooks={hooks} />)
|
||||||
|
|
||||||
|
expect(screen.getByTestId('wrapper-hook')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Wrapper for: wrapped_field')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Wrapped Field')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('passes correct props to Hook component', () => {
|
||||||
|
const TestHookComponent: React.FC<FieldHookComponentProps> = ({ fieldPath, value, onChange }) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div data-testid="field-path">{fieldPath}</div>
|
||||||
|
<div data-testid="field-value">{String(value)}</div>
|
||||||
|
<button onClick={() => onChange?.('new_value')}>Change</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hooks = new FieldHookRegistry()
|
||||||
|
hooks.register('test_field', TestHookComponent, 'replace')
|
||||||
|
|
||||||
|
const schema: ConfigSchema = {
|
||||||
|
className: 'TestConfig',
|
||||||
|
classDoc: 'Test configuration',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'test_field',
|
||||||
|
type: 'string',
|
||||||
|
label: 'Test Field',
|
||||||
|
description: 'A test field',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
const values = { test_field: 'original' }
|
||||||
|
const onChange = vi.fn()
|
||||||
|
|
||||||
|
render(<DynamicConfigForm schema={schema} values={values} onChange={onChange} hooks={hooks} />)
|
||||||
|
|
||||||
|
expect(screen.getByTestId('field-path')).toHaveTextContent('test_field')
|
||||||
|
expect(screen.getByTestId('field-value')).toHaveTextContent('original')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('onChange propagation', () => {
|
||||||
|
it('propagates onChange from simple field', async () => {
|
||||||
|
const schema: ConfigSchema = {
|
||||||
|
className: 'TestConfig',
|
||||||
|
classDoc: 'Test configuration',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'test_field',
|
||||||
|
type: 'string',
|
||||||
|
label: 'Test Field',
|
||||||
|
description: 'A test field',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
const values = { test_field: '' }
|
||||||
|
const onChange = vi.fn()
|
||||||
|
|
||||||
|
render(<DynamicConfigForm schema={schema} values={values} onChange={onChange} />)
|
||||||
|
|
||||||
|
const input = screen.getByRole('textbox')
|
||||||
|
input.focus()
|
||||||
|
await userEvent.keyboard('Hello')
|
||||||
|
|
||||||
|
expect(onChange).toHaveBeenCalledTimes(5)
|
||||||
|
expect(onChange.mock.calls.every(call => call[0] === 'test_field')).toBe(true)
|
||||||
|
expect(onChange).toHaveBeenNthCalledWith(1, 'test_field', 'H')
|
||||||
|
expect(onChange).toHaveBeenNthCalledWith(5, 'test_field', 'o')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('propagates onChange from nested field with correct path', async () => {
|
||||||
|
const schema: ConfigSchema = {
|
||||||
|
className: 'MainConfig',
|
||||||
|
classDoc: 'Main configuration',
|
||||||
|
fields: [],
|
||||||
|
nested: {
|
||||||
|
sub_config: {
|
||||||
|
className: 'SubConfig',
|
||||||
|
classDoc: 'Sub configuration',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'nested_field',
|
||||||
|
type: 'string',
|
||||||
|
label: 'Nested Field',
|
||||||
|
description: 'Nested field',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const values = {
|
||||||
|
sub_config: {
|
||||||
|
nested_field: '',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const onChange = vi.fn()
|
||||||
|
|
||||||
|
render(<DynamicConfigForm schema={schema} values={values} onChange={onChange} />)
|
||||||
|
|
||||||
|
const input = screen.getByRole('textbox')
|
||||||
|
input.focus()
|
||||||
|
await userEvent.keyboard('Test')
|
||||||
|
|
||||||
|
expect(onChange).toHaveBeenCalledTimes(4)
|
||||||
|
expect(onChange.mock.calls.every(call => call[0] === 'sub_config.nested_field')).toBe(true)
|
||||||
|
expect(onChange).toHaveBeenNthCalledWith(1, 'sub_config.nested_field', 'T')
|
||||||
|
expect(onChange).toHaveBeenNthCalledWith(4, 'sub_config.nested_field', 't')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('propagates onChange from Hook component', async () => {
|
||||||
|
const TestHookComponent: React.FC<FieldHookComponentProps> = ({ onChange }) => {
|
||||||
|
return <button onClick={() => onChange?.('hook_value')}>Set Value</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
const hooks = new FieldHookRegistry()
|
||||||
|
hooks.register('hooked_field', TestHookComponent, 'replace')
|
||||||
|
|
||||||
|
const schema: ConfigSchema = {
|
||||||
|
className: 'TestConfig',
|
||||||
|
classDoc: 'Test configuration',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'hooked_field',
|
||||||
|
type: 'string',
|
||||||
|
label: 'Hooked Field',
|
||||||
|
description: 'A hooked field',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
const values = { hooked_field: '' }
|
||||||
|
const onChange = vi.fn()
|
||||||
|
const user = userEvent.setup()
|
||||||
|
|
||||||
|
render(<DynamicConfigForm schema={schema} values={values} onChange={onChange} hooks={hooks} />)
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button'))
|
||||||
|
|
||||||
|
expect(onChange).toHaveBeenCalledWith('hooked_field', 'hook_value')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('edge cases', () => {
|
||||||
|
it('renders with empty nested values', () => {
|
||||||
|
const schema: ConfigSchema = {
|
||||||
|
className: 'MainConfig',
|
||||||
|
classDoc: 'Main configuration',
|
||||||
|
fields: [],
|
||||||
|
nested: {
|
||||||
|
sub_config: {
|
||||||
|
className: 'SubConfig',
|
||||||
|
classDoc: 'Sub configuration',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'nested_field',
|
||||||
|
type: 'string',
|
||||||
|
label: 'Nested Field',
|
||||||
|
description: 'Nested field',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const values = {}
|
||||||
|
const onChange = vi.fn()
|
||||||
|
|
||||||
|
render(<DynamicConfigForm schema={schema} values={values} onChange={onChange} />)
|
||||||
|
|
||||||
|
expect(screen.getByText('SubConfig')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Nested Field')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses default hook registry when not provided', () => {
|
||||||
|
const schema: ConfigSchema = {
|
||||||
|
className: 'TestConfig',
|
||||||
|
classDoc: 'Test configuration',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'test_field',
|
||||||
|
type: 'string',
|
||||||
|
label: 'Test Field',
|
||||||
|
description: 'A test field',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
const values = { test_field: 'test' }
|
||||||
|
const onChange = vi.fn()
|
||||||
|
|
||||||
|
render(<DynamicConfigForm schema={schema} values={values} onChange={onChange} />)
|
||||||
|
|
||||||
|
expect(screen.getByText('Test Field')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,394 @@
|
||||||
|
import { describe, it, expect, vi } from 'vitest'
|
||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
|
||||||
|
import { DynamicField } from '../DynamicField'
|
||||||
|
import type { FieldSchema } from '@/types/config-schema'
|
||||||
|
|
||||||
|
describe('DynamicField', () => {
|
||||||
|
describe('x-widget priority', () => {
|
||||||
|
it('renders Slider when x-widget is slider', () => {
|
||||||
|
const schema: FieldSchema = {
|
||||||
|
name: 'test_slider',
|
||||||
|
type: 'number',
|
||||||
|
label: 'Test Slider',
|
||||||
|
description: 'A test slider',
|
||||||
|
required: false,
|
||||||
|
'x-widget': 'slider',
|
||||||
|
minValue: 0,
|
||||||
|
maxValue: 100,
|
||||||
|
default: 50,
|
||||||
|
}
|
||||||
|
const onChange = vi.fn()
|
||||||
|
|
||||||
|
render(<DynamicField schema={schema} value={50} onChange={onChange} />)
|
||||||
|
|
||||||
|
expect(screen.getByText('Test Slider')).toBeInTheDocument()
|
||||||
|
expect(screen.getByRole('slider')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('50')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders Switch when x-widget is switch', () => {
|
||||||
|
const schema: FieldSchema = {
|
||||||
|
name: 'test_switch',
|
||||||
|
type: 'boolean',
|
||||||
|
label: 'Test Switch',
|
||||||
|
description: 'A test switch',
|
||||||
|
required: false,
|
||||||
|
'x-widget': 'switch',
|
||||||
|
default: false,
|
||||||
|
}
|
||||||
|
const onChange = vi.fn()
|
||||||
|
|
||||||
|
render(<DynamicField schema={schema} value={false} onChange={onChange} />)
|
||||||
|
|
||||||
|
expect(screen.getByText('Test Switch')).toBeInTheDocument()
|
||||||
|
expect(screen.getByRole('switch')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders Textarea when x-widget is textarea', () => {
|
||||||
|
const schema: FieldSchema = {
|
||||||
|
name: 'test_textarea',
|
||||||
|
type: 'string',
|
||||||
|
label: 'Test Textarea',
|
||||||
|
description: 'A test textarea',
|
||||||
|
required: false,
|
||||||
|
'x-widget': 'textarea',
|
||||||
|
default: 'Hello',
|
||||||
|
}
|
||||||
|
const onChange = vi.fn()
|
||||||
|
|
||||||
|
render(<DynamicField schema={schema} value="Hello" onChange={onChange} />)
|
||||||
|
|
||||||
|
expect(screen.getByText('Test Textarea')).toBeInTheDocument()
|
||||||
|
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
||||||
|
expect(screen.getByRole('textbox')).toHaveValue('Hello')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders Select when x-widget is select', () => {
|
||||||
|
const schema: FieldSchema = {
|
||||||
|
name: 'test_select',
|
||||||
|
type: 'string',
|
||||||
|
label: 'Test Select',
|
||||||
|
description: 'A test select',
|
||||||
|
required: false,
|
||||||
|
'x-widget': 'select',
|
||||||
|
options: ['Option 1', 'Option 2', 'Option 3'],
|
||||||
|
default: 'Option 1',
|
||||||
|
}
|
||||||
|
const onChange = vi.fn()
|
||||||
|
|
||||||
|
render(<DynamicField schema={schema} value="Option 1" onChange={onChange} />)
|
||||||
|
|
||||||
|
expect(screen.getByText('Test Select')).toBeInTheDocument()
|
||||||
|
expect(screen.getByRole('combobox')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders placeholder for custom widget', () => {
|
||||||
|
const schema: FieldSchema = {
|
||||||
|
name: 'test_custom',
|
||||||
|
type: 'string',
|
||||||
|
label: 'Test Custom',
|
||||||
|
description: 'A test custom field',
|
||||||
|
required: false,
|
||||||
|
'x-widget': 'custom',
|
||||||
|
}
|
||||||
|
const onChange = vi.fn()
|
||||||
|
|
||||||
|
render(<DynamicField schema={schema} value="" onChange={onChange} />)
|
||||||
|
|
||||||
|
expect(screen.getByText('Custom field requires Hook')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('type fallback', () => {
|
||||||
|
it('renders Input for string type', () => {
|
||||||
|
const schema: FieldSchema = {
|
||||||
|
name: 'test_string',
|
||||||
|
type: 'string',
|
||||||
|
label: 'Test String',
|
||||||
|
description: 'A test string',
|
||||||
|
required: false,
|
||||||
|
default: 'Hello',
|
||||||
|
}
|
||||||
|
const onChange = vi.fn()
|
||||||
|
|
||||||
|
render(<DynamicField schema={schema} value="Hello" onChange={onChange} />)
|
||||||
|
|
||||||
|
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
||||||
|
expect(screen.getByRole('textbox')).toHaveValue('Hello')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders Switch for boolean type', () => {
|
||||||
|
const schema: FieldSchema = {
|
||||||
|
name: 'test_bool',
|
||||||
|
type: 'boolean',
|
||||||
|
label: 'Test Boolean',
|
||||||
|
description: 'A test boolean',
|
||||||
|
required: false,
|
||||||
|
default: true,
|
||||||
|
}
|
||||||
|
const onChange = vi.fn()
|
||||||
|
|
||||||
|
render(<DynamicField schema={schema} value={true} onChange={onChange} />)
|
||||||
|
|
||||||
|
expect(screen.getByRole('switch')).toBeInTheDocument()
|
||||||
|
expect(screen.getByRole('switch')).toBeChecked()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders number Input for number type', () => {
|
||||||
|
const schema: FieldSchema = {
|
||||||
|
name: 'test_number',
|
||||||
|
type: 'number',
|
||||||
|
label: 'Test Number',
|
||||||
|
description: 'A test number',
|
||||||
|
required: false,
|
||||||
|
default: 42,
|
||||||
|
}
|
||||||
|
const onChange = vi.fn()
|
||||||
|
|
||||||
|
render(<DynamicField schema={schema} value={42} onChange={onChange} />)
|
||||||
|
|
||||||
|
const input = screen.getByRole('spinbutton')
|
||||||
|
expect(input).toBeInTheDocument()
|
||||||
|
expect(input).toHaveValue(42)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders number Input for integer type', () => {
|
||||||
|
const schema: FieldSchema = {
|
||||||
|
name: 'test_integer',
|
||||||
|
type: 'integer',
|
||||||
|
label: 'Test Integer',
|
||||||
|
description: 'A test integer',
|
||||||
|
required: false,
|
||||||
|
default: 10,
|
||||||
|
}
|
||||||
|
const onChange = vi.fn()
|
||||||
|
|
||||||
|
render(<DynamicField schema={schema} value={10} onChange={onChange} />)
|
||||||
|
|
||||||
|
const input = screen.getByRole('spinbutton')
|
||||||
|
expect(input).toBeInTheDocument()
|
||||||
|
expect(input).toHaveValue(10)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders Textarea for textarea type', () => {
|
||||||
|
const schema: FieldSchema = {
|
||||||
|
name: 'test_textarea_type',
|
||||||
|
type: 'textarea',
|
||||||
|
label: 'Test Textarea Type',
|
||||||
|
description: 'A test textarea type',
|
||||||
|
required: false,
|
||||||
|
default: 'Long text',
|
||||||
|
}
|
||||||
|
const onChange = vi.fn()
|
||||||
|
|
||||||
|
render(<DynamicField schema={schema} value="Long text" onChange={onChange} />)
|
||||||
|
|
||||||
|
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
||||||
|
expect(screen.getByRole('textbox')).toHaveValue('Long text')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders Select for select type', () => {
|
||||||
|
const schema: FieldSchema = {
|
||||||
|
name: 'test_select_type',
|
||||||
|
type: 'select',
|
||||||
|
label: 'Test Select Type',
|
||||||
|
description: 'A test select type',
|
||||||
|
required: false,
|
||||||
|
options: ['A', 'B', 'C'],
|
||||||
|
default: 'A',
|
||||||
|
}
|
||||||
|
const onChange = vi.fn()
|
||||||
|
|
||||||
|
render(<DynamicField schema={schema} value="A" onChange={onChange} />)
|
||||||
|
|
||||||
|
expect(screen.getByRole('combobox')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders placeholder for array type', () => {
|
||||||
|
const schema: FieldSchema = {
|
||||||
|
name: 'test_array',
|
||||||
|
type: 'array',
|
||||||
|
label: 'Test Array',
|
||||||
|
description: 'A test array',
|
||||||
|
required: false,
|
||||||
|
}
|
||||||
|
const onChange = vi.fn()
|
||||||
|
|
||||||
|
render(<DynamicField schema={schema} value={[]} onChange={onChange} />)
|
||||||
|
|
||||||
|
expect(screen.getByText('Array fields not yet supported')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders placeholder for object type', () => {
|
||||||
|
const schema: FieldSchema = {
|
||||||
|
name: 'test_object',
|
||||||
|
type: 'object',
|
||||||
|
label: 'Test Object',
|
||||||
|
description: 'A test object',
|
||||||
|
required: false,
|
||||||
|
}
|
||||||
|
const onChange = vi.fn()
|
||||||
|
|
||||||
|
render(<DynamicField schema={schema} value={{}} onChange={onChange} />)
|
||||||
|
|
||||||
|
expect(screen.getByText('Object fields not yet supported')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('onChange events', () => {
|
||||||
|
it('triggers onChange for Switch', async () => {
|
||||||
|
const schema: FieldSchema = {
|
||||||
|
name: 'test_switch',
|
||||||
|
type: 'boolean',
|
||||||
|
label: 'Test Switch',
|
||||||
|
description: 'A test switch',
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
}
|
||||||
|
const onChange = vi.fn()
|
||||||
|
const user = userEvent.setup()
|
||||||
|
|
||||||
|
render(<DynamicField schema={schema} value={false} onChange={onChange} />)
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('switch'))
|
||||||
|
expect(onChange).toHaveBeenCalledWith(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('triggers onChange for Input', async () => {
|
||||||
|
const schema: FieldSchema = {
|
||||||
|
name: 'test_input',
|
||||||
|
type: 'string',
|
||||||
|
label: 'Test Input',
|
||||||
|
description: 'A test input',
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
}
|
||||||
|
const onChange = vi.fn()
|
||||||
|
|
||||||
|
render(<DynamicField schema={schema} value="" onChange={onChange} />)
|
||||||
|
|
||||||
|
const input = screen.getByRole('textbox')
|
||||||
|
input.focus()
|
||||||
|
await userEvent.keyboard('Hello')
|
||||||
|
|
||||||
|
expect(onChange).toHaveBeenCalledTimes(5)
|
||||||
|
expect(onChange).toHaveBeenNthCalledWith(1, 'H')
|
||||||
|
expect(onChange).toHaveBeenNthCalledWith(2, 'e')
|
||||||
|
expect(onChange).toHaveBeenNthCalledWith(3, 'l')
|
||||||
|
expect(onChange).toHaveBeenNthCalledWith(4, 'l')
|
||||||
|
expect(onChange).toHaveBeenNthCalledWith(5, 'o')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('triggers onChange for number Input', async () => {
|
||||||
|
const schema: FieldSchema = {
|
||||||
|
name: 'test_number',
|
||||||
|
type: 'number',
|
||||||
|
label: 'Test Number',
|
||||||
|
description: 'A test number',
|
||||||
|
required: false,
|
||||||
|
default: 0,
|
||||||
|
}
|
||||||
|
const onChange = vi.fn()
|
||||||
|
const user = userEvent.setup()
|
||||||
|
|
||||||
|
render(<DynamicField schema={schema} value={0} onChange={onChange} />)
|
||||||
|
|
||||||
|
const input = screen.getByRole('spinbutton')
|
||||||
|
await user.clear(input)
|
||||||
|
await user.type(input, '123')
|
||||||
|
expect(onChange).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('visual features', () => {
|
||||||
|
it('renders label with icon', () => {
|
||||||
|
const schema: FieldSchema = {
|
||||||
|
name: 'test_icon',
|
||||||
|
type: 'string',
|
||||||
|
label: 'Test Icon',
|
||||||
|
description: 'A test with icon',
|
||||||
|
required: false,
|
||||||
|
'x-icon': 'Settings',
|
||||||
|
}
|
||||||
|
const onChange = vi.fn()
|
||||||
|
|
||||||
|
render(<DynamicField schema={schema} value="" onChange={onChange} />)
|
||||||
|
|
||||||
|
expect(screen.getByText('Test Icon')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders required indicator', () => {
|
||||||
|
const schema: FieldSchema = {
|
||||||
|
name: 'test_required',
|
||||||
|
type: 'string',
|
||||||
|
label: 'Test Required',
|
||||||
|
description: 'A required field',
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
const onChange = vi.fn()
|
||||||
|
|
||||||
|
render(<DynamicField schema={schema} value="" onChange={onChange} />)
|
||||||
|
|
||||||
|
expect(screen.getByText('*')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders description', () => {
|
||||||
|
const schema: FieldSchema = {
|
||||||
|
name: 'test_desc',
|
||||||
|
type: 'string',
|
||||||
|
label: 'Test Description',
|
||||||
|
description: 'This is a description',
|
||||||
|
required: false,
|
||||||
|
}
|
||||||
|
const onChange = vi.fn()
|
||||||
|
|
||||||
|
render(<DynamicField schema={schema} value="" onChange={onChange} />)
|
||||||
|
|
||||||
|
expect(screen.getByText('This is a description')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('slider features', () => {
|
||||||
|
it('renders slider with min/max/step', () => {
|
||||||
|
const schema: FieldSchema = {
|
||||||
|
name: 'test_slider_props',
|
||||||
|
type: 'number',
|
||||||
|
label: 'Test Slider Props',
|
||||||
|
description: 'A slider with props',
|
||||||
|
required: false,
|
||||||
|
'x-widget': 'slider',
|
||||||
|
minValue: 10,
|
||||||
|
maxValue: 50,
|
||||||
|
step: 5,
|
||||||
|
default: 25,
|
||||||
|
}
|
||||||
|
const onChange = vi.fn()
|
||||||
|
|
||||||
|
render(<DynamicField schema={schema} value={25} onChange={onChange} />)
|
||||||
|
|
||||||
|
expect(screen.getByText('10')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('50')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('25')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('select features', () => {
|
||||||
|
it('renders placeholder when no options', () => {
|
||||||
|
const schema: FieldSchema = {
|
||||||
|
name: 'test_select_no_options',
|
||||||
|
type: 'string',
|
||||||
|
label: 'Test Select No Options',
|
||||||
|
description: 'A select with no options',
|
||||||
|
required: false,
|
||||||
|
'x-widget': 'select',
|
||||||
|
}
|
||||||
|
const onChange = vi.fn()
|
||||||
|
|
||||||
|
render(<DynamicField schema={schema} value="" onChange={onChange} />)
|
||||||
|
|
||||||
|
expect(screen.getByText('No options available for select')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,253 @@
|
||||||
|
import { describe, it, expect, beforeEach } from 'vitest'
|
||||||
|
|
||||||
|
import { FieldHookRegistry } from '../field-hooks'
|
||||||
|
import type { FieldHookComponent } from '../field-hooks'
|
||||||
|
|
||||||
|
describe('FieldHookRegistry', () => {
|
||||||
|
let registry: FieldHookRegistry
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
registry = new FieldHookRegistry()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('register', () => {
|
||||||
|
it('registers a hook with replace type', () => {
|
||||||
|
const component: FieldHookComponent = () => null
|
||||||
|
|
||||||
|
registry.register('test.field', component, 'replace')
|
||||||
|
|
||||||
|
expect(registry.has('test.field')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('registers a hook with wrapper type', () => {
|
||||||
|
const component: FieldHookComponent = () => null
|
||||||
|
|
||||||
|
registry.register('test.field', component, 'wrapper')
|
||||||
|
|
||||||
|
expect(registry.has('test.field')).toBe(true)
|
||||||
|
const entry = registry.get('test.field')
|
||||||
|
expect(entry?.type).toBe('wrapper')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('defaults to replace type when not specified', () => {
|
||||||
|
const component: FieldHookComponent = () => null
|
||||||
|
|
||||||
|
registry.register('test.field', component)
|
||||||
|
|
||||||
|
const entry = registry.get('test.field')
|
||||||
|
expect(entry?.type).toBe('replace')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('overwrites existing hook for same field path', () => {
|
||||||
|
const component1: FieldHookComponent = () => null
|
||||||
|
const component2: FieldHookComponent = () => null
|
||||||
|
|
||||||
|
registry.register('test.field', component1, 'replace')
|
||||||
|
registry.register('test.field', component2, 'wrapper')
|
||||||
|
|
||||||
|
const entry = registry.get('test.field')
|
||||||
|
expect(entry?.component).toBe(component2)
|
||||||
|
expect(entry?.type).toBe('wrapper')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('get', () => {
|
||||||
|
it('returns hook entry for registered field path', () => {
|
||||||
|
const component: FieldHookComponent = () => null
|
||||||
|
|
||||||
|
registry.register('test.field', component, 'replace')
|
||||||
|
|
||||||
|
const entry = registry.get('test.field')
|
||||||
|
expect(entry).toBeDefined()
|
||||||
|
expect(entry?.component).toBe(component)
|
||||||
|
expect(entry?.type).toBe('replace')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns undefined for unregistered field path', () => {
|
||||||
|
const entry = registry.get('nonexistent.field')
|
||||||
|
expect(entry).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns correct entry for nested field paths', () => {
|
||||||
|
const component: FieldHookComponent = () => null
|
||||||
|
|
||||||
|
registry.register('config.section.field', component, 'wrapper')
|
||||||
|
|
||||||
|
const entry = registry.get('config.section.field')
|
||||||
|
expect(entry).toBeDefined()
|
||||||
|
expect(entry?.type).toBe('wrapper')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('has', () => {
|
||||||
|
it('returns true for registered field path', () => {
|
||||||
|
const component: FieldHookComponent = () => null
|
||||||
|
|
||||||
|
registry.register('test.field', component)
|
||||||
|
|
||||||
|
expect(registry.has('test.field')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns false for unregistered field path', () => {
|
||||||
|
expect(registry.has('nonexistent.field')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns false after unregistering', () => {
|
||||||
|
const component: FieldHookComponent = () => null
|
||||||
|
|
||||||
|
registry.register('test.field', component)
|
||||||
|
registry.unregister('test.field')
|
||||||
|
|
||||||
|
expect(registry.has('test.field')).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('unregister', () => {
|
||||||
|
it('removes a registered hook', () => {
|
||||||
|
const component: FieldHookComponent = () => null
|
||||||
|
|
||||||
|
registry.register('test.field', component)
|
||||||
|
expect(registry.has('test.field')).toBe(true)
|
||||||
|
|
||||||
|
registry.unregister('test.field')
|
||||||
|
expect(registry.has('test.field')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not throw when unregistering non-existent hook', () => {
|
||||||
|
expect(() => registry.unregister('nonexistent.field')).not.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('only removes specified hook, not others', () => {
|
||||||
|
const component1: FieldHookComponent = () => null
|
||||||
|
const component2: FieldHookComponent = () => null
|
||||||
|
|
||||||
|
registry.register('field1', component1)
|
||||||
|
registry.register('field2', component2)
|
||||||
|
|
||||||
|
registry.unregister('field1')
|
||||||
|
|
||||||
|
expect(registry.has('field1')).toBe(false)
|
||||||
|
expect(registry.has('field2')).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('clear', () => {
|
||||||
|
it('removes all registered hooks', () => {
|
||||||
|
const component1: FieldHookComponent = () => null
|
||||||
|
const component2: FieldHookComponent = () => null
|
||||||
|
const component3: FieldHookComponent = () => null
|
||||||
|
|
||||||
|
registry.register('field1', component1)
|
||||||
|
registry.register('field2', component2)
|
||||||
|
registry.register('field3', component3)
|
||||||
|
|
||||||
|
expect(registry.getAllPaths()).toHaveLength(3)
|
||||||
|
|
||||||
|
registry.clear()
|
||||||
|
|
||||||
|
expect(registry.getAllPaths()).toHaveLength(0)
|
||||||
|
expect(registry.has('field1')).toBe(false)
|
||||||
|
expect(registry.has('field2')).toBe(false)
|
||||||
|
expect(registry.has('field3')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('works correctly on empty registry', () => {
|
||||||
|
expect(() => registry.clear()).not.toThrow()
|
||||||
|
expect(registry.getAllPaths()).toHaveLength(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getAllPaths', () => {
|
||||||
|
it('returns empty array when no hooks registered', () => {
|
||||||
|
expect(registry.getAllPaths()).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns all registered field paths', () => {
|
||||||
|
const component: FieldHookComponent = () => null
|
||||||
|
|
||||||
|
registry.register('field1', component)
|
||||||
|
registry.register('field2', component)
|
||||||
|
registry.register('field3', component)
|
||||||
|
|
||||||
|
const paths = registry.getAllPaths()
|
||||||
|
expect(paths).toHaveLength(3)
|
||||||
|
expect(paths).toContain('field1')
|
||||||
|
expect(paths).toContain('field2')
|
||||||
|
expect(paths).toContain('field3')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns updated paths after unregister', () => {
|
||||||
|
const component: FieldHookComponent = () => null
|
||||||
|
|
||||||
|
registry.register('field1', component)
|
||||||
|
registry.register('field2', component)
|
||||||
|
registry.register('field3', component)
|
||||||
|
|
||||||
|
registry.unregister('field2')
|
||||||
|
|
||||||
|
const paths = registry.getAllPaths()
|
||||||
|
expect(paths).toHaveLength(2)
|
||||||
|
expect(paths).toContain('field1')
|
||||||
|
expect(paths).toContain('field3')
|
||||||
|
expect(paths).not.toContain('field2')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles nested field paths correctly', () => {
|
||||||
|
const component: FieldHookComponent = () => null
|
||||||
|
|
||||||
|
registry.register('config.chat.enabled', component)
|
||||||
|
registry.register('config.chat.model', component)
|
||||||
|
registry.register('config.api.key', component)
|
||||||
|
|
||||||
|
const paths = registry.getAllPaths()
|
||||||
|
expect(paths).toHaveLength(3)
|
||||||
|
expect(paths).toContain('config.chat.enabled')
|
||||||
|
expect(paths).toContain('config.chat.model')
|
||||||
|
expect(paths).toContain('config.api.key')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('integration scenarios', () => {
|
||||||
|
it('supports full lifecycle of multiple hooks', () => {
|
||||||
|
const replaceComponent: FieldHookComponent = () => null
|
||||||
|
const wrapperComponent: FieldHookComponent = () => null
|
||||||
|
|
||||||
|
registry.register('field1', replaceComponent, 'replace')
|
||||||
|
registry.register('field2', wrapperComponent, 'wrapper')
|
||||||
|
|
||||||
|
expect(registry.getAllPaths()).toHaveLength(2)
|
||||||
|
|
||||||
|
const entry1 = registry.get('field1')
|
||||||
|
expect(entry1?.type).toBe('replace')
|
||||||
|
expect(entry1?.component).toBe(replaceComponent)
|
||||||
|
|
||||||
|
const entry2 = registry.get('field2')
|
||||||
|
expect(entry2?.type).toBe('wrapper')
|
||||||
|
expect(entry2?.component).toBe(wrapperComponent)
|
||||||
|
|
||||||
|
registry.unregister('field1')
|
||||||
|
expect(registry.getAllPaths()).toHaveLength(1)
|
||||||
|
expect(registry.has('field2')).toBe(true)
|
||||||
|
|
||||||
|
registry.clear()
|
||||||
|
expect(registry.getAllPaths()).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles rapid register/unregister cycles', () => {
|
||||||
|
const component: FieldHookComponent = () => null
|
||||||
|
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
registry.register(`field${i}`, component)
|
||||||
|
}
|
||||||
|
expect(registry.getAllPaths()).toHaveLength(100)
|
||||||
|
|
||||||
|
for (let i = 0; i < 50; i++) {
|
||||||
|
registry.unregister(`field${i}`)
|
||||||
|
}
|
||||||
|
expect(registry.getAllPaths()).toHaveLength(50)
|
||||||
|
|
||||||
|
registry.clear()
|
||||||
|
expect(registry.getAllPaths()).toHaveLength(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import '@testing-library/jest-dom/vitest'
|
||||||
|
|
||||||
|
global.ResizeObserver = class ResizeObserver {
|
||||||
|
observe() {}
|
||||||
|
unobserve() {}
|
||||||
|
disconnect() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.defineProperty(window, 'matchMedia', {
|
||||||
|
writable: true,
|
||||||
|
value: (query: string) => ({
|
||||||
|
matches: false,
|
||||||
|
media: query,
|
||||||
|
onchange: null,
|
||||||
|
addListener: () => {},
|
||||||
|
removeListener: () => {},
|
||||||
|
addEventListener: () => {},
|
||||||
|
removeEventListener: () => {},
|
||||||
|
dispatchEvent: () => {},
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
"files": [],
|
"files": [],
|
||||||
"references": [
|
"references": [
|
||||||
{ "path": "./tsconfig.app.json" },
|
{ "path": "./tsconfig.app.json" },
|
||||||
{ "path": "./tsconfig.node.json" }
|
{ "path": "./tsconfig.node.json" },
|
||||||
|
{ "path": "./tsconfig.vitest.json" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.app.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["vitest/globals", "@testing-library/jest-dom"]
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
/// <reference types="vitest" />
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
@ -5,6 +6,11 @@ import path from 'path'
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
environment: 'jsdom',
|
||||||
|
setupFiles: './src/test/setup.ts',
|
||||||
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 7999,
|
port: 7999,
|
||||||
proxy: {
|
proxy: {
|
||||||
|
|
@ -23,6 +29,9 @@ export default defineConfig({
|
||||||
'@': path.resolve(__dirname, './src'),
|
'@': path.resolve(__dirname, './src'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
optimizeDeps: {
|
||||||
|
include: ['react', 'react-dom'],
|
||||||
|
},
|
||||||
build: {
|
build: {
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
output: {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
/// <reference types="vitest" />
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
environment: 'jsdom',
|
||||||
|
setupFiles: './src/test/setup.ts',
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue