UNPKG

@braid/vue-formulate

Version:

The easiest way to build forms in Vue.

1,168 lines (1,087 loc) 41 kB
import Vue from 'vue' import flushPromises from 'flush-promises' import { mount, createLocalVue } from '@vue/test-utils' import Formulate from '@/Formulate.js' import FormulateForm from '@/FormulateForm.vue' import FormulateInput from '@/FormulateInput.vue' import FormulateInputBox from '@/inputs/FormulateInputBox.vue' import { classKeys } from '@/libs/classes' const globalRule = jest.fn((context) => { return false }) const options = { locales: { en: { email: ({ value }) => `Super invalid email: ${value}` } }, rules: { globalRule }, library: { special: { classification: 'box', component: 'FormulateInputBox' } }, useInputDecorators: true, validationNameStrategy: false } Vue.use(Formulate, options) const resetInstance = () => { Formulate.install(Vue, options) } describe('FormulateInput', () => { it('sets unknown classification if not in library', () => { const wrapper = mount(FormulateInput, { propsData: { type: 'foobar', } }) expect(wrapper.vm.classification).toBe('unknown') }) it('uses a hard-coded fallback validation error if no default rules exist', async () => { const localVue = createLocalVue() localVue.use(Formulate, { rules: { foobar: () => false }, locales: { en: 'xyz' } }) const wrapper = mount(FormulateInput, { localVue, propsData: { validation: 'foobar', errorBehavior: 'live' }}) await flushPromises() expect(wrapper.find('.formulate-input-error').text()).toBe('Invalid field value') resetInstance() }) it('allows custom field-rule level validation strings', async () => { const wrapper = mount(FormulateInput, { propsData: { type: 'text', validation: 'required|in:abcdef', validationMessages: {in: 'the value was different than expected'}, errorBehavior: 'live', value: 'other value' } }) await flushPromises() expect(wrapper.find('.formulate-input-error').text()).toBe('the value was different than expected') }) it('allows custom field-rule level validation functions', async () => { const wrapper = mount(FormulateInput, { propsData: { type: 'text', validation: 'required|in:abcdef', validationMessages: { in: ({ value }) => `The string ${value} is not correct.` }, errorBehavior: 'live', value: 'other value' } }) await flushPromises() expect(wrapper.find('.formulate-input-error').text()).toBe('The string other value is not correct.') }) it('uses globally overridden validation messages', async () => { const wrapper = mount(FormulateInput, { propsData: { type: 'text', validation: 'required|email', errorBehavior: 'live', value: 'wrong email' } }) await flushPromises() expect(wrapper.find('.formulate-input-error').text()).toBe('Super invalid email: wrong email') }) it('uses custom async validation rules on defined on the field', async () => { const wrapper = mount(FormulateInput, { propsData: { type: 'text', validation: 'required|foobar', validationMessages: { foobar: 'failed the foobar check' }, validationRules: { foobar: async ({ value }) => value === 'foo' }, errorBehavior: 'live', value: 'bar' } }) await flushPromises() expect(wrapper.find('.formulate-input-error').text()).toBe('failed the foobar check') }) it('uses custom sync validation rules on defined on the field', async () => { const wrapper = mount(FormulateInput, { propsData: { type: 'text', validation: 'required|foobar', validationMessages: { foobar: 'failed the foobar check' }, validationRules: { foobar: ({ value }) => value === 'foo' }, errorBehavior: 'live', value: 'bar' } }) await flushPromises() expect(wrapper.find('.formulate-input-error').text()).toBe('failed the foobar check') }) it('uses global custom validation rules', async () => { const wrapper = mount(FormulateInput, { propsData: { type: 'text', validation: 'required|globalRule', errorBehavior: 'live', value: 'bar' } }) await flushPromises() expect(globalRule.mock.calls.length).toBe(1) }) it('skips validation when the optional rule exists fields', async () => { const wrapper = mount(FormulateInput, { propsData: { type: 'text', validation: 'optional|min:6,length', errorBehavior: 'live' }}) await flushPromises() expect(wrapper.vm.context.validationErrors.length).toBe(0) }) it('skips validation when the optional rule is at the stack end', async () => { const wrapper = mount(FormulateInput, { propsData: { type: 'text', validation: 'min:6,length|optional', errorBehavior: 'live' }}) await flushPromises() expect(wrapper.vm.context.validationErrors.length).toBe(0) }) it('ignores modifiers on the optional rule', async () => { const wrapper = mount(FormulateInput, { propsData: { type: 'text', validation: '^optional|min:6,length|max:10', errorBehavior: 'live' }}) await flushPromises() expect(wrapper.vm.context.validationErrors.length).toBe(0) }) it('skips validation when the optional rule is used with required', async () => { const wrapper = mount(FormulateInput, { propsData: { type: 'text', validation: 'optional|required|min:6,length', errorBehavior: 'live' }}) await flushPromises() expect(wrapper.vm.context.validationErrors.length).toBe(0) }) it('skips validation when the optional rule is used in array format', async () => { const wrapper = mount(FormulateInput, { propsData: { type: 'text', validation: [ ['optional'], ['required'], ['min', '6', 'length'], ], errorBehavior: 'live' }}) await flushPromises() expect(wrapper.vm.context.validationErrors.length).toBe(0) }) it('remaining validation rules after passing the optional rule', async () => { const wrapper = mount(FormulateInput, { propsData: { type: 'text', validation: 'optional|min:6,length|max:10', errorBehavior: 'live' }}) wrapper.find('input').setValue('hi') await flushPromises() expect(wrapper.vm.context.validationErrors.length).toBe(1) }) it('can extend its standard library of inputs', async () => { const wrapper = mount(FormulateInput, { propsData: { type: 'special', validation: 'required', errorBehavior: 'live', value: 'bar' } }) await flushPromises() expect(wrapper.findComponent(FormulateInputBox).exists()).toBe(true) }) it('emits correct validation event', async () => { const wrapper = mount(FormulateInput, { propsData: { type: 'text', validation: 'required', errorBehavior: 'live', value: '', name: 'testinput', } }) await flushPromises() const errorObject = wrapper.emitted('validation')[0][0] expect(errorObject).toEqual({ name: 'testinput', errors: [ expect.any(String) ], hasErrors: true }) }) it('emits a error-visibility event on blur', async () => { const wrapper = mount(FormulateInput, { propsData: { type: 'text', validation: 'required', errorBehavior: 'blur', value: '', name: 'testinput', } }) await flushPromises() expect(wrapper.emitted('error-visibility')[0][0]).toBe(false) wrapper.find('input[type="text"]').trigger('blur') await flushPromises() expect(wrapper.emitted('error-visibility')[1][0]).toBe(true) }) it('emits error-visibility event immediately when live', async () => { const wrapper = mount(FormulateInput, { propsData: { type: 'text', validation: 'required', errorBehavior: 'live', value: '', name: 'testinput', } }) await flushPromises() expect(wrapper.emitted('error-visibility').length).toBe(1) }) it('Does not emit an error-visibility event if visibility did not change', async () => { const wrapper = mount(FormulateInput, { propsData: { type: 'text', validation: 'in:xyz', errorBehavior: 'live', value: 'bar', name: 'testinput', } }) await flushPromises() expect(wrapper.emitted('error-visibility').length).toBe(1) wrapper.find('input[type="text"]').setValue('bar') await flushPromises() expect(wrapper.emitted('error-visibility').length).toBe(1) }) it('allows overriding the label default slot component', async () => { const localVue = createLocalVue() localVue.component('CustomLabel', { render: function (h) { return h('div', { class: 'custom-label' }, [`custom: ${this.context.label}`]) }, props: ['context'] }) localVue.use(Formulate, { slotComponents: { label: 'CustomLabel' } }) const wrapper = mount(FormulateInput, { localVue, propsData: { label: 'My label here' } }) expect(wrapper.find('.custom-label').html()).toBe('<div class="custom-label">custom: My label here</div>') }) it('allows overriding the help default slot component', async () => { const localVue = createLocalVue() localVue.component('CustomHelp', { render: function (h) { return h('small', { class: 'custom-help' }, [`custom: ${this.context.help}`]) }, props: ['context'] }) localVue.use(Formulate, { slotComponents: { help: 'CustomHelp' } }) const wrapper = mount(FormulateInput, { localVue, propsData: { help: 'My help here' } }) expect(wrapper.find('.custom-help').html()).toBe('<small class="custom-help">custom: My help here</small>') }) it('allows overriding the errors component', async () => { const localVue = createLocalVue() localVue.component('CustomErrors', { render: function (h) { return h('ul', { class: 'my-errors' }, this.context.visibleValidationErrors.map(message => h('li', message))) }, props: ['context'] }) localVue.use(Formulate, { slotComponents: { errors: 'CustomErrors' } }) const wrapper = mount(FormulateInput, { localVue, propsData: { help: 'My help here', errorBehavior: 'live', validation: 'required' } }) await flushPromises() expect(wrapper.find('.my-errors').html()) .toBe(`<ul class="my-errors">\n <li>Text is required.</li>\n</ul>`) // Clean up after this call — we should probably get rid of the singleton all together.... Formulate.extend({ slotComponents: { errors: 'FormulateErrors' }}) }) it('links help text with `aria-describedby`', async () => { const wrapper = mount(FormulateInput, { propsData: { type: 'text', validation: 'required', errorBehavior: 'live', value: 'bar', help: 'Some help text' } }) await flushPromises() const id = `${wrapper.vm.context.id}-help` expect(wrapper.find('input').attributes('aria-describedby')).toBe(id) expect(wrapper.find('.formulate-input-help').attributes().id).toBe(id) }); it('it does not use aria-describedby if there is no help text', async () => { const wrapper = mount(FormulateInput, { propsData: { type: 'text', validation: 'required', errorBehavior: 'live', value: 'bar', } }) await flushPromises() expect(wrapper.find('input').attributes('aria-describedby')).toBeFalsy() }); it('it allows overriding aria-describedby', async () => { const wrapper = mount(FormulateInput, { propsData: { type: 'text', validation: 'required', errorBehavior: 'live', value: 'bar', help: 'abc', 'aria-describedby': 'other-id' } }) await flushPromises() expect(wrapper.find('input').attributes('aria-describedby')).toBe('other-id') }); it('can bail on validation when encountering the bail rule', async () => { const wrapper = mount(FormulateInput, { propsData: { type: 'text', validation: 'bail|required|in:xyz', errorBehavior: 'live' } }) await flushPromises(); expect(wrapper.vm.context.visibleValidationErrors.length).toBe(1); }) it('continues to bail after content was entered', async () => { const wrapper = mount(FormulateInput, { propsData: { type: 'text', name: 'letters', validation: 'bail|required|in:xyz', errorBehavior: 'live' } }) await flushPromises(); wrapper.find('input').setValue('xyz') await flushPromises() expect(wrapper.findAll('.formulate-input-errors li').length).toBe(0); wrapper.find('input').setValue('') await flushPromises() expect(wrapper.findAll('.formulate-input-errors li').length).toBe(1); expect(wrapper.find('.formulate-input-errors li').text()).toBe('Letters is required.'); }) it('can show multiple validation errors if they occur before the bail rule', async () => { const wrapper = mount(FormulateInput, { propsData: { type: 'text', validation: 'required|in:xyz|bail', errorBehavior: 'live' } }) await flushPromises(); expect(wrapper.vm.context.visibleValidationErrors.length).toBe(2); }) it('can avoid bail behavior by using modifier', async () => { const wrapper = mount(FormulateInput, { propsData: { type: 'text', validation: '^required|in:xyz|min:10,length', errorBehavior: 'live', value: '123' } }) await flushPromises(); expect(wrapper.vm.context.visibleValidationErrors.length).toBe(2); }) it('prevents later error messages when modified rule fails', async () => { const wrapper = mount(FormulateInput, { propsData: { type: 'text', validation: '^required|in:xyz|min:10,length', errorBehavior: 'live' } }) await flushPromises(); expect(wrapper.vm.context.visibleValidationErrors.length).toBe(1); }) it('can bail in the middle of the rule set with a modifier', async () => { const wrapper = mount(FormulateInput, { propsData: { type: 'text', validation: 'required|^in:xyz|min:10,length', errorBehavior: 'live' } }) await flushPromises(); expect(wrapper.vm.context.visibleValidationErrors.length).toBe(2); }) it('does not show errors on blur when set error-behavior is submit', async () => { const wrapper = mount(FormulateInput, { propsData: { type: 'text', validation: 'required', errorBehavior: 'submit', } }) wrapper.find('input').trigger('input') wrapper.find('input').trigger('blur') await flushPromises() expect(wrapper.find('.formulate-input-errors').exists()).toBe(false) }) it('does not show errors initially when error-behavior is value', async () => { const wrapper = mount(FormulateInput, { propsData: { type: 'text', validation: 'required', errorBehavior: 'value', } }) wrapper.find('input').trigger('input') await flushPromises() expect(wrapper.find('.formulate-input-errors').exists()).toBeFalsy() }) it('shows errors when error-behavior is value and an input is blurred', async () => { const wrapper = mount(FormulateInput, { propsData: { type: 'text', validation: 'required', errorBehavior: 'value', } }) wrapper.find('input').trigger('blur') await flushPromises() expect(wrapper.find('.formulate-input-errors').exists()).toBeTruthy() }) it('shows errors initially when error-behavior is value and it has a value', async () => { const wrapper = mount(FormulateInput, { propsData: { type: 'text', validation: 'required|email', errorBehavior: 'value', value: 'test' } }) wrapper.find('input').setValue('Added some text') await flushPromises() expect(wrapper.vm.touched).toBe(true) expect(wrapper.find('.formulate-input-errors').exists()).toBeTruthy() }) it('hides errors initially, and then shows them after first touch when error-behavior is value', async () => { const wrapper = mount(FormulateInput, { propsData: { type: 'text', validation: 'required|email', errorBehavior: 'value' } }) await flushPromises() expect(wrapper.find('.formulate-input-errors').exists()).toBeFalsy() wrapper.find('input').setValue('Added some text') await flushPromises() expect(wrapper.find('.formulate-input-errors').exists()).toBeTruthy() }) it('displays errors when error-behavior is submit and form is submitted', async () => { const wrapper = mount(FormulateForm, { slots: { default: `<FormulateInput error-behavior="submit" validation="required" />` } }) wrapper.trigger('submit') await flushPromises() expect(wrapper.find('.formulate-input-errors').exists()).toBe(true) }) it('passes $emit to rootEmit inside the context object', async () => { const wrapper = mount(FormulateInput, { propsData: { type: 'text' } }) wrapper.vm.context.rootEmit('foo', 'bar') await flushPromises() expect(wrapper.emitted().foo[0]).toEqual(['bar']) }) it('allows getFormValues inside of custom validation messages', async () => { const wrapper = mount({ template: ` <FormulateForm> <FormulateInput type="text" name="first" /> <FormulateInput type="text" name="last" /> <FormulateInput type="text" name="name" validation="fullName" :validation-rules="{ fullName }" :validation-messages="{ fullName: fullNameMessage }" error-behavior="live" /> </FormulateForm> `, methods: { fullName ({ value, getFormValues }) { const values = getFormValues() return `${values.first} ${values.last}` === value }, fullNameMessage ({ value, formValues }) { return `${formValues.first} ${formValues.last} does not equal ${value}.` } } }) await flushPromises() const inputs = wrapper.findAll('input[type="text"]') inputs.at(0).setValue('jon') inputs.at(1).setValue('baley') inputs.at(2).setValue('jon baley') await flushPromises() expect(wrapper.find('.formulate-input-errors').exists()).toBe(false) inputs.at(1).setValue('parker') await flushPromises() expect(wrapper.find('.formulate-input-errors').exists()).toBe(true) expect(wrapper.find('.formulate-input-errors li').text()).toBe('jon parker does not equal jon baley.') }) it('emits an input event a single time on change', async () => { const wrapper = mount(FormulateInput, { propsData: { type: 'text' }}) await flushPromises() wrapper.find('input').setValue('a') await flushPromises() expect(wrapper.emitted().input.length).toBe(1); }) it('allows you to replace classes on the outer element', () => { const wrapper = mount({ template: `<FormulateInput type='text' class='my-custom-class' />` }) expect(wrapper.attributes('class')).toBe('my-custom-class formulate-input') }) it('has formulate-input outer classes by default', () => { const wrapper = mount(FormulateInput, { propsData: { type: 'text' }}) expect(wrapper.attributes('class')).toBe('formulate-input'); expect(wrapper.find('.formulate-input > *').attributes('class')).toBe('formulate-input-wrapper') }) it('has formulate-input-label wrapper classes by default', () => { const wrapper = mount(FormulateInput, { propsData: { type: 'text', label: 'blah' }}) expect(wrapper.find('label').attributes('class')).toBe('formulate-input-label formulate-input-label--before'); }) it('can override the baseClasses function globally', () => { const localVue = createLocalVue() localVue.use(Formulate, { baseClasses () { return classKeys.reduce((classMap, key) => Object.assign(classMap, { [key]: 'my-class' }), {}) } }) const wrapper = mount(FormulateInput, { localVue, propsData: { type: 'text', label: 'blah' }}) expect(wrapper.find('label').attributes('class')).toBe('my-class'); }) it('can override individual classKey globals', () => { const localVue = createLocalVue() localVue.use(Formulate, { classes: { label: 'label-class' } }) const wrapper = mount(FormulateInput, { localVue, propsData: { type: 'text', label: 'blah' }}) expect(wrapper.attributes('class')).toBe('formulate-input'); expect(wrapper.find('label').attributes('class')).toBe('label-class'); }) it('can override individual classKey globals with functions', async () => { const localVue = createLocalVue() localVue.use(Formulate, { classes: { outer: (c, d) => d.concat(['adds-1-class']), wrapper: (c, d) => d.concat(['adds-2-class']), label: (c, d) => d.concat(['adds-3-class']), element: (c, d) => d.concat(['adds-4-class']), input: (c, d) => d.concat(['adds-5-class']), help: (c, d) => d.concat(['adds-6-class']), errors: (c, d) => d.concat(['adds-7-class']), error: (c, d) => d.concat(['adds-8-class']) } }) const wrapper = mount(FormulateInput, { localVue, propsData: { type: 'text', label: 'foo', help: 'bar', errorBehavior: 'live', validation: 'required' }}) await flushPromises() expect(wrapper.attributes('class')).toBe('formulate-input adds-1-class'); // Test the wrapper override expect(wrapper.find('.formulate-input-wrapper').attributes('class')) .toBe('formulate-input-wrapper adds-2-class'); // Test the label override expect(wrapper.find('label').attributes('class')) .toBe('formulate-input-label formulate-input-label--before adds-3-class'); // Test the element override expect(wrapper.find('.formulate-input-element').attributes('class')) .toBe('formulate-input-element formulate-input-element--text adds-4-class'); // Test the input override expect(wrapper.find('input').attributes('class')) .toBe('adds-5-class'); // Test the input override expect(wrapper.find('.formulate-input-help').attributes('class')) .toBe('formulate-input-help formulate-input-help--after adds-6-class'); // Test the errors wrapper expect(wrapper.find('.formulate-input-errors').attributes('class')) .toBe('formulate-input-errors adds-7-class'); // Test the inner error expect(wrapper.find('.formulate-input-error').attributes('class')) .toBe('formulate-input-error adds-8-class'); }) it('allows you to fully override the class on the label element', () => { // We have to do this because the previous tests messed with the singleton object — ideally we should const localVue = createLocalVue() localVue.use(Formulate, {classes: {}}) const wrapper = mount(FormulateInput, { propsData: { type: 'text', label: 'foobar', labelClass: 'my-custom-class' }}) expect(wrapper.find('label').attributes('class')).toBe('my-custom-class') }) it('allows you to modify all class keys via props', async () => { const wrapper = mount(FormulateInput, { propsData: { type: 'text', label: 'foo', help: 'bar', errorBehavior: 'live', validation: 'required', outerClass: ['custom-1-class'], wrapperClass: ['custom-2-class'], labelClass: ['custom-3-class'], elementClass: ['custom-4-class'], inputClass: ['custom-5-class'], helpClass: ['custom-6-class'], errorsClass: ['custom-7-class'], errorClass: ['custom-8-class'], }}) await flushPromises(); expect(wrapper.attributes('class')).toBe('formulate-input custom-1-class'); // Test the wrapper override expect(wrapper.find('.formulate-input-wrapper').attributes('class')) .toBe('formulate-input-wrapper custom-2-class'); // Test the label override expect(wrapper.find('label').attributes('class')) .toBe('formulate-input-label formulate-input-label--before custom-3-class'); // Test the element override expect(wrapper.find('.formulate-input-element').attributes('class')) .toBe('formulate-input-element formulate-input-element--text custom-4-class'); // Test the input override expect(wrapper.find('input').attributes('class')) .toBe('custom-5-class'); // Test the input override expect(wrapper.find('.formulate-input-help').attributes('class')) .toBe('formulate-input-help formulate-input-help--after custom-6-class'); // Test the errors wrapper expect(wrapper.find('.formulate-input-errors').attributes('class')) .toBe('formulate-input-errors custom-7-class'); // Test the inner error expect(wrapper.find('.formulate-input-error').attributes('class')) .toBe('formulate-input-error custom-8-class'); }) it('responds to hasValue state class key', async () => { const wrapper = mount(FormulateInput, { propsData: { type: 'text', labelHasValueClass: 'field-has-value', label: 'My label' }}) wrapper.find('input').setValue('some value') await flushPromises() expect(wrapper.find('label').attributes('class')).toBe('formulate-input-label formulate-input-label--before field-has-value') }) it('responds to isValid and hasErrors state class key', async () => { const wrapper = mount(FormulateInput, { propsData: { type: 'text', validation: 'required', inputIsValidClass: 'is-valid-input', inputHasErrorsClass: 'is-invalid-input', errorBehavior: 'live' }}) await flushPromises() expect(wrapper.find('input').attributes('class')).toBe('is-invalid-input') wrapper.find('input').setValue('some value') await flushPromises() expect(wrapper.find('input').attributes('class')).toBe('is-valid-input') }) it('responds to globally registered hasValue on every element key', async () => { const localVue = createLocalVue() localVue.use(Formulate, { classes: { outerHasValue: 'has-1-value', wrapperHasValue: 'has-2-value', labelHasValue: 'has-3-value', elementHasValue: 'has-4-value', inputHasValue: 'has-5-value', helpHasValue: 'has-6-value', errorsHasValue: 'has-7-value', errorHasValue: 'has-8-value' } }) const wrapper = mount(FormulateInput, { localVue, propsData: { type: 'text', label: 'foobar', help: 'barfoo', validation: 'required|in:123', errorBehavior: 'live' }}) wrapper.find('input').setValue('foobar') await flushPromises() expect(wrapper.attributes('class')).toBe('formulate-input has-1-value'); // Test the wrapper override expect(wrapper.find('.formulate-input-wrapper').attributes('class')) .toBe('formulate-input-wrapper has-2-value'); // Test the label override expect(wrapper.find('label').attributes('class')) .toBe('formulate-input-label formulate-input-label--before has-3-value'); // Test the element override expect(wrapper.find('.formulate-input-element').attributes('class')) .toBe('formulate-input-element formulate-input-element--text has-4-value'); // Test the input override expect(wrapper.find('input').attributes('class')) .toBe('has-5-value'); // Test the input override expect(wrapper.find('.formulate-input-help').attributes('class')) .toBe('formulate-input-help formulate-input-help--after has-6-value'); // Test the errors wrapper expect(wrapper.find('.formulate-input-errors').attributes('class')) .toBe('formulate-input-errors has-7-value'); // Test the inner error expect(wrapper.find('.formulate-input-error').attributes('class')) .toBe('formulate-input-error has-8-value'); resetInstance() }) it('assigns isValid irregardless of error-behavior', async () => { const wrapper = mount(FormulateInput, { propsData: { type: 'text', label: 'blah', labelIsValidClass: 'my-valid', errorBehavior: 'submit', validation: "required|in:foobar", value: '123' }}) await flushPromises() expect(wrapper.find('label').attributes('class')).toBe('formulate-input-label formulate-input-label--before') wrapper.find('input').setValue('foobar') await flushPromises() expect(wrapper.find('label').attributes('class')).toBe('formulate-input-label formulate-input-label--before my-valid') }) it('assigns hasErrors only when triggered by error visibility', async () => { const wrapper = mount(FormulateInput, { propsData: { type: 'text', inputHasErrorsClass: 'input-has-errors', errorBehavior: 'blur', validation: "required|in:foobar", value: '123' }}) await flushPromises() expect(wrapper.find('input').attributes('class')).toBe(undefined) wrapper.vm.context.blurHandler() await flushPromises() expect(wrapper.find('input').attributes('class')).toBe('input-has-errors') }) it('extracts pseudoProps when camelCase', () => { const wrapper = mount(FormulateInput, { propsData: { type: 'text', label: 'blah', labelClass: 'my-class' }}) expect(wrapper.vm.pseudoProps['labelClass']).toBe('my-class') }) it('extracts pseudoProps when kebab case', () => { const wrapper = mount({ template: '<FormulateInput type="text" label="blah" label-class="my-class" />' }) expect(wrapper.findComponent(FormulateInput).vm.pseudoProps['labelClass']).toBe('my-class') }) it('removes slotProps from the attributes and passes it', async () => { const localVue = createLocalVue() localVue.component('MyCustomLabel', { functional: true, render: (h, { props }) => h('label', props.infoText) }) localVue.component('MyCustomHelp', { functional: true, render: (h, { props }) => h('span', { class: 'my-help' }, props.infoHelp) }) localVue.component('MyCustomErrors', { functional: true, render: (h, { props }) => { if (props.context.visibleValidationErrors.length) { return h('div', { class: 'my-errors' }, props.infoErrors) } return null } }) localVue.component('MyAddMore', { functional: true, render: (h, { props }) => h('button', { class: 'my-add-more' }, props.infoAddMore) }) // localVue.component('MyRepeatable', { // functional: true, // render: (h, { props }) => h('div', { class: 'my-repeatable' }, props.infoRepeatable) // }) localVue.component('MyRemove', { functional: true, render: (h, { props }) => h('div', { class: 'my-remove' }, props.infoRemove) }) localVue.use(Formulate, { slotProps: { label: ['infoText'], help: ['infoHelp'], errors: ['infoErrors'], addMore: ['infoAddMore'], // repeatable: ['infoRepeatable'], remove: ['infoRemove'], }, slotComponents: { label: 'MyCustomLabel', help: 'MyCustomHelp', errors: 'MyCustomErrors', addMore: 'MyAddMore', // repeatable: 'MyRepeatable', remove: 'MyRemove' } }) const wrapper = mount(FormulateInput, { localVue, propsData: { type: 'group', validation: 'min:5,length', value: [{ test: '123' }], repeatable: true, errorBehavior: 'live', help: 'foobar', label: 'blah', infoText: 'Some label text', infoHelp: 'Some help text', infoErrors: 'Some error text', infoAddMore: 'Add some goodies', // infoRepeatable: 'Repeat me', infoRemove: 'Get outta here' }, slots: { default: '<FormulateInput type="text" name="test" />' } }) await flushPromises() expect(wrapper.find('[info-text]').exists()).toBe(false) expect(wrapper.find('label').text()).toBe('Some label text') expect(wrapper.find('.my-help').text()).toBe('Some help text') expect(wrapper.find('.my-errors').text()).toBe('Some error text') expect(wrapper.find('.my-add-more').text()).toBe('Add some goodies') // expect(wrapper.find('.my-repeatable').text()).toBe('Repeat me') expect(wrapper.find('.my-remove').text()).toBe('Get outta here') }) it('can remove all baseClasses globally', async () => { const localVue = createLocalVue() localVue.use(Formulate, { baseClasses: () => [] }) const wrapper = mount(FormulateInput, { localVue, propsData: { type: 'text', validation: 'required|in:abcdef', errorBehavior: 'live', label: 'foobar', help: 'foobar', value: 'other value' } }) await flushPromises() expect(wrapper.find('.formulate-input').exists()).toBe(false) expect(wrapper.find('.formulate-input-label').exists()).toBe(false) expect(wrapper.find('.formulate-input-label--before').exists()).toBe(false) expect(wrapper.find('.formulate-input-help').exists()).toBe(false) resetInstance() }) it('allows an empty string as a validation prop', async () => { const wrapper = mount(FormulateInput, { propsData: { validation: '', errorBehavior: 'live' }}) await flushPromises() expect(wrapper.find('.formulate-errors').exists()).toBe(false) }) it('allows access to attributes attrs in classes context object', async () => { const localVue = createLocalVue() localVue.use(Formulate, { classes: { input: (context, classes) => context.attrs.disabled ? classes.concat(['is-disabled']) : classes } }) const wrapper = mount(FormulateInput, { localVue, propsData: { type: 'button', disabled: 'true' } }) await flushPromises() expect(wrapper.find('button.is-disabled').exists()).toBe(true) resetInstance() }) it('allows custom slotProps for custom inputs', async () => { const localVue = createLocalVue() localVue.component('MyCustomInput', { functional: true, render: (h, { props }) => h('button', { class: 'my-custom-input' }, props.customInputProp) }) localVue.use(Formulate, { library: { 'my-input': { classification: 'text', component: 'MyCustomInput' } }, slotProps: { component: ['customInputProp'] } }) const wrapper = mount(FormulateInput, { localVue, propsData: { type: 'my-input', customInputProp: 'foo-bar' } }) await flushPromises() expect(wrapper.find('button.my-custom-input').text()).toBe('foo-bar') resetInstance() }) it('allows the disabling of input decorators with useInputDecorators: false', async () => { const localVue = createLocalVue() localVue.use(Formulate, { useInputDecorators: false }) const wrapper = mount(FormulateInput, { localVue, propsData: { type: 'checkbox', options: { a: 'A', b: 'B' } } }) await flushPromises() expect(wrapper.find('.formulate-input-element-decorator').exists()).toBeFalsy() resetInstance() }) it('uses the validationName by default', async () => { const localVue = createLocalVue() localVue.use(Formulate, { }) const wrapper = mount(FormulateInput, { localVue, propsData: { label: 'Hello world', name: 'something', validationName: 'This field', validation: 'required', errorBehavior: 'live' } }) await flushPromises() expect(wrapper.find('.formulate-input-errors li').text()).toBe('This field is required.') resetInstance() }) it('allows overriding the validation strategy via array', async () => { const localVue = createLocalVue() localVue.use(Formulate, { validationNameStrategy: ['label', 'name'] }) const wrapper = mount(FormulateInput, { localVue, propsData: { label: 'Hello world', name: 'something', validation: 'required', errorBehavior: 'live' } }) await flushPromises() expect(wrapper.find('.formulate-input-errors li').text()).toBe('Hello world is required.') resetInstance() }) it('allows overriding the validation strategy via function', async () => { const localVue = createLocalVue() localVue.use(Formulate, { validationNameStrategy: function (vm) { return vm.type === 'text' ? 'Some text' : vm.context.name } }) const wrapper = mount(FormulateInput, { localVue, propsData: { label: 'Hello world', name: 'something', validation: 'required', errorBehavior: 'live' } }) await flushPromises() expect(wrapper.find('.formulate-input-errors li').text()).toBe('Some text is required.') resetInstance() }) it('sets the field based on the value over the v-model when both are set', async () => { const wrapper = mount({ template: `<FormulateInput value="abc" v-model="modelValue" />`, data () { return { modelValue: '123' } } }) await flushPromises() expect(wrapper.find('input').element.value).toBe('abc') }) it('re-runs validation rules if the validation rules change', async () => { const wrapper = mount({ template: ` <FormulateInput :validation="validationRules" error-behavior="live" name="city" /> `, data () { return { validationRules: 'required' } } }) await flushPromises() expect(wrapper.find('.formulate-input-errors li').text()).toBe('City is required.') wrapper.vm.validationRules = 'optional' await flushPromises() expect(wrapper.find('.formulate-input-errors li').exists()).toBeFalsy() }) it('exposes validation rules within the context object', async () => { const wrapper = mount({ template: ` <FormulateInput label="Email" validation="required" > <template #label="{ rules, label }"> <label>{{ label }}{{ rules.some(({ ruleName }) => ruleName === 'required') ? '*' : '' }}</label> </template> </FormulateInput> ` }) expect(wrapper.find('label').text()).toBe('Email*') }) it('can override the errorList slotComponent', async () => { const localVue = createLocalVue() localVue.component('CustomErrors', { render: function (h) { return h( 'div', { class: 'custom-errors' }, this.visibleErrors .map(error => h('div', { class: 'error-item', key: error }, error)) ) }, props: ['visibleErrors'] }) localVue.use(Formulate, { slotComponents: { errorList: 'CustomErrors' } }) const wrapper = mount(FormulateInput, { localVue, propsData: { name: 'name', validation: 'required', errorBehavior: 'live' } }) await flushPromises() expect(wrapper.find('.formulate-input-error').exists()).toBeFalsy() expect(wrapper.find('.custom-errors .error-item').text()).toBe('Name is required.') resetInstance() }) it('can override the prefix slotComponent', async () => { const localVue = createLocalVue() localVue.component('PrefixMe', { render: function (h) { return h( 'div', { class: 'custom-prefix' }, ['Hello there', this.context.name] ) }, props: ['context'] }) localVue.use(Formulate, { slotComponents: { prefix: 'PrefixMe' } }) const wrapper = mount(FormulateInput, { localVue, propsData: { name: 'justin', } }) await flushPromises() expect(wrapper.find('.formulate-input-element .custom-prefix').text()).toBe('Hello therejustin') resetInstance() }) it('emits a blur-context event on blur', async () => { const listener = jest.fn(); const wrapper = mount(FormulateInput, { propsData: { type: 'text', validation: 'required|email', value: 'not an email', errorBehavior: 'live' }, listeners: { 'blur-context': listener } }) await flushPromises() wrapper.find('input').trigger('blur') await flushPromises() expect(listener.mock.calls.length).toBe(1) expect(listener.mock.calls[0][0].isValid).toBe(false) }) it('can debounce a standard input', async () => { const isdebounced = jest.fn(); const wrapper = mount(FormulateInput, { propsData: { type: 'text', validation: 'isdebounced', errorBehavior: 'live', debounce: 50, validationRules: { isdebounced } } }) const input = wrapper.find('input') setTimeout(() => input.setValue('a'), 5) setTimeout(() => input.setValue('ab'), 10) setTimeout(() => input.setValue('abc'), 15) setTimeout(() => input.setValue('abcd'), 20) await new Promise(resolve => setTimeout(resolve, 100)) expect(isdebounced.mock.calls.length).toBe(2) expect(isdebounced.mock.calls[1][0].value).toBe('abcd') }) })