UNPKG

vuetify

Version:

Vue Material Component Framework

872 lines (679 loc) 22.1 kB
import Vue from 'vue' import VTextField from '../VTextField' import VProgressLinear from '../../VProgressLinear' import { mount, MountOptions, Wrapper, } from '@vue/test-utils' import { waitAnimationFrame } from '../../../../test' describe('VTextField.ts', () => { // eslint-disable-line max-statements type Instance = InstanceType<typeof VTextField> let mountFunction: (options?: MountOptions<Instance>) => Wrapper<Instance> beforeEach(() => { mountFunction = (options?: MountOptions<Instance>) => { return mount(VTextField, { // https://github.com/vuejs/vue-test-utils/issues/1130 sync: false, mocks: { $vuetify: { rtl: false, }, }, ...options, }) } }) it('should render component and match snapshot', () => { const wrapper = mountFunction() expect(wrapper.html()).toMatchSnapshot() }) it('should pass required attr to the input', () => { const wrapper = mountFunction({ attrs: { required: true, }, }) const input = wrapper.findAll('input').at(0) expect(input.element.getAttribute('required')).toBe('required') }) it('should pass events to internal input field', () => { const keyup = jest.fn() const component = { render (h) { return h(VTextField, { on: { keyUp: keyup }, props: { download: '' }, attrs: {} }) }, } const wrapper = mount(component) const input = wrapper.findAll('input').at(0) input.trigger('keyUp', { keyCode: 65 }) expect(keyup).toHaveBeenCalled() }) it('should not render aria-label attribute on text field element with no label value or id', () => { const wrapper = mountFunction({ propsData: { label: null, }, attrs: {}, }) const inputGroup = wrapper.findAll('input').at(0) expect(inputGroup.element.getAttribute('aria-label')).toBeFalsy() }) it('should not render aria-label attribute on text field element with id', () => { const wrapper = mountFunction({ propsData: { label: 'Test', }, attrs: { id: 'Test', }, }) const inputGroup = wrapper.findAll('input').at(0) expect(inputGroup.element.getAttribute('aria-label')).toBeFalsy() }) it('should start out as invalid', () => { const wrapper = mountFunction({ propsData: { rules: [v => !!v || 'Required'], }, }) expect(wrapper.vm.valid).toEqual(false) }) it('should start validating on input', async () => { const wrapper = mountFunction() expect(wrapper.vm.shouldValidate).toEqual(false) wrapper.setProps({ value: 'asd' }) await wrapper.vm.$nextTick() expect(wrapper.vm.shouldValidate).toEqual(true) }) it('should not start validating on input if validate-on-blur prop is set', async () => { const wrapper = mountFunction({ propsData: { validateOnBlur: true, }, }) expect(wrapper.vm.shouldValidate).toEqual(false) wrapper.setProps({ value: 'asd' }) await wrapper.vm.$nextTick() expect(wrapper.vm.shouldValidate).toEqual(false) }) // TODO: this fails without sync, nextTick doesn't help // https://github.com/vuejs/vue-test-utils/issues/1130 it.skip('should not display counter when set to false/undefined/null', async () => { const wrapper = mountFunction({ propsData: { counter: true, }, attrs: { maxlength: 50, }, }) expect(wrapper.findAll('.v-counter').wrappers[0]).not.toBeUndefined() expect(wrapper.html()).toMatchSnapshot() wrapper.setProps({ counter: false }) await wrapper.vm.$nextTick() expect(wrapper.html()).toMatchSnapshot() expect(wrapper.findAll('.v-counter').wrappers[0]).toBeUndefined() wrapper.setProps({ counter: undefined }) await wrapper.vm.$nextTick() expect(wrapper.findAll('.v-counter').wrappers[0]).toBeUndefined() wrapper.setProps({ counter: null }) await wrapper.vm.$nextTick() expect(wrapper.findAll('.v-counter').wrappers[0]).toBeUndefined() }) it('should have readonly attribute', () => { const wrapper = mountFunction({ propsData: { readonly: true, }, }) const input = wrapper.findAll('input').at(0) expect(input.element.getAttribute('readonly')).toBe('readonly') }) it('should clear input value', async () => { const wrapper = mountFunction({ propsData: { clearable: true, value: 'foo', }, }) const clear = wrapper.findAll('.v-input__icon--clear .v-icon').at(0) const input = jest.fn() wrapper.vm.$on('input', input) expect(wrapper.vm.internalValue).toBe('foo') clear.trigger('click') await wrapper.vm.$nextTick() expect(input).toHaveBeenCalledWith(null) }) it('should not clear input if not clearable and has appended icon (with callback)', async () => { const click = jest.fn() const wrapper = mountFunction({ propsData: { value: 'foo', appendIcon: 'block', }, listeners: { 'click:append': click, }, }) const icon = wrapper.findAll('.v-input__icon--append .v-icon').at(0) icon.trigger('click') await wrapper.vm.$nextTick() expect(wrapper.vm.internalValue).toBe('foo') expect(click).toHaveBeenCalledTimes(1) }) it('should not clear input if not clearable and has appended icon (without callback)', async () => { const wrapper = mountFunction({ propsData: { value: 'foo', appendIcon: 'block', }, }) const icon = wrapper.findAll('.v-input__icon--append .v-icon').at(0) icon.trigger('click') await wrapper.vm.$nextTick() expect(wrapper.vm.internalValue).toBe('foo') }) it('should start validating on blur', async () => { const rule = jest.fn().mockReturnValue(true) const wrapper = mountFunction({ propsData: { rules: [rule], validateOnBlur: true, }, }) const input = wrapper.find('input') expect(wrapper.vm.shouldValidate).toEqual(false) // Rules are called once on mount expect(rule).toHaveBeenCalledTimes(1) input.trigger('focus') await wrapper.vm.$nextTick() input.element.value = 'f' input.trigger('input') await wrapper.vm.$nextTick() expect(rule).toHaveBeenCalledTimes(1) input.trigger('blur') await wrapper.vm.$nextTick() expect(wrapper.vm.shouldValidate).toEqual(true) expect(rule).toHaveBeenCalledTimes(2) }) it('should keep its value on blur', async () => { const wrapper = mountFunction({ propsData: { value: 'asd', }, }) const input = wrapper.findAll('input').at(0) input.element.value = 'fgh' input.trigger('input') input.trigger('blur') expect(input.element.value).toBe('fgh') }) // TODO: this fails without sync, nextTick doesn't help // https://github.com/vuejs/vue-test-utils/issues/1130 it.skip('should update if value is changed externally', async () => { const wrapper = mountFunction({}) const input = wrapper.findAll('input').at(0) wrapper.setProps({ value: 'fgh' }) expect(input.element.value).toBe('fgh') input.trigger('focus') wrapper.setProps({ value: 'jkl' }) expect(input.element.value).toBe('jkl') }) it('should fire a single change event on blur', async () => { let value = 'asd' const change = jest.fn() const component = { render (h) { return h(VTextField, { on: { input: i => value = i, change, }, props: { value }, }) }, } const wrapper = mount(component) const input = wrapper.findAll('input').at(0) input.trigger('focus') await wrapper.vm.$nextTick() input.element.value = 'fgh' input.trigger('input') await wrapper.vm.$nextTick() input.trigger('blur') await wrapper.vm.$nextTick() expect(change).toHaveBeenCalledWith('fgh') expect(change.mock.calls).toHaveLength(1) }) // TODO: this fails without sync // https://github.com/vuejs/vue-test-utils/issues/1130 it.skip('should not make prepend icon clearable', () => { const wrapper = mountFunction({ propsData: { prependIcon: 'check', appendIcon: 'check', value: 'test', clearable: true, }, }) const prepend = wrapper.findAll('.v-input__icon--append .v-icon').at(0) expect(prepend.text()).toBe('check') expect(prepend.element.classList).not.toContain('input-group__icon-cb') }) // TODO: this fails even without sync for some reason // https://github.com/vuejs/vue-test-utils/issues/1130 it.skip('should not emit change event if value has not changed', async () => { const change = jest.fn() let value = 'test' const component = { // eslint-disable-next-line sonarjs/no-identical-functions render (h) { return h(VTextField, { on: { input: i => value = i, change, }, props: { value }, }) }, } const wrapper = mount(component, { sync: false }) const input = wrapper.findAll('input').at(0) input.trigger('focus') await wrapper.vm.$nextTick() input.trigger('blur') await wrapper.vm.$nextTick() expect(change.mock.calls).toHaveLength(0) }) it('should render component with async loading and match snapshot', () => { const wrapper = mountFunction({ propsData: { loading: true, }, }) expect(wrapper.html()).toMatchSnapshot() }) it('should render component with async loading and custom progress and match snapshot', () => { Vue.prototype.$vuetify = { rtl: false, } const progress = Vue.component('test', { render (h) { return h(VProgressLinear, { props: { indeterminate: true, height: 7, color: 'orange', }, }) }, }) const wrapper = mountFunction({ sync: false, propsData: { loading: true, }, slots: { progress: [progress], }, }) expect(wrapper.html()).toMatchSnapshot() }) it('should display the number 0', async () => { const wrapper = mountFunction({ propsData: { value: 0 }, }) expect(wrapper.vm.$refs.input.value).toBe('0') }) it('should autofocus', async () => { const wrapper = mountFunction({ attachToDocument: true, propsData: { autofocus: true, }, }) const focus = jest.fn() wrapper.vm.$on('focus', focus) await wrapper.vm.$nextTick() expect(wrapper.vm.isFocused).toBe(true) wrapper.vm.onClick() expect(focus.mock.calls).toHaveLength(0) wrapper.setData({ isFocused: false }) wrapper.vm.onClick() expect(focus.mock.calls).toHaveLength(1) wrapper.setProps({ disabled: true }) wrapper.setData({ isFocused: false }) wrapper.vm.onClick() expect(focus.mock.calls).toHaveLength(1) wrapper.setProps({ disabled: false }) wrapper.vm.onClick() expect(focus.mock.calls).toHaveLength(2) delete wrapper.vm.$refs.input wrapper.vm.onFocus() expect(focus.mock.calls).toHaveLength(2) }) it('should have prefix and suffix', () => { const wrapper = mountFunction({ propsData: { prefix: '$', suffix: '.com', }, }) expect(wrapper.html()).toMatchSnapshot() }) it('should use a custom clear callback', async () => { const clear = jest.fn() const wrapper = mountFunction({ propsData: { clearable: true, value: 'foo', }, listeners: { 'click:clear': clear, }, }) wrapper.vm.$on('click:clear', clear) wrapper.find('.v-input__icon--clear .v-icon').trigger('click') expect(clear).toHaveBeenCalled() }) it('should not generate label', () => { const wrapper = mountFunction() expect(wrapper.vm.genLabel()).toBeNull() wrapper.setProps({ singleLine: true }) expect(wrapper.vm.genLabel()).toBeNull() wrapper.setProps({ placeholder: 'foo' }) expect(wrapper.vm.genLabel()).toBeNull() wrapper.setProps({ placeholder: undefined, value: 'bar', }) expect(wrapper.vm.genLabel()).toBeNull() wrapper.setProps({ label: 'bar', value: undefined, }) expect(wrapper.vm.genLabel()).toBeTruthy() }) it('should propagate id to label for attribute', () => { const wrapper = mountFunction({ propsData: { label: 'foo', id: 'bar', }, attrs: { id: 'bar', }, domProps: { id: 'bar', }, }) const label = wrapper.find('label') expect(label.element.getAttribute('for')).toBe('bar') }) it('should render an appended outer icon', () => { const wrapper = mountFunction({ propsData: { appendOuterIcon: 'search', }, }) expect(wrapper.find('.v-input__icon--append-outer .v-icon').element.innerHTML).toBe('search') }) // TODO: this fails without sync, nextTick doesn't help // https://github.com/vuejs/vue-test-utils/issues/1130 it.skip('should have correct max value', async () => { const wrapper = mountFunction({ attrs: { maxlength: 25, }, propsData: { counter: true, }, }) const counter = wrapper.find('.v-counter') expect(counter.element.innerHTML).toBe('0 / 25') wrapper.setProps({ counter: '50' }) await wrapper.vm.$nextTick() expect(counter.element.innerHTML).toBe('0 / 50') }) it('should use counter value function', async () => { const wrapper = mountFunction({ attrs: { maxlength: 25, }, propsData: { counter: true, counterValue: (value?: string): number => (value || '').replace(/\s/g, '').length, }, }) const counter = wrapper.find('.v-counter') expect(counter.element.innerHTML).toBe('0 / 25') wrapper.setProps({ value: 'foo bar baz' }) await wrapper.vm.$nextTick() expect(counter.element.innerHTML).toBe('9 / 25') wrapper.setProps({ counter: '50' }) await wrapper.vm.$nextTick() expect(counter.element.innerHTML).toBe('9 / 50') wrapper.setProps({ counterValue: (value?: string): number => (value || '').replace(/ba/g, '').length, }) await wrapper.vm.$nextTick() expect(counter.element.innerHTML).toBe('7 / 50') }) it('should set bad input on input', () => { const wrapper = mountFunction() expect(wrapper.vm.badInput).toBeFalsy() wrapper.vm.onInput({ target: {}, }) expect(wrapper.vm.badInput).toBeFalsy() wrapper.vm.onInput({ target: { validity: { badInput: false } }, }) expect(wrapper.vm.badInput).toBeFalsy() wrapper.vm.onInput({ target: { validity: { badInput: true } }, }) expect(wrapper.vm.badInput).toBe(true) }) it('should not apply id to root element', () => { const wrapper = mountFunction({ attrs: { id: 'foo' }, }) const input = wrapper.find('input') expect(wrapper.element.id).toBe('') expect(input.element.id).toBe('foo') }) it('should fire change event when pressing enter', () => { const wrapper = mountFunction() const input = wrapper.find('input') const change = jest.fn() wrapper.vm.$on('change', change) input.trigger('focus') input.element.value = 'foo' input.trigger('input') input.trigger('keydown.enter') input.trigger('keydown.enter') expect(change).toHaveBeenCalledTimes(2) }) it('should have focus and blur methods', async () => { const wrapper = mountFunction() const onBlur = jest.spyOn(wrapper.vm.$refs.input, 'blur') const onFocus = jest.spyOn(wrapper.vm.$refs.input, 'focus') wrapper.vm.focus() expect(onFocus).toHaveBeenCalledTimes(1) wrapper.vm.blur() // https://github.com/vuetifyjs/vuetify/issues/5913 // Blur waits a requestAnimationFrame // to resolve a bug in MAC / Safari await waitAnimationFrame() expect(onBlur).toHaveBeenCalledTimes(1) }) // TODO: this fails without sync, nextTick doesn't help // https://github.com/vuejs/vue-test-utils/issues/1130 it.skip('should activate label when using dirtyTypes', async () => { const dirtyTypes = ['color', 'file', 'time', 'date', 'datetime-local', 'week', 'month'] const wrapper = mountFunction({ propsData: { label: 'Foobar', }, }) const label = wrapper.find('.v-label') for (const type of dirtyTypes) { wrapper.setProps({ type }) await wrapper.vm.$nextTick() expect(label.element.classList).toContain('v-label--active') expect(wrapper.vm.$el.classList).toContain('v-input--is-label-active') wrapper.setProps({ type: undefined }) await wrapper.vm.$nextTick() expect(label.element.classList).not.toContain('v-label--active') expect(wrapper.vm.$el.classList).not.toContain('v-input--is-label-active') } }) it('should apply theme to label, counter, messages and icons', () => { const wrapper = mountFunction({ propsData: { counter: true, label: 'foo', hint: 'bar', persistentHint: true, light: true, prependIcon: 'prepend', appendIcon: 'append', prependInnerIcon: 'prepend-inner', appendOuterIcon: 'append-outer', }, }) expect(wrapper.html()).toMatchSnapshot() }) // https://github.com/vuetifyjs/vuetify/issues/5018 it('should not focus input when mousedown did not originate from input', () => { const focus = jest.fn() const wrapper = mountFunction({ methods: { focus }, }) const input = wrapper.find('.v-input__slot') input.trigger('mousedown') input.trigger('mouseup') input.trigger('mouseup') expect(focus).toHaveBeenCalledTimes(1) }) it('should hide messages if no messages and hide-details is auto', async () => { const wrapper = mountFunction({ propsData: { hideDetails: 'auto', }, }) expect(wrapper.html()).toMatchSnapshot() wrapper.setProps({ counter: 7 }) await wrapper.vm.$nextTick() expect(wrapper.html()).toMatchSnapshot() wrapper.setProps({ counter: null, errorMessages: 'required' }) await wrapper.vm.$nextTick() expect(wrapper.html()).toMatchSnapshot() }) // https://github.com/vuetifyjs/vuetify/issues/8268 // TODO: this fails without sync, nextTick doesn't help // https://github.com/vuejs/vue-test-utils/issues/1130 it.skip('should recalculate prefix width on prefix change', async () => { const setPrefixWidth = jest.fn() const wrapper = mountFunction({ methods: { setPrefixWidth }, }) wrapper.setProps({ prefix: 'foobar' }) await wrapper.vm.$nextTick() expect(setPrefixWidth).toHaveBeenCalledTimes(2) }) // https://github.com/vuetifyjs/vuetify/pull/8724 it('should fire events in correct order when clear icon is clicked and input is not focused', async () => { const calls: string[] = [] const change = jest.fn(() => calls.push('change')) const blur = jest.fn(() => calls.push('blur')) const focus = jest.fn(() => calls.push('focus')) const input = jest.fn(() => calls.push('input')) const component = { render (h) { return h(VTextField, { on: { change, blur, focus, input, }, props: { value: 'test', clearable: true, }, }) }, } const wrapper = mount(component) const inputElement = wrapper.findAll('input').at(0) const clearIcon = wrapper.find('.v-input__icon--clear .v-icon') clearIcon.trigger('click') await wrapper.vm.$nextTick() inputElement.trigger('blur') await wrapper.vm.$nextTick() expect(calls).toEqual([ 'focus', 'input', 'change', 'blur', ]) expect(inputElement.element.value).toBe('') }) // https://material.io/components/text-fields/#filled-text-field it('should be single if using the filled prop with no label', () => { const wrapper = mountFunction({ propsData: { filled: true }, }) expect(wrapper.vm.isSingle).toBe(true) wrapper.setProps({ label: 'Foobar ' }) expect(wrapper.vm.isSingle).toBe(false) }) it('should autofocus text-field when intersected', async () => { const wrapper = mountFunction({ propsData: { autofocus: true }, }) const input = wrapper.find('input') const element = input.element as HTMLInputElement expect(document.activeElement === element).toBe(true) element.blur() expect(document.activeElement === element).toBe(false) // Simulate observe firing that is visible wrapper.vm.onObserve([], [], true) expect(document.activeElement === element).toBe(true) element.blur() // Simulate observe firing that is not visible wrapper.vm.onObserve([], [], false) expect(document.activeElement === element).toBe(false) element.blur() wrapper.setProps({ autofocus: false }) // Simulate observe firing with no autofocus wrapper.vm.onObserve([], [], true) expect(document.activeElement === element).toBe(false) }) it('should use the correct icon color when using the solo inverted prop', () => { const wrapper = mountFunction({ propsData: { soloInverted: true }, mocks: { $vuetify: { theme: { dark: false }, }, }, provide: { theme: { isDark: true }, }, }) expect(wrapper.vm.computedColor).toBe('white') wrapper.vm.focus() expect(wrapper.vm.computedColor).toBe('primary') }) it('should keep -0 in input when type is number', async () => { const wrapper = mountFunction({ propsData: { type: 'number', value: -0 }, }) const input = wrapper.find('input') expect(input.element.value).toBe('-0') }) })