import { mount } from '@vue/test-utils'; import { nextTick } from 'vue'; import ChronicDurationInput from '~/vue_shared/components/chronic_duration_input.vue'; const MOCK_VALUE = 2 * 3600 + 20 * 60; describe('vue_shared/components/chronic_duration_input', () => { let wrapper; let textElement; let hiddenElement; afterEach(() => { wrapper.destroy(); wrapper = null; textElement = null; hiddenElement = null; }); const findComponents = () => { textElement = wrapper.find('input[type=text]').element; hiddenElement = wrapper.find('input[type=hidden]').element; }; const createComponent = (props = {}) => { if (wrapper) { throw new Error('There should only be one wrapper created per test'); } wrapper = mount(ChronicDurationInput, { propsData: props }); findComponents(); }; describe('value', () => { it('has human-readable output with value', () => { createComponent({ value: MOCK_VALUE }); expect(textElement.value).toBe('2 hrs 20 mins'); expect(hiddenElement.value).toBe(MOCK_VALUE.toString()); }); it('has empty output with no value', () => { createComponent({ value: null }); expect(textElement.value).toBe(''); expect(hiddenElement.value).toBe(''); }); }); describe('change', () => { const createAndDispatch = async (initialValue, humanReadableInput) => { createComponent({ value: initialValue }); await nextTick(); textElement.value = humanReadableInput; textElement.dispatchEvent(new Event('input')); }; describe('when starting with no value and receiving human-readable input', () => { beforeEach(() => { createAndDispatch(null, '2hr20min'); }); it('updates hidden field', () => { expect(textElement.value).toBe('2hr20min'); expect(hiddenElement.value).toBe(MOCK_VALUE.toString()); }); it('emits change event', () => { expect(wrapper.emitted('change')).toEqual([[MOCK_VALUE]]); }); }); describe('when starting with a value and receiving empty input', () => { beforeEach(() => { createAndDispatch(MOCK_VALUE, ''); }); it('updates hidden field', () => { expect(textElement.value).toBe(''); expect(hiddenElement.value).toBe(''); }); it('emits change event', () => { expect(wrapper.emitted('change')).toEqual([[null]]); }); }); describe('when starting with a value and receiving invalid input', () => { beforeEach(() => { createAndDispatch(MOCK_VALUE, 'gobbledygook'); }); it('does not update hidden field', () => { expect(textElement.value).toBe('gobbledygook'); expect(hiddenElement.value).toBe(MOCK_VALUE.toString()); }); it('does not emit change event', () => { expect(wrapper.emitted('change')).toBeUndefined(); }); }); }); describe('valid', () => { describe('initial value', () => { beforeEach(() => { createComponent({ value: MOCK_VALUE }); }); it('emits valid with initial value', () => { expect(wrapper.emitted('valid')).toEqual([[{ valid: true, feedback: '' }]]); expect(textElement.validity.valid).toBe(true); expect(textElement.validity.customError).toBe(false); expect(textElement.validationMessage).toBe(''); expect(hiddenElement.validity.valid).toBe(true); expect(hiddenElement.validity.customError).toBe(false); expect(hiddenElement.validationMessage).toBe(''); }); it('emits valid with user input', async () => { textElement.value = '1m10s'; textElement.dispatchEvent(new Event('input')); await nextTick(); expect(wrapper.emitted('valid')).toEqual([ [{ valid: true, feedback: '' }], [{ valid: true, feedback: '' }], ]); expect(textElement.validity.valid).toBe(true); expect(textElement.validity.customError).toBe(false); expect(textElement.validationMessage).toBe(''); expect(hiddenElement.validity.valid).toBe(true); expect(hiddenElement.validity.customError).toBe(false); expect(hiddenElement.validationMessage).toBe(''); textElement.value = ''; textElement.dispatchEvent(new Event('input')); await nextTick(); expect(wrapper.emitted('valid')).toEqual([ [{ valid: true, feedback: '' }], [{ valid: true, feedback: '' }], [{ valid: null, feedback: '' }], ]); expect(textElement.validity.valid).toBe(true); expect(textElement.validity.customError).toBe(false); expect(textElement.validationMessage).toBe(''); expect(hiddenElement.validity.valid).toBe(true); expect(hiddenElement.validity.customError).toBe(false); expect(hiddenElement.validationMessage).toBe(''); }); it('emits invalid with user input', async () => { textElement.value = 'gobbledygook'; textElement.dispatchEvent(new Event('input')); await nextTick(); expect(wrapper.emitted('valid')).toEqual([ [{ valid: true, feedback: '' }], [{ valid: false, feedback: ChronicDurationInput.i18n.INVALID_INPUT_FEEDBACK }], ]); expect(textElement.validity.valid).toBe(false); expect(textElement.validity.customError).toBe(true); expect(textElement.validationMessage).toBe( ChronicDurationInput.i18n.INVALID_INPUT_FEEDBACK, ); expect(hiddenElement.validity.valid).toBe(false); expect(hiddenElement.validity.customError).toBe(true); // Hidden elements do not have validationMessage expect(hiddenElement.validationMessage).toBe(''); }); }); describe('no initial value', () => { beforeEach(() => { createComponent({ value: null }); }); it('emits valid with no initial value', () => { expect(wrapper.emitted('valid')).toEqual([[{ valid: null, feedback: '' }]]); expect(textElement.validity.valid).toBe(true); expect(textElement.validity.customError).toBe(false); expect(textElement.validationMessage).toBe(''); expect(hiddenElement.validity.valid).toBe(true); expect(hiddenElement.validity.customError).toBe(false); expect(hiddenElement.validationMessage).toBe(''); }); it('emits valid with updated value', async () => { wrapper.setProps({ value: MOCK_VALUE }); await nextTick(); expect(wrapper.emitted('valid')).toEqual([ [{ valid: null, feedback: '' }], [{ valid: true, feedback: '' }], ]); expect(textElement.validity.valid).toBe(true); expect(textElement.validity.customError).toBe(false); expect(textElement.validationMessage).toBe(''); expect(hiddenElement.validity.valid).toBe(true); expect(hiddenElement.validity.customError).toBe(false); expect(hiddenElement.validationMessage).toBe(''); }); }); describe('decimal input', () => { describe('when integerRequired is false', () => { beforeEach(() => { createComponent({ value: null, integerRequired: false }); }); it('emits valid when input is integer', async () => { textElement.value = '2hr20min'; textElement.dispatchEvent(new Event('input')); await nextTick(); expect(wrapper.emitted('change')).toEqual([[MOCK_VALUE]]); expect(wrapper.emitted('valid')).toEqual([ [{ valid: null, feedback: '' }], [{ valid: true, feedback: '' }], ]); expect(textElement.validity.valid).toBe(true); expect(textElement.validity.customError).toBe(false); expect(textElement.validationMessage).toBe(''); expect(hiddenElement.validity.valid).toBe(true); expect(hiddenElement.validity.customError).toBe(false); expect(hiddenElement.validationMessage).toBe(''); }); it('emits valid when input is decimal', async () => { textElement.value = '1.5s'; textElement.dispatchEvent(new Event('input')); await nextTick(); expect(wrapper.emitted('change')).toEqual([[1.5]]); expect(wrapper.emitted('valid')).toEqual([ [{ valid: null, feedback: '' }], [{ valid: true, feedback: '' }], ]); expect(textElement.validity.valid).toBe(true); expect(textElement.validity.customError).toBe(false); expect(textElement.validationMessage).toBe(''); expect(hiddenElement.validity.valid).toBe(true); expect(hiddenElement.validity.customError).toBe(false); expect(hiddenElement.validationMessage).toBe(''); }); }); describe('when integerRequired is unspecified', () => { beforeEach(() => { createComponent({ value: null }); }); it('emits valid when input is integer', async () => { textElement.value = '2hr20min'; textElement.dispatchEvent(new Event('input')); await nextTick(); expect(wrapper.emitted('change')).toEqual([[MOCK_VALUE]]); expect(wrapper.emitted('valid')).toEqual([ [{ valid: null, feedback: '' }], [{ valid: true, feedback: '' }], ]); expect(textElement.validity.valid).toBe(true); expect(textElement.validity.customError).toBe(false); expect(textElement.validationMessage).toBe(''); expect(hiddenElement.validity.valid).toBe(true); expect(hiddenElement.validity.customError).toBe(false); expect(hiddenElement.validationMessage).toBe(''); }); it('emits invalid when input is decimal', async () => { textElement.value = '1.5s'; textElement.dispatchEvent(new Event('input')); await nextTick(); expect(wrapper.emitted('change')).toBeUndefined(); expect(wrapper.emitted('valid')).toEqual([ [{ valid: null, feedback: '' }], [ { valid: false, feedback: ChronicDurationInput.i18n.INVALID_DECIMAL_FEEDBACK, }, ], ]); expect(textElement.validity.valid).toBe(false); expect(textElement.validity.customError).toBe(true); expect(textElement.validationMessage).toBe( ChronicDurationInput.i18n.INVALID_DECIMAL_FEEDBACK, ); expect(hiddenElement.validity.valid).toBe(false); expect(hiddenElement.validity.customError).toBe(true); // Hidden elements do not have validationMessage expect(hiddenElement.validationMessage).toBe(''); }); }); }); }); describe('v-model', () => { beforeEach(() => { wrapper = mount({ data() { return { value: 1 * 60 + 10 }; }, components: { ChronicDurationInput }, template: '
', }); findComponents(); }); describe('value', () => { it('passes initial prop via v-model', () => { expect(textElement.value).toBe('1 min 10 secs'); expect(hiddenElement.value).toBe((1 * 60 + 10).toString()); }); it('passes updated prop via v-model', async () => { // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details // eslint-disable-next-line no-restricted-syntax wrapper.setData({ value: MOCK_VALUE }); await nextTick(); expect(textElement.value).toBe('2 hrs 20 mins'); expect(hiddenElement.value).toBe(MOCK_VALUE.toString()); }); }); describe('change', () => { it('passes user input to parent via v-model', async () => { textElement.value = '2hr20min'; textElement.dispatchEvent(new Event('input')); await nextTick(); expect(wrapper.findComponent(ChronicDurationInput).props('value')).toBe(MOCK_VALUE); expect(textElement.value).toBe('2hr20min'); expect(hiddenElement.value).toBe(MOCK_VALUE.toString()); }); }); }); describe('name', () => { beforeEach(() => { createComponent({ name: 'myInput' }); }); it('sets name of hidden field', () => { expect(hiddenElement.name).toBe('myInput'); }); it('does not set name of text field', () => { expect(textElement.name).toBe(''); }); }); describe('form submission', () => { beforeEach(() => { wrapper = mount({ template: `
`, components: { ChronicDurationInput, }, }); findComponents(); }); it('creates form data with initial value', () => { const formData = new FormData(wrapper.find('[data-testid=myForm]').element); const iter = formData.entries(); expect(iter.next()).toEqual({ value: ['myInput', MOCK_VALUE.toString()], done: false, }); expect(iter.next()).toEqual({ value: undefined, done: true }); }); it('creates form data with user-specified value', async () => { textElement.value = '1m10s'; textElement.dispatchEvent(new Event('input')); await nextTick(); const formData = new FormData(wrapper.find('[data-testid=myForm]').element); const iter = formData.entries(); expect(iter.next()).toEqual({ value: ['myInput', (1 * 60 + 10).toString()], done: false, }); expect(iter.next()).toEqual({ value: undefined, done: true }); }); }); });