import { describe, it, expect, vi } from 'vitest' // Mock vue lifecycle hooks since we're not in a component context vi.mock('vue', async () => { const actual = await vi.importActual('vue') return { ...actual, onMounted: vi.fn(), onUnmounted: vi.fn(), } }) import { evaluateCondition, useFormBuilder } from '../../composables/useFormBuilder' import type { FieldCondition, FormSchema, FormSectionSchema, FormStepSchema, } from '../../types/form-builder' // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function makeSection(fields: FormSectionSchema['fields'], id = 'section1'): FormSectionSchema { return { id, title: 'Section', fields } } function makeSchema(fields: FormSectionSchema['fields']): FormSchema { return { sections: [makeSection(fields)] } } // --------------------------------------------------------------------------- // evaluateCondition // --------------------------------------------------------------------------- describe('evaluateCondition', () => { describe('comparison operators', () => { it('eq: returns true when field equals value', () => { const condition: FieldCondition = { field: 'x', eq: 'hello' } expect(evaluateCondition(condition, { x: 'hello' })).toBe(true) }) it('eq: returns false when field does not equal value', () => { const condition: FieldCondition = { field: 'x', eq: 'hello' } expect(evaluateCondition(condition, { x: 'world' })).toBe(false) }) it('eq: performs strict equality (no coercion)', () => { const condition: FieldCondition = { field: 'x', eq: 1 } expect(evaluateCondition(condition, { x: '1' })).toBe(false) }) it('neq: returns true when field does not equal value', () => { const condition: FieldCondition = { field: 'x', neq: 'a' } expect(evaluateCondition(condition, { x: 'b' })).toBe(true) }) it('neq: returns false when field equals value', () => { const condition: FieldCondition = { field: 'x', neq: 'a' } expect(evaluateCondition(condition, { x: 'a' })).toBe(false) }) it('gt: returns true when number field is greater than threshold', () => { const condition: FieldCondition = { field: 'n', gt: 5 } expect(evaluateCondition(condition, { n: 6 })).toBe(true) }) it('gt: returns false when number field equals threshold', () => { const condition: FieldCondition = { field: 'n', gt: 5 } expect(evaluateCondition(condition, { n: 5 })).toBe(false) }) it('gt: returns false for non-numeric field', () => { const condition: FieldCondition = { field: 'n', gt: 5 } expect(evaluateCondition(condition, { n: 'six' })).toBe(false) }) it('lt: returns true when field is less than threshold', () => { const condition: FieldCondition = { field: 'n', lt: 10 } expect(evaluateCondition(condition, { n: 9 })).toBe(true) }) it('lt: returns false when field equals threshold', () => { const condition: FieldCondition = { field: 'n', lt: 10 } expect(evaluateCondition(condition, { n: 10 })).toBe(false) }) it('gte: returns true when field equals threshold', () => { const condition: FieldCondition = { field: 'n', gte: 5 } expect(evaluateCondition(condition, { n: 5 })).toBe(true) }) it('gte: returns true when field exceeds threshold', () => { const condition: FieldCondition = { field: 'n', gte: 5 } expect(evaluateCondition(condition, { n: 6 })).toBe(true) }) it('gte: returns false when field is below threshold', () => { const condition: FieldCondition = { field: 'n', gte: 5 } expect(evaluateCondition(condition, { n: 4 })).toBe(false) }) it('lte: returns true when field equals threshold', () => { const condition: FieldCondition = { field: 'n', lte: 5 } expect(evaluateCondition(condition, { n: 5 })).toBe(true) }) it('lte: returns false when field exceeds threshold', () => { const condition: FieldCondition = { field: 'n', lte: 5 } expect(evaluateCondition(condition, { n: 6 })).toBe(false) }) it('in: returns true when field value is in the list', () => { const condition: FieldCondition = { field: 'x', in: ['a', 'b', 'c'] } expect(evaluateCondition(condition, { x: 'b' })).toBe(true) }) it('in: returns false when field value is not in the list', () => { const condition: FieldCondition = { field: 'x', in: ['a', 'b', 'c'] } expect(evaluateCondition(condition, { x: 'd' })).toBe(false) }) it('notIn: returns true when field value is absent from the list', () => { const condition: FieldCondition = { field: 'x', notIn: ['a', 'b'] } expect(evaluateCondition(condition, { x: 'c' })).toBe(true) }) it('notIn: returns false when field value is in the list', () => { const condition: FieldCondition = { field: 'x', notIn: ['a', 'b'] } expect(evaluateCondition(condition, { x: 'a' })).toBe(false) }) it('truthy: returns true for truthy field value', () => { const condition: FieldCondition = { field: 'x', truthy: true } expect(evaluateCondition(condition, { x: 'non-empty' })).toBe(true) }) it('truthy: returns false for falsy field value', () => { const condition: FieldCondition = { field: 'x', truthy: true } expect(evaluateCondition(condition, { x: '' })).toBe(false) }) it('truthy: returns false when field is undefined', () => { const condition: FieldCondition = { field: 'x', truthy: true } expect(evaluateCondition(condition, {})).toBe(false) }) it('falsy: returns true for falsy field value', () => { const condition: FieldCondition = { field: 'x', falsy: true } expect(evaluateCondition(condition, { x: '' })).toBe(true) }) it('falsy: returns false for truthy field value', () => { const condition: FieldCondition = { field: 'x', falsy: true } expect(evaluateCondition(condition, { x: 'value' })).toBe(false) }) }) describe('contains operator', () => { it('returns true when string field contains substring', () => { const condition: FieldCondition = { field: 'x', contains: 'foo' } expect(evaluateCondition(condition, { x: 'foobar' })).toBe(true) }) it('returns false when string field does not contain substring', () => { const condition: FieldCondition = { field: 'x', contains: 'baz' } expect(evaluateCondition(condition, { x: 'foobar' })).toBe(false) }) it('returns true when array field contains the value', () => { const condition: FieldCondition = { field: 'arr', contains: 'b' } expect(evaluateCondition(condition, { arr: ['a', 'b', 'c'] })).toBe(true) }) it('returns false when array field does not contain the value', () => { const condition: FieldCondition = { field: 'arr', contains: 'z' } expect(evaluateCondition(condition, { arr: ['a', 'b', 'c'] })).toBe(false) }) it('returns false for non-string non-array field', () => { const condition: FieldCondition = { field: 'x', contains: 'foo' } expect(evaluateCondition(condition, { x: 42 })).toBe(false) }) }) describe('logical operators', () => { it('and: returns true when all sub-conditions pass', () => { const condition: FieldCondition = { and: [ { field: 'a', eq: 1 }, { field: 'b', eq: 2 }, ], } expect(evaluateCondition(condition, { a: 1, b: 2 })).toBe(true) }) it('and: returns false when any sub-condition fails', () => { const condition: FieldCondition = { and: [ { field: 'a', eq: 1 }, { field: 'b', eq: 99 }, ], } expect(evaluateCondition(condition, { a: 1, b: 2 })).toBe(false) }) it('and: returns true for empty sub-condition array', () => { const condition: FieldCondition = { and: [] } expect(evaluateCondition(condition, {})).toBe(true) }) it('or: returns true when any sub-condition passes', () => { const condition: FieldCondition = { or: [ { field: 'a', eq: 99 }, { field: 'b', eq: 2 }, ], } expect(evaluateCondition(condition, { a: 1, b: 2 })).toBe(true) }) it('or: returns false when all sub-conditions fail', () => { const condition: FieldCondition = { or: [ { field: 'a', eq: 99 }, { field: 'b', eq: 99 }, ], } expect(evaluateCondition(condition, { a: 1, b: 2 })).toBe(false) }) it('or: returns false for empty sub-condition array', () => { const condition: FieldCondition = { or: [] } expect(evaluateCondition(condition, {})).toBe(false) }) it('not: inverts a passing condition to false', () => { const condition: FieldCondition = { not: { field: 'x', eq: 'yes' } } expect(evaluateCondition(condition, { x: 'yes' })).toBe(false) }) it('not: inverts a failing condition to true', () => { const condition: FieldCondition = { not: { field: 'x', eq: 'yes' } } expect(evaluateCondition(condition, { x: 'no' })).toBe(true) }) it('supports nested and/or/not', () => { // (a == 1 AND b == 2) OR (NOT c == 3) const condition: FieldCondition = { or: [ { and: [{ field: 'a', eq: 1 }, { field: 'b', eq: 2 }] }, { not: { field: 'c', eq: 3 } }, ], } expect(evaluateCondition(condition, { a: 1, b: 2, c: 99 })).toBe(true) expect(evaluateCondition(condition, { a: 0, b: 2, c: 3 })).toBe(false) }) }) }) // --------------------------------------------------------------------------- // useFormBuilder - initial values // --------------------------------------------------------------------------- describe('useFormBuilder', () => { describe('initial values', () => { it('should use schema defaultValue when no initialData is provided', () => { const schema = makeSchema([ { name: 'name', label: 'Name', type: 'text', defaultValue: 'Alice' }, ]) const { form } = useFormBuilder(schema) expect(form.data.name).toBe('Alice') }) it('should prefer initialData over schema defaultValue', () => { const schema = makeSchema([ { name: 'name', label: 'Name', type: 'text', defaultValue: 'Alice' }, ]) const { form } = useFormBuilder(schema, { name: 'Bob' }) expect(form.data.name).toBe('Bob') }) it('should use getTypeDefault when no defaultValue and no initialData', () => { const schema = makeSchema([ { name: 'flag', label: 'Flag', type: 'checkbox' }, { name: 'tags', label: 'Tags', type: 'tags' }, { name: 'amount', label: 'Amount', type: 'number' }, { name: 'text', label: 'Text', type: 'text' }, ]) const { form } = useFormBuilder(schema) expect(form.data.flag).toBe(false) expect(form.data.tags).toEqual([]) expect(form.data.amount).toBeUndefined() expect(form.data.text).toBe('') }) it('should initialise with initialData value of 0 (falsy override)', () => { const schema = makeSchema([ { name: 'count', label: 'Count', type: 'number', defaultValue: 10 }, ]) const { form } = useFormBuilder(schema, { count: 0 }) expect(form.data.count).toBe(0) }) it('should initialise with initialData value of false (falsy override)', () => { const schema = makeSchema([ { name: 'enabled', label: 'Enabled', type: 'checkbox', defaultValue: true }, ]) const { form } = useFormBuilder(schema, { enabled: false }) expect(form.data.enabled).toBe(false) }) }) // --------------------------------------------------------------------------- // Field / section visibility // --------------------------------------------------------------------------- describe('isFieldVisible', () => { it('returns true when field has no condition', () => { const schema = makeSchema([{ name: 'name', label: 'Name', type: 'text' }]) const { isFieldVisible } = useFormBuilder(schema) expect(isFieldVisible('name')).toBe(true) }) it('returns false when field condition evaluates to false', () => { const schema = makeSchema([ { name: 'type', label: 'Type', type: 'text', defaultValue: 'basic' }, { name: 'extra', label: 'Extra', type: 'text', condition: { field: 'type', eq: 'advanced' }, }, ]) const { isFieldVisible } = useFormBuilder(schema) expect(isFieldVisible('extra')).toBe(false) }) it('returns true when field condition evaluates to true', () => { const schema = makeSchema([ { name: 'type', label: 'Type', type: 'text', defaultValue: 'advanced' }, { name: 'extra', label: 'Extra', type: 'text', condition: { field: 'type', eq: 'advanced' }, }, ]) const { isFieldVisible } = useFormBuilder(schema) expect(isFieldVisible('extra')).toBe(true) }) it('enhancement visible() overrides schema condition', () => { const schema = makeSchema([ { name: 'type', label: 'Type', type: 'text', defaultValue: 'basic' }, { name: 'extra', label: 'Extra', type: 'text', condition: { field: 'type', eq: 'advanced' }, }, ]) const { isFieldVisible } = useFormBuilder(schema, undefined, { fields: { extra: { visible: () => true }, }, }) // Schema condition would say false, but enhancement says true expect(isFieldVisible('extra')).toBe(true) }) it('returns false when unknown field name is queried (no schema match)', () => { const schema = makeSchema([{ name: 'name', label: 'Name', type: 'text' }]) const { isFieldVisible } = useFormBuilder(schema) // No field named 'ghost', no enhancement – falls through to return true // (function returns true as the default when field has no condition) expect(isFieldVisible('ghost')).toBe(true) }) }) describe('isSectionVisible', () => { it('returns true when section has no condition', () => { const schema: FormSchema = { sections: [ { id: 'sec1', title: 'Section 1', fields: [] }, ], } const { isSectionVisible } = useFormBuilder(schema) expect(isSectionVisible('sec1')).toBe(true) }) it('returns false when section condition evaluates to false', () => { const schema: FormSchema = { sections: [ { id: 'advanced', title: 'Advanced', condition: { field: 'mode', eq: 'expert' }, fields: [{ name: 'mode', label: 'Mode', type: 'text', defaultValue: 'simple' }], }, ], } const { isSectionVisible } = useFormBuilder(schema) expect(isSectionVisible('advanced')).toBe(false) }) it('returns true when section condition evaluates to true', () => { const schema: FormSchema = { sections: [ { id: 'advanced', title: 'Advanced', condition: { field: 'mode', eq: 'expert' }, fields: [{ name: 'mode', label: 'Mode', type: 'text', defaultValue: 'expert' }], }, ], } const { isSectionVisible } = useFormBuilder(schema) expect(isSectionVisible('advanced')).toBe(true) }) it('returns true for unknown section id', () => { const schema: FormSchema = { sections: [] } const { isSectionVisible } = useFormBuilder(schema) expect(isSectionVisible('non-existent')).toBe(true) }) }) // --------------------------------------------------------------------------- // Validation rules // --------------------------------------------------------------------------- describe('validate', () => { it('returns true when all required visible fields are filled', () => { const schema = makeSchema([ { name: 'name', label: 'Name', type: 'text', validation: { required: true } }, ]) const { validate } = useFormBuilder(schema, { name: 'Alice' }) expect(validate()).toBe(true) }) it('returns false when a required visible field is empty', () => { const schema = makeSchema([ { name: 'name', label: 'Name', type: 'text', validation: { required: true } }, ]) const { validate } = useFormBuilder(schema) expect(validate()).toBe(false) }) it('skips validation for hidden fields', () => { // 'extra' is required but hidden — validation should still pass const schema = makeSchema([ { name: 'type', label: 'Type', type: 'text', defaultValue: 'basic' }, { name: 'extra', label: 'Extra', type: 'text', validation: { required: true }, condition: { field: 'type', eq: 'advanced' }, }, ]) const { validate } = useFormBuilder(schema) expect(validate()).toBe(true) }) it('validates hidden-but-required field when it becomes visible', () => { const schema = makeSchema([ { name: 'type', label: 'Type', type: 'text', defaultValue: 'advanced' }, { name: 'extra', label: 'Extra', type: 'text', validation: { required: true }, condition: { field: 'type', eq: 'advanced' }, }, ]) const { validate } = useFormBuilder(schema) // 'extra' is visible and empty, so validation should fail expect(validate()).toBe(false) }) it('converts string pattern to RegExp', () => { const schema = makeSchema([ { name: 'code', label: 'Code', type: 'text', defaultValue: 'AB', validation: { pattern: '^[A-Z]{3}$' }, }, ]) const { validate, form } = useFormBuilder(schema) // 'AB' does not match ^[A-Z]{3}$ expect(validate()).toBe(false) form.data.code = 'ABC' expect(validate()).toBe(true) }) it('converts object pattern to RegExp with custom message', () => { const schema = makeSchema([ { name: 'code', label: 'Code', type: 'text', defaultValue: 'bad', validation: { pattern: { value: '^[A-Z]{3}$', message: 'Must be 3 uppercase letters' } }, }, ]) const { validate, form } = useFormBuilder(schema) expect(validate()).toBe(false) expect(form.errors.code).toBe('Must be 3 uppercase letters') }) it('applies enhancement custom validator', () => { const schema = makeSchema([ { name: 'age', label: 'Age', type: 'number', defaultValue: 15 }, ]) const { validate, form } = useFormBuilder(schema, undefined, { fields: { age: { validate: (value) => (typeof value === 'number' && value < 18 ? 'Must be 18+' : undefined), }, }, }) expect(validate()).toBe(false) expect(form.errors.age).toBe('Must be 18+') }) }) // --------------------------------------------------------------------------- // Submit // --------------------------------------------------------------------------- describe('submit', () => { it('calls onSubmit with visible field data only', async () => { const onSubmit = vi.fn() const schema = makeSchema([ { name: 'type', label: 'Type', type: 'text', defaultValue: 'basic' }, { name: 'extra', label: 'Extra', type: 'text', defaultValue: 'secret', condition: { field: 'type', eq: 'advanced' }, }, ]) const { submit } = useFormBuilder(schema, undefined, { onSubmit }) await submit() expect(onSubmit).toHaveBeenCalledOnce() const submittedData = onSubmit.mock.calls[0][0] as Record expect(submittedData.type).toBe('basic') // 'extra' is hidden (condition not met) — must not appear in submission expect('extra' in submittedData).toBe(false) }) it('does not call onSubmit when validation fails', async () => { const onSubmit = vi.fn() const schema = makeSchema([ { name: 'name', label: 'Name', type: 'text', validation: { required: true } }, ]) const { submit } = useFormBuilder(schema, undefined, { onSubmit }) await submit() expect(onSubmit).not.toHaveBeenCalled() }) it('applies transform before passing data to onSubmit', async () => { const onSubmit = vi.fn() const schema = makeSchema([ { name: 'name', label: 'Name', type: 'text', defaultValue: 'alice' }, ]) const { submit } = useFormBuilder(schema, undefined, { transform: (data) => ({ ...data, name: (data.name as string).toUpperCase() }), onSubmit, }) await submit() const submittedData = onSubmit.mock.calls[0][0] as Record expect(submittedData.name).toBe('ALICE') }) it('sets isSubmitting during onSubmit execution', async () => { let observedSubmitting = false const schema = makeSchema([{ name: 'n', label: 'N', type: 'text', defaultValue: 'x' }]) const { submit, form } = useFormBuilder(schema, undefined, { onSubmit: async () => { observedSubmitting = form.isSubmitting.value }, }) await submit() expect(observedSubmitting).toBe(true) expect(form.isSubmitting.value).toBe(false) }) it('clears isSubmitting even when onSubmit throws', async () => { const schema = makeSchema([{ name: 'n', label: 'N', type: 'text', defaultValue: 'x' }]) const { submit, form } = useFormBuilder(schema, undefined, { onSubmit: async () => { throw new Error('submit failed') }, }) await expect(submit()).rejects.toThrow('submit failed') expect(form.isSubmitting.value).toBe(false) }) it('does nothing when onSubmit is not provided and validation passes', async () => { const schema = makeSchema([{ name: 'n', label: 'N', type: 'text', defaultValue: 'x' }]) const { submit } = useFormBuilder(schema) // Should not throw await expect(submit()).resolves.toBeUndefined() }) }) // --------------------------------------------------------------------------- // Wizard step navigation // --------------------------------------------------------------------------- describe('wizard step navigation', () => { function makeWizardSchema( step1Fields: FormSectionSchema['fields'], step2Fields: FormSectionSchema['fields'], ): FormSchema { const steps: FormStepSchema[] = [ { id: 'step1', label: 'Step 1', sections: [{ id: 'sec1', title: 'Sec 1', fields: step1Fields }], }, { id: 'step2', label: 'Step 2', sections: [{ id: 'sec2', title: 'Sec 2', fields: step2Fields }], }, ] return { steps } } it('starts at step 0', () => { const schema = makeWizardSchema([], []) const { currentStep } = useFormBuilder(schema) expect(currentStep.value).toBe(0) }) it('goNext advances to the next step when current step is valid', () => { const schema = makeWizardSchema( [{ name: 'name', label: 'Name', type: 'text', defaultValue: 'Alice' }], [], ) const { goNext, currentStep } = useFormBuilder(schema) const result = goNext() expect(result).toBe(true) expect(currentStep.value).toBe(1) }) it('goNext does not advance when a required field on the current step is empty', () => { const schema = makeWizardSchema( [{ name: 'name', label: 'Name', type: 'text', validation: { required: true } }], [], ) const { goNext, currentStep } = useFormBuilder(schema) const result = goNext() expect(result).toBe(false) expect(currentStep.value).toBe(0) }) it('goNext does not go beyond the last step', () => { const schema = makeWizardSchema([], []) const { goNext, currentStep } = useFormBuilder(schema) goNext() // -> step 1 goNext() // -> should not go to step 2 (out of bounds) expect(currentStep.value).toBe(1) }) it('goBack decrements the step', () => { const schema = makeWizardSchema([], []) const { goNext, goBack, currentStep } = useFormBuilder(schema) goNext() expect(currentStep.value).toBe(1) goBack() expect(currentStep.value).toBe(0) }) it('goBack does nothing when already on the first step', () => { const schema = makeWizardSchema([], []) const { goBack, currentStep } = useFormBuilder(schema) goBack() expect(currentStep.value).toBe(0) }) it('goToStep jumps to the specified index', () => { const schema: FormSchema = { steps: [ { id: 's1', label: 'S1', sections: [{ id: 'sec1', title: 'S1', fields: [] }] }, { id: 's2', label: 'S2', sections: [{ id: 'sec2', title: 'S2', fields: [] }] }, { id: 's3', label: 'S3', sections: [{ id: 'sec3', title: 'S3', fields: [] }] }, ], } const { goToStep, currentStep } = useFormBuilder(schema) goToStep(2) expect(currentStep.value).toBe(2) }) it('goToStep ignores out-of-range negative index', () => { const schema = makeWizardSchema([], []) const { goToStep, currentStep } = useFormBuilder(schema) goToStep(-1) expect(currentStep.value).toBe(0) }) it('goToStep ignores out-of-range positive index', () => { const schema = makeWizardSchema([], []) const { goToStep, currentStep } = useFormBuilder(schema) goToStep(99) expect(currentStep.value).toBe(0) }) it('goToStep is a no-op on a flat (non-wizard) schema', () => { const schema = makeSchema([]) const { goToStep, currentStep } = useFormBuilder(schema) goToStep(1) expect(currentStep.value).toBe(0) }) it('goNext returns false for flat (non-wizard) schema', () => { const schema = makeSchema([]) const { goNext } = useFormBuilder(schema) expect(goNext()).toBe(false) }) it('goNext skips hidden fields when validating the current step', () => { const schema = makeWizardSchema( [ { name: 'type', label: 'Type', type: 'text', defaultValue: 'basic' }, { name: 'extra', label: 'Extra', type: 'text', validation: { required: true }, condition: { field: 'type', eq: 'advanced' }, }, ], [], ) const { goNext, currentStep } = useFormBuilder(schema) // 'extra' is required but its condition is not met (type=basic) → should still pass expect(goNext()).toBe(true) expect(currentStep.value).toBe(1) }) it('reset resets currentStep to 0', () => { const schema = makeWizardSchema([], []) const { goNext, reset, currentStep } = useFormBuilder(schema) goNext() expect(currentStep.value).toBe(1) reset() expect(currentStep.value).toBe(0) }) }) // --------------------------------------------------------------------------- // isCurrentStepValid // --------------------------------------------------------------------------- describe('isCurrentStepValid', () => { it('returns true when all visible step fields pass validation', () => { const schema: FormSchema = { steps: [ { id: 's1', label: 'S1', sections: [ { id: 'sec1', title: 'S1', fields: [ { name: 'name', label: 'Name', type: 'text', defaultValue: 'Alice', validation: { required: true } }, ], }, ], }, ], } const { isCurrentStepValid } = useFormBuilder(schema) expect(isCurrentStepValid.value).toBe(true) }) it('returns false when a required visible field is empty on current step', () => { const schema: FormSchema = { steps: [ { id: 's1', label: 'S1', sections: [ { id: 'sec1', title: 'S1', fields: [ { name: 'name', label: 'Name', type: 'text', validation: { required: true } }, ], }, ], }, ], } const { isCurrentStepValid } = useFormBuilder(schema) expect(isCurrentStepValid.value).toBe(false) }) it('falls back to form.isValid for flat schemas', () => { const schema = makeSchema([ { name: 'name', label: 'Name', type: 'text', defaultValue: 'Alice', validation: { required: true } }, ]) const { isCurrentStepValid } = useFormBuilder(schema) expect(isCurrentStepValid.value).toBe(true) }) }) // --------------------------------------------------------------------------- // getFieldOptions // --------------------------------------------------------------------------- describe('getFieldOptions', () => { it('returns options from field props when defined in schema', () => { const options = [ { label: 'Option A', value: 'a' }, { label: 'Option B', value: 'b' }, ] const schema = makeSchema([ { name: 'choice', label: 'Choice', type: 'select', props: { options } }, ]) const { getFieldOptions } = useFormBuilder(schema) expect(getFieldOptions('choice')).toEqual(options) }) it('returns undefined when field has no options', () => { const schema = makeSchema([ { name: 'name', label: 'Name', type: 'text' }, ]) const { getFieldOptions } = useFormBuilder(schema) expect(getFieldOptions('name')).toBeUndefined() }) it('returns dynamic options from enhancement over schema props', () => { const schemaOptions = [{ label: 'Static', value: 's' }] const dynamicOptions = [{ label: 'Dynamic', value: 'd' }] const schema = makeSchema([ { name: 'choice', label: 'Choice', type: 'select', props: { options: schemaOptions } }, ]) const { getFieldOptions } = useFormBuilder(schema, undefined, { fields: { choice: { options: () => dynamicOptions }, }, }) expect(getFieldOptions('choice')).toEqual(dynamicOptions) }) it('accepts shorthand dynamic options from enhancements', () => { const dynamicOptions = ['linear', 'logistic'] const schema = makeSchema([ { name: 'choice', label: 'Choice', type: 'select' }, ]) const { getFieldOptions, getResolvedFieldProps, fields } = useFormBuilder(schema, undefined, { fields: { choice: { options: () => dynamicOptions }, }, }) expect(getFieldOptions('choice')).toEqual(dynamicOptions) expect(getResolvedFieldProps(fields[0]).options).toEqual(dynamicOptions) }) }) // --------------------------------------------------------------------------- // fields list // --------------------------------------------------------------------------- describe('fields', () => { it('returns all fields from a flat schema', () => { const schema: FormSchema = { sections: [ { id: 'sec1', title: 'S1', fields: [{ name: 'a', label: 'A', type: 'text' }] }, { id: 'sec2', title: 'S2', fields: [{ name: 'b', label: 'B', type: 'number' }] }, ], } const { fields } = useFormBuilder(schema) expect(fields.map((f) => f.name)).toEqual(['a', 'b']) }) it('returns all fields flattened across wizard steps', () => { const schema: FormSchema = { steps: [ { id: 's1', label: 'S1', sections: [{ id: 'sec1', title: 'S1', fields: [{ name: 'a', label: 'A', type: 'text' }] }], }, { id: 's2', label: 'S2', sections: [{ id: 'sec2', title: 'S2', fields: [{ name: 'b', label: 'B', type: 'number' }] }], }, ], } const { fields } = useFormBuilder(schema) expect(fields.map((f) => f.name)).toEqual(['a', 'b']) }) }) // --------------------------------------------------------------------------- // reset // --------------------------------------------------------------------------- describe('reset', () => { it('resets form data to initial values', () => { const schema = makeSchema([ { name: 'name', label: 'Name', type: 'text', defaultValue: 'Alice' }, ]) const { form, reset } = useFormBuilder(schema) form.data.name = 'Bob' reset() expect(form.data.name).toBe('Alice') }) it('accepts override values on reset', () => { const schema = makeSchema([ { name: 'name', label: 'Name', type: 'text', defaultValue: 'Alice' }, ]) const { form, reset } = useFormBuilder(schema) reset({ name: 'Charlie' }) expect(form.data.name).toBe('Charlie') }) }) // --------------------------------------------------------------------------- // updateSchema // --------------------------------------------------------------------------- describe('updateSchema', () => { it('rebuilds fields, values, and validation rules for a replacement schema', () => { const initialSchema = makeSchema([ { name: 'name', label: 'Name', type: 'text', defaultValue: 'Alice' }, { name: 'legacy', label: 'Legacy', type: 'text', defaultValue: 'remove' }, ]) const nextSchema = makeSchema([ { name: 'name', label: 'Name', type: 'text', defaultValue: 'Alice', validation: { required: true }, }, { name: 'count', label: 'Count', type: 'number', defaultValue: 2, validation: { min: 3 } }, ]) const builder = useFormBuilder>(initialSchema) builder.form.data.name = 'Edited' builder.form.setFieldTouched('name') builder.updateSchema(nextSchema, { name: 'Loaded', count: 4 }) expect(builder.fields.map((field) => field.name)).toEqual(['name', 'count']) expect(builder.form.data.name).toBe('Loaded') expect(builder.form.data.count).toBe(4) expect(builder.form.data.legacy).toBeUndefined() expect(builder.form.touched.name).toBe(false) expect(builder.form.errors.name).toBeNull() expect(builder.validate()).toBe(true) builder.form.data.count = 1 expect(builder.validate()).toBe(false) expect(builder.form.errors.count).toBe('Must be at least 3') }) it('resets wizard position when the schema changes', () => { const stepSchema: FormSchema = { steps: [ { id: 'one', label: 'One', sections: [makeSection([{ name: 'a', label: 'A', type: 'text' }])] }, { id: 'two', label: 'Two', sections: [makeSection([{ name: 'b', label: 'B', type: 'text' }])] }, ], } const nextSchema = makeSchema([ { name: 'name', label: 'Name', type: 'text', defaultValue: 'Alice' }, ]) const builder = useFormBuilder(stepSchema) expect(builder.goNext()).toBe(true) expect(builder.currentStep.value).toBe(1) builder.updateSchema(nextSchema) expect(builder.currentStep.value).toBe(0) expect(builder.fields.map((field) => field.name)).toEqual(['name']) expect(builder.form.data.name).toBe('Alice') }) }) })