diff --git a/frontend/src/components/input/FormField.test.ts b/frontend/src/components/input/FormField.test.ts new file mode 100644 index 000000000..6d1fa8058 --- /dev/null +++ b/frontend/src/components/input/FormField.test.ts @@ -0,0 +1,141 @@ +import {describe, it, expect} from 'vitest' +import {mount} from '@vue/test-utils' +import FormField from './FormField.vue' + +describe('FormField', () => { + it('renders simple input', () => { + const wrapper = mount(FormField) + expect(wrapper.find('.field').exists()).toBe(true) + expect(wrapper.find('.control').exists()).toBe(true) + expect(wrapper.find('input.input').exists()).toBe(true) + }) + + it('supports v-model binding', async () => { + const wrapper = mount(FormField, { + props: { + modelValue: 'initial', + 'onUpdate:modelValue': (val: string) => wrapper.setProps({modelValue: val}), + }, + }) + const input = wrapper.find('input') + expect(input.element.value).toBe('initial') + + await input.setValue('updated') + expect(wrapper.props('modelValue')).toBe('updated') + }) + + it('renders label when provided', () => { + const wrapper = mount(FormField, { + props: {label: 'Username'}, + }) + const label = wrapper.find('label.label') + expect(label.exists()).toBe(true) + expect(label.text()).toBe('Username') + }) + + it('does not render label when not provided', () => { + const wrapper = mount(FormField) + expect(wrapper.find('label.label').exists()).toBe(false) + }) + + it('displays error message when provided', () => { + const wrapper = mount(FormField, { + props: {error: 'This field is required'}, + }) + const help = wrapper.find('.help.is-danger') + expect(help.exists()).toBe(true) + expect(help.text()).toBe('This field is required') + }) + + it('does not display error message when error is null', () => { + const wrapper = mount(FormField, { + props: {error: null}, + }) + expect(wrapper.find('.help.is-danger').exists()).toBe(false) + }) + + it('does not display error message when error is empty string', () => { + const wrapper = mount(FormField, { + props: {error: ''}, + }) + expect(wrapper.find('.help.is-danger').exists()).toBe(false) + }) + + it('renders addon slot when provided', () => { + const wrapper = mount(FormField, { + slots: { + addon: '', + }, + }) + expect(wrapper.find('.field.has-addons').exists()).toBe(true) + expect(wrapper.find('.control.is-expanded').exists()).toBe(true) + expect(wrapper.findAll('.control').length).toBe(2) + expect(wrapper.find('button').text()).toBe('Copy') + }) + + it('renders custom input via default slot', () => { + const wrapper = mount(FormField, { + props: {label: 'Custom'}, + slots: { + default: '', + }, + }) + expect(wrapper.find('input').exists()).toBe(false) + expect(wrapper.find('select').exists()).toBe(true) + }) + + it('passes attributes through to input', () => { + const wrapper = mount(FormField, { + attrs: { + type: 'email', + placeholder: 'Enter email', + disabled: true, + readonly: true, + autocomplete: 'email', + }, + }) + const input = wrapper.find('input') + expect(input.attributes('type')).toBe('email') + expect(input.attributes('placeholder')).toBe('Enter email') + expect(input.attributes('disabled')).toBe('') + expect(input.attributes('readonly')).toBe('') + expect(input.attributes('autocomplete')).toBe('email') + }) + + it('uses provided id for input', () => { + const wrapper = mount(FormField, { + props: {id: 'my-input', label: 'My Input'}, + }) + const input = wrapper.find('input') + const label = wrapper.find('label') + expect(input.attributes('id')).toBe('my-input') + expect(label.attributes('for')).toBe('my-input') + }) + + it('generates unique id when not provided', () => { + const wrapper = mount(FormField, { + props: {label: 'My Input'}, + }) + const input = wrapper.find('input') + const label = wrapper.find('label') + const inputId = input.attributes('id') + expect(inputId).toBeTruthy() + expect(label.attributes('for')).toBe(inputId) + }) + + it('links label to input via for attribute', () => { + const wrapper = mount(FormField, { + props: {label: 'Test Label'}, + }) + const label = wrapper.find('label') + const input = wrapper.find('input') + expect(label.attributes('for')).toBe(input.attributes('id')) + }) + + it('exposes input value for direct access', async () => { + const wrapper = mount(FormField) + const input = wrapper.find('input') + await input.setValue('test value') + expect(wrapper.vm.value).toBe('test value') + }) +}) diff --git a/frontend/src/components/input/FormField.vue b/frontend/src/components/input/FormField.vue new file mode 100644 index 000000000..fb7335928 --- /dev/null +++ b/frontend/src/components/input/FormField.vue @@ -0,0 +1,82 @@ + + +