diff --git a/test/unit/Formulario.test.js b/test/unit/Formulario.test.js new file mode 100644 index 0000000..9b8d479 --- /dev/null +++ b/test/unit/Formulario.test.js @@ -0,0 +1,95 @@ +import Formulario from '@/Formulario.js' + +describe('Formulario', () => { + it('can merge simple object', () => { + let a = { + optionA: true, + optionB: '1234' + } + let b = { + optionA: false + } + expect(Formulario.merge(a, b)).toEqual({ + optionA: false, + optionB: '1234' + }) + }) + + it('can add to simple array', () => { + let a = { + optionA: true, + optionB: ['first', 'second'] + } + let b = { + optionB: ['third'] + } + expect(Formulario.merge(a, b, true)).toEqual({ + optionA: true, + optionB: ['first', 'second', 'third'] + }) + }) + + it('can merge recursively', () => { + let a = { + optionA: true, + optionC: { + first: '123', + third: { + a: 'b' + } + }, + optionB: '1234' + } + let b = { + optionB: '567', + optionC: { + first: '1234', + second: '789', + } + } + expect(Formulario.merge(a, b)).toEqual({ + optionA: true, + optionC: { + first: '1234', + third: { + a: 'b' + }, + second: '789' + }, + optionB: '567' + }) + }) + + it('installs on vue instance', () => { + const components = [ + 'FormularioForm', + 'FormularioInput', + 'FormularioGrouping', + ] + const registry = [] + function Vue () {} + Vue.component = function (name, instance) { + registry.push(name) + } + Formulario.install(Vue, { extended: true }) + expect(Vue.prototype.$formulario).toBe(Formulario) + expect(registry).toEqual(components) + }) + + it('can extend instance in a plugin', () => { + function Vue () {} + Vue.component = function (name, instance) {} + const plugin = function (i) { + i.extend({ + rules: { + testRule: () => false + } + }) + } + Formulario.install(Vue, { + plugins: [ plugin ] + }) + + expect(typeof Vue.prototype.$formulario.options.rules.testRule).toBe('function') + }) +}) diff --git a/test/unit/FormularioForm.test.js b/test/unit/FormularioForm.test.js new file mode 100644 index 0000000..1ef9b20 --- /dev/null +++ b/test/unit/FormularioForm.test.js @@ -0,0 +1,527 @@ +import Vue from 'vue' +import { mount, shallowMount } from '@vue/test-utils' +import flushPromises from 'flush-promises' +import Formulario from '../../src/Formulario.js' +import FormSubmission from '../../src/FormSubmission.js' +import FormularioForm from '@/FormularioForm.vue' +import FormularioInput from '@/FormularioInput.vue' + +Vue.use(Formulario) + +describe('FormularioForm', () => { + it('render a form DOM element', () => { + const wrapper = mount(FormularioForm) + expect(wrapper.find('form').exists()).toBe(true) + }) + + it('accepts a default slot', () => { + const wrapper = mount(FormularioForm, { + slots: { + default: '
' + } + }) + expect(wrapper.find('form div.default-slot-item').exists()).toBe(true) + }) + + it('intercepts submit event', () => { + const formSubmitted = jest.fn() + const wrapper = mount(FormularioForm, { + slots: { + default: "', - } - }) - const repeats = wrapper.findAll('.remove-this') - expect(repeats.length).toBe(2) - const button = wrapper.find('.remove-this') - expect(button.text()).toBe('Get outta here') - button.trigger('click') - await flushPromises() - expect(wrapper.findAll('input').wrappers.map(w => w.element.value)).toEqual(['Android']) - }) - - it('removes the proper item from the group', async () => { - const wrapper = mount(FormulateInput, { - propsData: { type: 'group', repeatable: true }, - slots: { - default: '' - } - }) - wrapper.find('input').setValue('first entry') - wrapper.find('.formulate-input-group-add-more button').trigger('click') - wrapper.find('.formulate-input-group-add-more button').trigger('click') - await flushPromises(); - wrapper.findAll('input').at(1).setValue('second entry') - wrapper.findAll('input').at(2).setValue('third entry') - // First verify all the proper entries are where we expect - expect(wrapper.findAll('input').wrappers.map(input => input.element.value)).toEqual(['first entry', 'second entry', 'third entry']) - // Now remove the middle one - wrapper.findAll('.formulate-input-group-repeatable-remove').at(1).trigger('click') - await flushPromises() - expect(wrapper.findAll('input').wrappers.map(input => input.element.value)).toEqual(['first entry', 'third entry']) - }) - - it('does not show an error message on group input when child has an error', async () => { - const wrapper = mount({ - template: ` - - - - - - - - - `, - data () { - return { - users: [{email: 'jon@example.com'}, {}], - } - } - }) - const form = wrapper.findComponent(FormulateForm) - await form.vm.formSubmitted() - expect(wrapper.find('[data-classification="group"] > .formulate-input-errors').exists()).toBe(false) - }) - - it('exposes the index to the context object on default slot', async () => { - const wrapper = mount({ - template: ` - -
{{ name }}-{{ index }}
-
- `, - }) - const repeatables = wrapper.findAll('.repeatable') - expect(repeatables.length).toBe(2) - expect(repeatables.at(0).text()).toBe('test-0') - expect(repeatables.at(1).text()).toBe('test-1') - }) - - it('forces non-repeatable groups to not initialize with an empty array', async () => { - const wrapper = mount({ - template: ` - -
- - `, - data () { - return { - model: [] - } - } - }) - await flushPromises(); - expect(wrapper.findComponent(FormulateGrouping).vm.items).toEqual([{}]) - }) - - it('allows repeatable groups to initialize with an empty array', async () => { - const wrapper = mount({ - template: ` - -
- - `, - data () { - return { - model: [] - } - } - }) - await flushPromises(); - expect(wrapper.findComponent(FormulateGrouping).vm.items).toEqual([]) - }) -}) diff --git a/test/unit/FormulateInputSelect.test.js b/test/unit/FormulateInputSelect.test.js deleted file mode 100644 index 2b6924d..0000000 --- a/test/unit/FormulateInputSelect.test.js +++ /dev/null @@ -1,55 +0,0 @@ -import Vue from 'vue' -import { mount } from '@vue/test-utils' -import flushPromises from 'flush-promises' -import Formulate from '@/Formulate.js' -import FormulateInput from '@/FormulateInput.vue' -import FormulateInputSelect from '@/inputs/FormulateInputSelect.vue' - -Vue.use(Formulate) - -describe('FormulateInputSelect', () => { - it('renders select input when type is "select"', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'select' } }) - expect(wrapper.findComponent(FormulateInputSelect).exists()).toBe(true) - }) - - it('renders select options when options object is passed', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'select', options: { first: 'First', second: 'Second' } } }) - const option = wrapper.find('option[value="second"]') - expect(option.exists()).toBe(true) - expect(option.text()).toBe('Second') - }) - - it('renders select options when options array is passed', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'select', options: [ - { value: 13, label: 'Jane' }, - { value: 22, label: 'Jon' } - ]} }) - const option = wrapper.find('option[value="22"]') - expect(option.exists()).toBe(true) - expect(option.text()).toBe('Jon') - }) - - it('renders select list with no options when empty array is passed.', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'select', options: []} }) - const option = wrapper.find('option') - expect(option.exists()).toBe(false) - }) - - it('renders select list placeholder option.', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'select', placeholder: 'Select this', options: []} }) - const options = wrapper.findAll('option') - expect(options.length).toBe(1) - expect(options.at(0).attributes('disabled')).toBeTruthy() - }) - - it('passes an explicitly given name prop through to the root element', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'select', options: [], name: 'foo' } }) - expect(wrapper.find('select[name="foo"]').exists()).toBe(true) - }) - - it('additional context does not bleed through to text select attributes', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'select' } } ) - expect(Object.keys(wrapper.find('select').attributes())).toEqual(["id"]) - }) -}) diff --git a/test/unit/FormulateInputSlider.test.js b/test/unit/FormulateInputSlider.test.js deleted file mode 100644 index 7129b3f..0000000 --- a/test/unit/FormulateInputSlider.test.js +++ /dev/null @@ -1,38 +0,0 @@ -import Vue from 'vue' -import { mount } from '@vue/test-utils' -import Formulate from '@/Formulate.js' -import FormulateInput from '@/FormulateInput.vue' -import FormulateInputSlider from '@/inputs/FormulateInputSlider.vue' - -Vue.use(Formulate) - -/** - * Test each type of slider element - */ - -describe('FormulateInputSlider', () => { - it('renders range input when type is "range"', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'range' } }) - expect(wrapper.findComponent(FormulateInputSlider).exists()).toBe(true) - }) - - it('does not show value if the show-value prop is not set', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'range', value: '15', min: '0', max: '100' } }) - expect(wrapper.find('.formulate-input-element-range-value').exists()).toBe(false) - }) - - it('renders the value when type is "range" and show-value prop is set', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'range', showValue: 'true', value: '15', min: '0', max: '100' } }) - expect(wrapper.find('.formulate-input-element-range-value').text()).toBe('15') - }) - - it('passes an explicitly given name prop through to the root element', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'range', name: 'foo' } }) - expect(wrapper.find('input[name="foo"]').exists()).toBe(true) - }) - - it('additional context does not bleed through to range input attributes', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'range' } } ) - expect(Object.keys(wrapper.find('input[type="range"]').attributes())).toEqual(["type", "id"]) - }) -}) diff --git a/test/unit/FormulateInputText.test.js b/test/unit/FormulateInputText.test.js deleted file mode 100644 index 9b1f9e0..0000000 --- a/test/unit/FormulateInputText.test.js +++ /dev/null @@ -1,313 +0,0 @@ -import Vue from 'vue' -import flushPromises from 'flush-promises' -import { mount } from '@vue/test-utils' -import Formulate from '../../src/Formulate.js' -import FormulateInput from '@/FormulateInput.vue' -import FormulateInputText from '@/inputs/FormulateInputText.vue' -import { doesNotReject } from 'assert'; - -Vue.use(Formulate) - -/** - * Test each type of text element - */ - -describe('FormulateInputText', () => { - it('renders text input when type is "text"', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'text' } }) - expect(wrapper.findComponent(FormulateInputText).exists()).toBe(true) - }) - - it('renders search input when type is "search"', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'search' } }) - expect(wrapper.findComponent(FormulateInputText).exists()).toBe(true) - }) - - it('renders email input when type is "email"', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'email' } }) - expect(wrapper.findComponent(FormulateInputText).exists()).toBe(true) - }) - - it('renders number input when type is "number"', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'number' } }) - expect(wrapper.findComponent(FormulateInputText).exists()).toBe(true) - }) - - it('renders color input when type is "color"', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'color' } }) - expect(wrapper.findComponent(FormulateInputText).exists()).toBe(true) - }) - - it('renders date input when type is "date"', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'date' } }) - expect(wrapper.findComponent(FormulateInputText).exists()).toBe(true) - }) - - it('renders month input when type is "month"', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'month' } }) - expect(wrapper.findComponent(FormulateInputText).exists()).toBe(true) - }) - - it('renders password input when type is "password"', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'password' } }) - expect(wrapper.findComponent(FormulateInputText).exists()).toBe(true) - }) - - it('renders tel input when type is "tel"', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'tel' } }) - expect(wrapper.findComponent(FormulateInputText).exists()).toBe(true) - }) - - it('renders time input when type is "time"', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'time' } }) - expect(wrapper.findComponent(FormulateInputText).exists()).toBe(true) - }) - - it('renders url input when type is "url"', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'url' } }) - expect(wrapper.findComponent(FormulateInputText).exists()).toBe(true) - }) - - it('renders week input when type is "week"', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'week' } }) - expect(wrapper.findComponent(FormulateInputText).exists()).toBe(true) - }) - - /** - * Test rendering functionality to text inputs - */ - - it('automatically assigns an id', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'text' } }) - expect(wrapper.vm.context).toHaveProperty('id') - expect(wrapper.find(`input[id="${wrapper.vm.context.attributes.id}"]`).exists()).toBe(true) - }) - - it('passes an explicitly given name prop through to the root text element', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'text', name: 'foo' } }) - expect(wrapper.find('input[name="foo"]').exists()).toBe(true) - }) - - it('passes an explicitly given name prop through to the root textarea element', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'textarea', name: 'foo' } }) - expect(wrapper.find('textarea[name="foo"]').exists()).toBe(true) - }) - - it('additional context does not bleed through to text input attributes', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'text' } } ) - expect(Object.keys(wrapper.find('input[type="text"]').attributes())).toEqual(["type", "id"]) - }) - - it('additional context does not bleed through to textarea input attributes', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'textarea' } } ) - expect(Object.keys(wrapper.find('textarea').attributes())).toEqual(["id"]) - }) - - it('doesn’t automatically add a label', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'text' } }) - expect(wrapper.find('label').exists()).toBe(false) - }) - - it('renders labels', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'text', label: 'Field label' } }) - expect(wrapper.find(`label[for="${wrapper.vm.context.attributes.id}"]`).exists()).toBe(true) - }) - - it('doesn’t automatically render help text', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'text' } }) - expect(wrapper.find(`.formulate-input-help`).exists()).toBe(false) - }) - - it('renders help text', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'text', help: 'This is some help text' } }) - expect(wrapper.find(`.formulate-input-help`).exists()).toBe(true) - }) - - /** - * Test data binding - */ - it('emits input (vmodel) event with value when edited', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'text' } }) - wrapper.find('input').setValue('Updated Value') - expect(wrapper.emitted().input).toBeTruthy() - expect(wrapper.emitted().input[0]).toEqual(['Updated Value']) - }) - - - it('doesn’t re-context itself if there were no changes', async () => { - const wrapper = mount({ - data () { - return { - valueA: 'first value', - valueB: 'second value' - } - }, - template: ` -
- - -
- ` - }) - await flushPromises() - const firstContext = wrapper.findComponent({ref: "first"}).vm.context - const secondContext = wrapper.findComponent({ref: "second"}).vm.context - wrapper.find('input').setValue('new value') - await flushPromises() - expect(firstContext).toBeTruthy() - expect(wrapper.vm.valueA === 'new value').toBe(true) - expect(wrapper.vm.valueB === 'second value').toBe(true) - expect(wrapper.findComponent({ref: "first"}).vm.context === firstContext).toBe(false) - expect(wrapper.findComponent({ref: "second"}).vm.context === secondContext).toBe(true) - }) - - it('uses the v-model value as the initial value', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'text', formulateValue: 'initial val' } }) - expect(wrapper.find('input').element.value).toBe('initial val') - }) - - it('uses the value as the initial value', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'text', value: 'initial val' } }) - expect(wrapper.find('input').element.value).toBe('initial val') - }) - - it('uses the value prop as the initial value when v-model provided', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'text', formulateValue: 'initial val', value: 'initial other val' } }) - expect(wrapper.find('input').element.value).toBe('initial other val') - }) - - it('uses a proxy model internally if it doesnt have a v-model', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'textarea' } }) - const input = wrapper.find('textarea') - input.setValue('changed value') - expect(wrapper.vm.proxy).toBe('changed value') - }) - - - /** - * Test error handling - */ - it('doesn’t automatically render errors', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'text' } }) - expect(wrapper.find('.formulate-input-errors').exists()).toBe(false) - }) - - it('accepts a single string as an error prop', async () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'text', error: 'This is an error' } }) - expect(wrapper.find('.formulate-input-errors').exists()).toBe(true) - }) - - it('accepts an array as errors prop', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'text', errorBehavior: 'live', errors: ['This is an error', 'this is another'] } }) - expect(wrapper.findAll('.formulate-input-error').length).toBe(2) - }) - - it('removes any duplicate errors', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'text', errorBehavior: 'live', errors: ['This is an error', 'This is an error'] } }) - expect(wrapper.findAll('.formulate-input-error').length).toBe(1) - }) - - it('adds data-has-errors when there are errors', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'text', errorBehavior: 'live', errors: ['This is an error', 'This is an error'] } }) - expect(wrapper.find('[data-has-errors]').exists()).toBe(true) - }) - - it('Should always show explicitly set errors, but not validation errors', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'text', validation: 'required', errorBehavior: 'blur', errors: ['Bad input'] } }) - expect(wrapper.find('[data-has-errors]').exists()).toBe(true) - expect(wrapper.find('[data-is-showing-errors]').exists()).toBe(true) - expect(wrapper.findAll('.formulate-input-error').length).toBe(1) - }) - - it('Should show no errors when there are no errors', () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'text' } }) - expect(wrapper.find('[data-has-errors]').exists()).toBe(false) - expect(wrapper.find('[data-is-showing-errors]').exists()).toBe(false) - expect(wrapper.findAll('.formulate-input-error').exists()).toBe(false) - }) - - it('allows error-behavior blur to be overridden with show-errors', async () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'text', errorBehavior: 'blur', showErrors: true, validation: 'required' } }) - await flushPromises() - expect(wrapper.find('[data-has-errors]').exists()).toBe(true) - expect(wrapper.find('[data-is-showing-errors]').exists()).toBe(true) - expect(wrapper.findAll('.formulate-input-errors').exists()).toBe(true) - expect(wrapper.findAll('.formulate-input-error').length).toBe(1) - }) - - it('shows errors on blur with error-behavior blur', async () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'text', errorBehavior: 'blur', validation: 'required' } }) - await wrapper.vm.$nextTick() - await flushPromises() - expect(wrapper.find('[data-has-errors]').exists()).toBe(true) - expect(wrapper.find('[data-is-showing-errors]').exists()).toBe(false) - expect(wrapper.findAll('.formulate-input-error').exists()).toBe(false) - expect(wrapper.vm.showValidationErrors).toBe(false) - wrapper.find('input').trigger('blur') - await flushPromises() - expect(wrapper.vm.showValidationErrors).toBe(true) - expect(wrapper.find('[data-is-showing-errors]').exists()).toBe(true) - // expect(wrapper.findAll('.formulate-input-errors').exists()).toBe(true) - // expect(wrapper.findAll('.formulate-input-error').length).toBe(1) - }) - - it('continues to show errors if validation fires more than one time', async () => { - const wrapper = mount(FormulateInput, { propsData: { type: 'date', errorBehavior: 'live', validation: [['after', '01/01/2021']] , value: '01/01/1999' } }) - await wrapper.vm.$nextTick() - await flushPromises() - expect(wrapper.find('[data-has-errors]').exists()).toBe(true) - wrapper.find('input').trigger('input') - await wrapper.vm.$nextTick() - await flushPromises() - expect(wrapper.find('[data-has-errors]').exists()).toBe(true) - }) - - - it('allows label-before override with scoped slot', async () => { - const wrapper = mount(FormulateInput, { - propsData: { type: 'text', label: 'flavor' }, - scopedSlots: { - label: '' - } - }) - expect(wrapper.find('label').text()).toBe('flavor town') - }) - - it('allows label-after override with scoped slot', async () => { - const wrapper = mount(FormulateInput, { - propsData: { type: 'text', label: 'flavor', labelPosition: 'after' }, - scopedSlots: { - label: '' - } - }) - expect(wrapper.find('label').text()).toBe('flavor town') - }) - - it('allows help-before override', async () => { - const wrapper = mount(FormulateInput, { - propsData: { type: 'text', label: 'flavor', help: 'I love this next field...', helpPosition: 'before' }, - }) - expect(wrapper.find('label + *').classes('formulate-input-help')).toBe(true) - }) - - it('Allow help text override with scoped slot', async () => { - const wrapper = mount(FormulateInput, { - propsData: { type: 'text', name: 'soda', help: 'Do you want some'}, - scopedSlots: { - help: '{{ props.help }} {{ props.name }}?' - } - }) - expect(wrapper.find('small').text()).toBe('Do you want some soda?') - }) - - it('Allow errors override with scoped slot', async () => { - const wrapper = mount(FormulateInput, { - propsData: { type: 'text', name: 'soda', validation: 'required|in:foo,bar', errorBehavior: 'live' }, - scopedSlots: { - errors: '
  • {{ error }}
' - } - }) - await flushPromises(); - expect(wrapper.findAll('.my-errors li').length).toBe(2) - }) -})