diff options
Diffstat (limited to 'spec/frontend/vue_shared/components')
69 files changed, 1309 insertions, 1837 deletions
diff --git a/spec/frontend/vue_shared/components/blob_viewers/__snapshots__/simple_viewer_spec.js.snap b/spec/frontend/vue_shared/components/blob_viewers/__snapshots__/simple_viewer_spec.js.snap index 06753044e93..fbf3d17fd64 100644 --- a/spec/frontend/vue_shared/components/blob_viewers/__snapshots__/simple_viewer_spec.js.snap +++ b/spec/frontend/vue_shared/components/blob_viewers/__snapshots__/simple_viewer_spec.js.snap @@ -6,7 +6,7 @@ exports[`Blob Simple Viewer component rendering matches the snapshot 1`] = ` class="file-content code js-syntax-highlight" > <div - class="line-numbers" + class="line-numbers gl-pt-0!" > <a class="diff-line-num js-line-number" @@ -56,7 +56,7 @@ exports[`Blob Simple Viewer component rendering matches the snapshot 1`] = ` class="blob-content" > <pre - class="code highlight" + class="code highlight gl-p-0! gl-display-flex" > <code data-blob-hash="foo-bar" diff --git a/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js b/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js index 3277aab43f0..663ebd3e12f 100644 --- a/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js +++ b/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js @@ -1,16 +1,21 @@ import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import { HIGHLIGHT_CLASS_NAME } from '~/vue_shared/components/blob_viewers/constants'; import SimpleViewer from '~/vue_shared/components/blob_viewers/simple_viewer.vue'; +import LineHighlighter from '~/blob/line_highlighter'; + +jest.mock('~/blob/line_highlighter'); describe('Blob Simple Viewer component', () => { let wrapper; const contentMock = `<span id="LC1">First</span>\n<span id="LC2">Second</span>\n<span id="LC3">Third</span>`; const blobHash = 'foo-bar'; - function createComponent(content = contentMock, isRawContent = false) { + function createComponent(content = contentMock, isRawContent = false, glFeatures = {}) { wrapper = shallowMount(SimpleViewer, { provide: { blobHash, + glFeatures, }, propsData: { content, @@ -25,6 +30,20 @@ describe('Blob Simple Viewer component', () => { wrapper.destroy(); }); + describe('refactorBlobViewer feature flag', () => { + it('loads the LineHighlighter if refactorBlobViewer is enabled', () => { + createComponent('', false, { refactorBlobViewer: true }); + + expect(LineHighlighter).toHaveBeenCalled(); + }); + + it('does not load the LineHighlighter if refactorBlobViewer is disabled', () => { + createComponent('', false, { refactorBlobViewer: false }); + + expect(LineHighlighter).not.toHaveBeenCalled(); + }); + }); + it('does not fail if content is empty', () => { const spy = jest.spyOn(window.console, 'error'); createComponent(''); @@ -69,7 +88,7 @@ describe('Blob Simple Viewer component', () => { expect(linetoBeHighlighted.classes()).toContain(HIGHLIGHT_CLASS_NAME); }); - it('switches highlighting when another line is selected', () => { + it('switches highlighting when another line is selected', async () => { const currentlyHighlighted = wrapper.find('#LC2'); const hash = '#LC3'; const linetoBeHighlighted = wrapper.find(hash); @@ -78,11 +97,10 @@ describe('Blob Simple Viewer component', () => { wrapper.vm.scrollToLine(hash); - return wrapper.vm.$nextTick(() => { - expect(wrapper.vm.highlightedLine).toBe(linetoBeHighlighted.element); - expect(currentlyHighlighted.classes()).not.toContain(HIGHLIGHT_CLASS_NAME); - expect(linetoBeHighlighted.classes()).toContain(HIGHLIGHT_CLASS_NAME); - }); + await nextTick(); + expect(wrapper.vm.highlightedLine).toBe(linetoBeHighlighted.element); + expect(currentlyHighlighted.classes()).not.toContain(HIGHLIGHT_CLASS_NAME); + expect(linetoBeHighlighted.classes()).toContain(HIGHLIGHT_CLASS_NAME); }); }); }); diff --git a/spec/frontend/vue_shared/components/chronic_duration_input_spec.js b/spec/frontend/vue_shared/components/chronic_duration_input_spec.js index 083a5f60d1d..6932a812287 100644 --- a/spec/frontend/vue_shared/components/chronic_duration_input_spec.js +++ b/spec/frontend/vue_shared/components/chronic_duration_input_spec.js @@ -1,4 +1,5 @@ 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; @@ -48,7 +49,7 @@ describe('vue_shared/components/chronic_duration_input', () => { describe('change', () => { const createAndDispatch = async (initialValue, humanReadableInput) => { createComponent({ value: initialValue }); - await wrapper.vm.$nextTick(); + await nextTick(); textElement.value = humanReadableInput; textElement.dispatchEvent(new Event('input')); }; @@ -118,7 +119,7 @@ describe('vue_shared/components/chronic_duration_input', () => { it('emits valid with user input', async () => { textElement.value = '1m10s'; textElement.dispatchEvent(new Event('input')); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.emitted('valid')).toEqual([ [{ valid: true, feedback: '' }], @@ -133,7 +134,7 @@ describe('vue_shared/components/chronic_duration_input', () => { textElement.value = ''; textElement.dispatchEvent(new Event('input')); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.emitted('valid')).toEqual([ [{ valid: true, feedback: '' }], @@ -151,7 +152,7 @@ describe('vue_shared/components/chronic_duration_input', () => { it('emits invalid with user input', async () => { textElement.value = 'gobbledygook'; textElement.dispatchEvent(new Event('input')); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.emitted('valid')).toEqual([ [{ valid: true, feedback: '' }], @@ -186,7 +187,7 @@ describe('vue_shared/components/chronic_duration_input', () => { it('emits valid with updated value', async () => { wrapper.setProps({ value: MOCK_VALUE }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.emitted('valid')).toEqual([ [{ valid: null, feedback: '' }], @@ -210,7 +211,7 @@ describe('vue_shared/components/chronic_duration_input', () => { it('emits valid when input is integer', async () => { textElement.value = '2hr20min'; textElement.dispatchEvent(new Event('input')); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.emitted('change')).toEqual([[MOCK_VALUE]]); expect(wrapper.emitted('valid')).toEqual([ @@ -228,7 +229,7 @@ describe('vue_shared/components/chronic_duration_input', () => { it('emits valid when input is decimal', async () => { textElement.value = '1.5s'; textElement.dispatchEvent(new Event('input')); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.emitted('change')).toEqual([[1.5]]); expect(wrapper.emitted('valid')).toEqual([ @@ -252,7 +253,7 @@ describe('vue_shared/components/chronic_duration_input', () => { it('emits valid when input is integer', async () => { textElement.value = '2hr20min'; textElement.dispatchEvent(new Event('input')); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.emitted('change')).toEqual([[MOCK_VALUE]]); expect(wrapper.emitted('valid')).toEqual([ @@ -270,7 +271,7 @@ describe('vue_shared/components/chronic_duration_input', () => { it('emits invalid when input is decimal', async () => { textElement.value = '1.5s'; textElement.dispatchEvent(new Event('input')); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.emitted('change')).toBeUndefined(); expect(wrapper.emitted('valid')).toEqual([ @@ -318,7 +319,7 @@ describe('vue_shared/components/chronic_duration_input', () => { // 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 wrapper.vm.$nextTick(); + await nextTick(); expect(textElement.value).toBe('2 hrs 20 mins'); expect(hiddenElement.value).toBe(MOCK_VALUE.toString()); @@ -329,7 +330,7 @@ describe('vue_shared/components/chronic_duration_input', () => { it('passes user input to parent via v-model', async () => { textElement.value = '2hr20min'; textElement.dispatchEvent(new Event('input')); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.findComponent(ChronicDurationInput).props('value')).toBe(MOCK_VALUE); expect(textElement.value).toBe('2hr20min'); @@ -377,7 +378,7 @@ describe('vue_shared/components/chronic_duration_input', () => { it('creates form data with user-specified value', async () => { textElement.value = '1m10s'; textElement.dispatchEvent(new Event('input')); - await wrapper.vm.$nextTick(); + await nextTick(); const formData = new FormData(wrapper.find('[data-testid=myForm]').element); const iter = formData.entries(); diff --git a/spec/frontend/vue_shared/components/confirm_fork_modal_spec.js b/spec/frontend/vue_shared/components/confirm_fork_modal_spec.js new file mode 100644 index 00000000000..1cde92cf522 --- /dev/null +++ b/spec/frontend/vue_shared/components/confirm_fork_modal_spec.js @@ -0,0 +1,80 @@ +import { GlModal } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import ConfirmForkModal, { i18n } from '~/vue_shared/components/confirm_fork_modal.vue'; + +describe('vue_shared/components/confirm_fork_modal', () => { + let wrapper = null; + + const forkPath = '/fake/fork/path'; + const modalId = 'confirm-fork-modal'; + const defaultProps = { modalId, forkPath }; + + const findModal = () => wrapper.findComponent(GlModal); + const findModalProp = (prop) => findModal().props(prop); + const findModalActionProps = () => findModalProp('actionPrimary'); + + const createComponent = (props = {}) => + shallowMountExtended(ConfirmForkModal, { + propsData: { + ...defaultProps, + ...props, + }, + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('visible = false', () => { + beforeEach(() => { + wrapper = createComponent(); + }); + + it('sets the visible prop to `false`', () => { + expect(findModalProp('visible')).toBe(false); + }); + + it('sets the modal title', () => { + const title = findModalProp('title'); + expect(title).toBe(i18n.title); + }); + + it('sets the modal id', () => { + const fakeModalId = findModalProp('modalId'); + expect(fakeModalId).toBe(modalId); + }); + + it('has the fork path button', () => { + const modalProps = findModalActionProps(); + expect(modalProps.text).toBe(i18n.btnText); + expect(modalProps.attributes.variant).toBe('confirm'); + }); + + it('sets the correct fork path', () => { + const modalProps = findModalActionProps(); + expect(modalProps.attributes.href).toBe(forkPath); + }); + + it('has the fork message', () => { + expect(findModal().text()).toContain(i18n.message); + }); + }); + + describe('visible = true', () => { + beforeEach(() => { + wrapper = createComponent({ visible: true }); + }); + + it('sets the visible prop to `true`', () => { + expect(findModalProp('visible')).toBe(true); + }); + + it('emits the `change` event if the modal is hidden', () => { + expect(wrapper.emitted('change')).toBeUndefined(); + + findModal().vm.$emit('change', false); + + expect(wrapper.emitted('change')).toEqual([[false]]); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js index 33667a1bb71..d4b6b987c69 100644 --- a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js +++ b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js @@ -1,5 +1,6 @@ import { mount } from '@vue/test-utils'; import timezoneMock from 'timezone-mock'; +import { nextTick } from 'vue'; import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue'; import { defaultTimeRanges, @@ -29,26 +30,23 @@ describe('DateTimePicker', () => { wrapper.destroy(); }); - it('renders dropdown toggle button with selected text', () => { + it('renders dropdown toggle button with selected text', async () => { createComponent(); - return wrapper.vm.$nextTick(() => { - expect(dropdownToggle().text()).toBe(defaultTimeRange.label); - }); + await nextTick(); + expect(dropdownToggle().text()).toBe(defaultTimeRange.label); }); - it('renders dropdown toggle button with selected text and utc label', () => { + it('renders dropdown toggle button with selected text and utc label', async () => { createComponent({ utc: true }); - return wrapper.vm.$nextTick(() => { - expect(dropdownToggle().text()).toContain(defaultTimeRange.label); - expect(dropdownToggle().text()).toContain('UTC'); - }); + await nextTick(); + expect(dropdownToggle().text()).toContain(defaultTimeRange.label); + expect(dropdownToggle().text()).toContain('UTC'); }); - it('renders dropdown with 2 custom time range inputs', () => { + it('renders dropdown with 2 custom time range inputs', async () => { createComponent(); - return wrapper.vm.$nextTick(() => { - expect(wrapper.findAll('input').length).toBe(2); - }); + await nextTick(); + expect(wrapper.findAll('input').length).toBe(2); }); describe('renders label with h/m/s truncated if possible', () => { @@ -80,33 +78,30 @@ describe('DateTimePicker', () => { label: '2019-10-10 00:00:01 to 2019-10-10 00:00:01 UTC', }, ].forEach(({ start, end, utc, label }) => { - it(`for start ${start}, end ${end}, and utc ${utc}, label is ${label}`, () => { + it(`for start ${start}, end ${end}, and utc ${utc}, label is ${label}`, async () => { createComponent({ value: { start, end }, utc, }); - return wrapper.vm.$nextTick(() => { - expect(dropdownToggle().text()).toBe(label); - }); + await nextTick(); + expect(dropdownToggle().text()).toBe(label); }); }); }); - it(`renders dropdown with ${optionsCount} (default) items in quick range`, () => { + it(`renders dropdown with ${optionsCount} (default) items in quick range`, async () => { createComponent(); dropdownToggle().trigger('click'); - return wrapper.vm.$nextTick(() => { - expect(findQuickRangeItems().length).toBe(optionsCount); - }); + await nextTick(); + expect(findQuickRangeItems().length).toBe(optionsCount); }); - it('renders dropdown with a default quick range item selected', () => { + it('renders dropdown with a default quick range item selected', async () => { createComponent(); dropdownToggle().trigger('click'); - return wrapper.vm.$nextTick(() => { - expect(wrapper.find('.dropdown-item.active').exists()).toBe(true); - expect(wrapper.find('.dropdown-item.active').text()).toBe(defaultTimeRange.label); - }); + await nextTick(); + expect(wrapper.find('.dropdown-item.active').exists()).toBe(true); + expect(wrapper.find('.dropdown-item.active').text()).toBe(defaultTimeRange.label); }); it('renders a disabled apply button on wrong input', () => { @@ -118,74 +113,63 @@ describe('DateTimePicker', () => { }); describe('user input', () => { - const fillInputAndBlur = (input, val) => { + const fillInputAndBlur = async (input, val) => { wrapper.find(input).setValue(val); - return wrapper.vm.$nextTick().then(() => { - wrapper.find(input).trigger('blur'); - return wrapper.vm.$nextTick(); - }); + await nextTick(); + wrapper.find(input).trigger('blur'); + await nextTick(); }; - beforeEach(() => { + beforeEach(async () => { createComponent(); - return wrapper.vm.$nextTick(); + await nextTick(); }); - it('displays inline error message if custom time range inputs are invalid', () => { - return fillInputAndBlur('#custom-time-from', '2019-10-01abc') - .then(() => fillInputAndBlur('#custom-time-to', '2019-10-10abc')) - .then(() => { - expect(wrapper.findAll('.invalid-feedback').length).toBe(2); - }); + it('displays inline error message if custom time range inputs are invalid', async () => { + await fillInputAndBlur('#custom-time-from', '2019-10-01abc'); + await fillInputAndBlur('#custom-time-to', '2019-10-10abc'); + expect(wrapper.findAll('.invalid-feedback').length).toBe(2); }); - it('keeps apply button disabled with invalid custom time range inputs', () => { - return fillInputAndBlur('#custom-time-from', '2019-10-01abc') - .then(() => fillInputAndBlur('#custom-time-to', '2019-09-19')) - .then(() => { - expect(applyButtonElement().getAttribute('disabled')).toBe('disabled'); - }); + it('keeps apply button disabled with invalid custom time range inputs', async () => { + await fillInputAndBlur('#custom-time-from', '2019-10-01abc'); + await fillInputAndBlur('#custom-time-to', '2019-09-19'); + expect(applyButtonElement().getAttribute('disabled')).toBe('disabled'); }); - it('enables apply button with valid custom time range inputs', () => { - return fillInputAndBlur('#custom-time-from', '2019-10-01') - .then(() => fillInputAndBlur('#custom-time-to', '2019-10-19')) - .then(() => { - expect(applyButtonElement().getAttribute('disabled')).toBeNull(); - }); + it('enables apply button with valid custom time range inputs', async () => { + await fillInputAndBlur('#custom-time-from', '2019-10-01'); + await fillInputAndBlur('#custom-time-to', '2019-10-19'); + expect(applyButtonElement().getAttribute('disabled')).toBeNull(); }); describe('when "apply" is clicked', () => { - it('emits iso dates', () => { - return fillInputAndBlur('#custom-time-from', '2019-10-01 00:00:00') - .then(() => fillInputAndBlur('#custom-time-to', '2019-10-19 00:00:00')) - .then(() => { - applyButtonElement().click(); - - expect(wrapper.emitted().input).toHaveLength(1); - expect(wrapper.emitted().input[0]).toEqual([ - { - end: '2019-10-19T00:00:00Z', - start: '2019-10-01T00:00:00Z', - }, - ]); - }); + it('emits iso dates', async () => { + await fillInputAndBlur('#custom-time-from', '2019-10-01 00:00:00'); + await fillInputAndBlur('#custom-time-to', '2019-10-19 00:00:00'); + applyButtonElement().click(); + + expect(wrapper.emitted().input).toHaveLength(1); + expect(wrapper.emitted().input[0]).toEqual([ + { + end: '2019-10-19T00:00:00Z', + start: '2019-10-01T00:00:00Z', + }, + ]); }); - it('emits iso dates, for dates without time of day', () => { - return fillInputAndBlur('#custom-time-from', '2019-10-01') - .then(() => fillInputAndBlur('#custom-time-to', '2019-10-19')) - .then(() => { - applyButtonElement().click(); - - expect(wrapper.emitted().input).toHaveLength(1); - expect(wrapper.emitted().input[0]).toEqual([ - { - end: '2019-10-19T00:00:00Z', - start: '2019-10-01T00:00:00Z', - }, - ]); - }); + it('emits iso dates, for dates without time of day', async () => { + await fillInputAndBlur('#custom-time-from', '2019-10-01'); + await fillInputAndBlur('#custom-time-to', '2019-10-19'); + applyButtonElement().click(); + + expect(wrapper.emitted().input).toHaveLength(1); + expect(wrapper.emitted().input[0]).toEqual([ + { + end: '2019-10-19T00:00:00Z', + start: '2019-10-01T00:00:00Z', + }, + ]); }); describe('when timezone is different', () => { @@ -196,52 +180,46 @@ describe('DateTimePicker', () => { timezoneMock.unregister(); }); - it('emits iso dates', () => { - return fillInputAndBlur('#custom-time-from', '2019-10-01 00:00:00') - .then(() => fillInputAndBlur('#custom-time-to', '2019-10-19 12:00:00')) - .then(() => { - applyButtonElement().click(); - - expect(wrapper.emitted().input).toHaveLength(1); - expect(wrapper.emitted().input[0]).toEqual([ - { - start: '2019-10-01T07:00:00Z', - end: '2019-10-19T19:00:00Z', - }, - ]); - }); + it('emits iso dates', async () => { + await fillInputAndBlur('#custom-time-from', '2019-10-01 00:00:00'); + await fillInputAndBlur('#custom-time-to', '2019-10-19 12:00:00'); + applyButtonElement().click(); + + expect(wrapper.emitted().input).toHaveLength(1); + expect(wrapper.emitted().input[0]).toEqual([ + { + start: '2019-10-01T07:00:00Z', + end: '2019-10-19T19:00:00Z', + }, + ]); }); - it('emits iso dates with utc format', () => { + it('emits iso dates with utc format', async () => { wrapper.setProps({ utc: true }); - return wrapper.vm - .$nextTick() - .then(() => fillInputAndBlur('#custom-time-from', '2019-10-01 00:00:00')) - .then(() => fillInputAndBlur('#custom-time-to', '2019-10-19 12:00:00')) - .then(() => { - applyButtonElement().click(); - - expect(wrapper.emitted().input).toHaveLength(1); - expect(wrapper.emitted().input[0]).toEqual([ - { - start: '2019-10-01T00:00:00Z', - end: '2019-10-19T12:00:00Z', - }, - ]); - }); + await nextTick(); + await fillInputAndBlur('#custom-time-from', '2019-10-01 00:00:00'); + await fillInputAndBlur('#custom-time-to', '2019-10-19 12:00:00'); + applyButtonElement().click(); + + expect(wrapper.emitted().input).toHaveLength(1); + expect(wrapper.emitted().input[0]).toEqual([ + { + start: '2019-10-01T00:00:00Z', + end: '2019-10-19T12:00:00Z', + }, + ]); }); }); }); - it('unchecks quick range when text is input is clicked', () => { + it('unchecks quick range when text is input is clicked', async () => { const findActiveItems = () => findQuickRangeItems().filter((w) => w.classes().includes('active')); expect(findActiveItems().length).toBe(1); - return fillInputAndBlur('#custom-time-from', '2019-10-01').then(() => { - expect(findActiveItems().length).toBe(0); - }); + await fillInputAndBlur('#custom-time-from', '2019-10-01'); + expect(findActiveItems().length).toBe(0); }); it('emits dates in an object when a is clicked', () => { @@ -257,16 +235,14 @@ describe('DateTimePicker', () => { }); }); - it('hides the popover with cancel button', () => { + it('hides the popover with cancel button', async () => { dropdownToggle().trigger('click'); - return wrapper.vm.$nextTick(() => { - cancelButton().trigger('click'); + await nextTick(); + cancelButton().trigger('click'); - return wrapper.vm.$nextTick(() => { - expect(dropdownMenu().classes('show')).toBe(false); - }); - }); + await nextTick(); + expect(dropdownMenu().classes('show')).toBe(false); }); }); @@ -293,7 +269,7 @@ describe('DateTimePicker', () => { jest.spyOn(Date, 'now').mockImplementation(() => MOCK_NOW); }); - it('renders dropdown with a label in the quick range', () => { + it('renders dropdown with a label in the quick range', async () => { createComponent({ value: { duration: { seconds: 60 * 5 }, @@ -301,12 +277,11 @@ describe('DateTimePicker', () => { options: otherTimeRanges, }); dropdownToggle().trigger('click'); - return wrapper.vm.$nextTick(() => { - expect(dropdownToggle().text()).toBe('5 minutes'); - }); + await nextTick(); + expect(dropdownToggle().text()).toBe('5 minutes'); }); - it('renders dropdown with a label in the quick range and utc label', () => { + it('renders dropdown with a label in the quick range and utc label', async () => { createComponent({ value: { duration: { seconds: 60 * 5 }, @@ -315,12 +290,11 @@ describe('DateTimePicker', () => { options: otherTimeRanges, }); dropdownToggle().trigger('click'); - return wrapper.vm.$nextTick(() => { - expect(dropdownToggle().text()).toBe('5 minutes UTC'); - }); + await nextTick(); + expect(dropdownToggle().text()).toBe('5 minutes UTC'); }); - it('renders dropdown with quick range items', () => { + it('renders dropdown with quick range items', async () => { createComponent({ value: { duration: { seconds: 60 * 2 }, @@ -328,31 +302,29 @@ describe('DateTimePicker', () => { options: otherTimeRanges, }); dropdownToggle().trigger('click'); - return wrapper.vm.$nextTick(() => { - const items = findQuickRangeItems(); + await nextTick(); + const items = findQuickRangeItems(); - expect(items.length).toBe(Object.keys(otherTimeRanges).length); - expect(items.at(0).text()).toBe('1 minute'); - expect(items.at(0).classes()).not.toContain('active'); + expect(items.length).toBe(Object.keys(otherTimeRanges).length); + expect(items.at(0).text()).toBe('1 minute'); + expect(items.at(0).classes()).not.toContain('active'); - expect(items.at(1).text()).toBe('2 minutes'); - expect(items.at(1).classes()).toContain('active'); + expect(items.at(1).text()).toBe('2 minutes'); + expect(items.at(1).classes()).toContain('active'); - expect(items.at(2).text()).toBe('5 minutes'); - expect(items.at(2).classes()).not.toContain('active'); - }); + expect(items.at(2).text()).toBe('5 minutes'); + expect(items.at(2).classes()).not.toContain('active'); }); - it('renders dropdown with a label not in the quick range', () => { + it('renders dropdown with a label not in the quick range', async () => { createComponent({ value: { duration: { seconds: 60 * 4 }, }, }); dropdownToggle().trigger('click'); - return wrapper.vm.$nextTick(() => { - expect(dropdownToggle().text()).toBe('2020-01-23 19:56:00 to 2020-01-23 20:00:00'); - }); + await nextTick(); + expect(dropdownToggle().text()).toBe('2020-01-23 19:56:00 to 2020-01-23 20:00:00'); }); }); }); diff --git a/spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js b/spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js index b812ced72c9..59653a0ec13 100644 --- a/spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js +++ b/spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js @@ -1,4 +1,5 @@ import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import DeployBoardInstance from '~/vue_shared/components/deployment_instance.vue'; import { folder } from './mock_data'; @@ -28,17 +29,15 @@ describe('Deploy Board Instance', () => { expect(wrapper.attributes('title')).toEqual('This is a pod'); }); - it('should render a div without tooltip data', (done) => { + it('should render a div without tooltip data', async () => { wrapper = createComponent({ status: 'deploying', tooltipText: '', }); - wrapper.vm.$nextTick(() => { - expect(wrapper.classes('deployment-instance-deploying')).toBe(true); - expect(wrapper.attributes('title')).toEqual(''); - done(); - }); + await nextTick(); + expect(wrapper.classes('deployment-instance-deploying')).toBe(true); + expect(wrapper.attributes('title')).toEqual(''); }); it('should have a log path computed with a pod name as a parameter', () => { @@ -58,15 +57,13 @@ describe('Deploy Board Instance', () => { wrapper.destroy(); }); - it('should render a div with canary class when stable prop is provided as false', (done) => { + it('should render a div with canary class when stable prop is provided as false', async () => { wrapper = createComponent({ stable: false, }); - wrapper.vm.$nextTick(() => { - expect(wrapper.classes('deployment-instance-canary')).toBe(true); - done(); - }); + await nextTick(); + expect(wrapper.classes('deployment-instance-canary')).toBe(true); }); }); @@ -75,17 +72,15 @@ describe('Deploy Board Instance', () => { wrapper.destroy(); }); - it('should not be a link without a logsPath prop', (done) => { + it('should not be a link without a logsPath prop', async () => { wrapper = createComponent({ stable: false, logsPath: '', }); - wrapper.vm.$nextTick(() => { - expect(wrapper.vm.computedLogPath).toBeNull(); - expect(wrapper.vm.isLink).toBeFalsy(); - done(); - }); + await nextTick(); + expect(wrapper.vm.computedLogPath).toBeNull(); + expect(wrapper.vm.isLink).toBeFalsy(); }); it('should render a link without href if path is not passed', () => { diff --git a/spec/frontend/vue_shared/components/design_management/design_note_pin_spec.js b/spec/frontend/vue_shared/components/design_management/design_note_pin_spec.js index 984a28c93d6..353d493add9 100644 --- a/spec/frontend/vue_shared/components/design_management/design_note_pin_spec.js +++ b/spec/frontend/vue_shared/components/design_management/design_note_pin_spec.js @@ -39,4 +39,72 @@ describe('Design note pin component', () => { createComponent({ position: null }); expect(wrapper.element).toMatchSnapshot(); }); + + it('applies `on-image` class when isOnImage is true', () => { + createComponent({ isOnImage: true }); + + expect(wrapper.find('.on-image').exists()).toBe(true); + }); + + it('applies `draft` class when isDraft is true', () => { + createComponent({ isDraft: true }); + + expect(wrapper.find('.draft').exists()).toBe(true); + }); + + describe('size', () => { + it('is `sm` it applies `small` class', () => { + createComponent({ size: 'sm' }); + expect(wrapper.find('.small').exists()).toBe(true); + }); + + it('is `md` it applies no size class', () => { + createComponent({ size: 'md' }); + expect(wrapper.find('.small').exists()).toBe(false); + expect(wrapper.find('.medium').exists()).toBe(false); + }); + + it('throws when passed any other value except `sm` or `md`', () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + + createComponent({ size: 'lg' }); + + // eslint-disable-next-line no-console + expect(console.error).toHaveBeenCalled(); + }); + }); + + describe('ariaLabel', () => { + describe('when value is passed', () => { + it('overrides default aria-label', () => { + const ariaLabel = 'Aria Label'; + + createComponent({ ariaLabel }); + + const button = wrapper.find('button'); + + expect(button.attributes('aria-label')).toBe(ariaLabel); + }); + }); + + describe('when no value is passed', () => { + it('shows new note label as aria-label when label is absent', () => { + createComponent({ label: null }); + + const button = wrapper.find('button'); + + expect(button.attributes('aria-label')).toBe('Comment form position'); + }); + + it('shows label position as aria-label when label is present', () => { + const label = 1; + + createComponent({ label, isNewNote: false }); + + const button = wrapper.find('button'); + + expect(button.attributes('aria-label')).toBe(`Comment '${label}' position`); + }); + }); + }); }); diff --git a/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js b/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js index 68e3ee11a0d..69964b2687d 100644 --- a/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js +++ b/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js @@ -1,4 +1,4 @@ -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import mountComponent from 'helpers/vue_mount_component_helper'; import { GREEN_BOX_IMAGE_URL, RED_BOX_IMAGE_URL } from 'spec/test_constants'; @@ -26,27 +26,25 @@ describe('DiffViewer', () => { vm.$destroy(); }); - it('renders image diff', (done) => { + it('renders image diff', async () => { window.gon = { relative_url_root: '', }; createComponent({ ...requiredProps, projectPath: '' }); - setImmediate(() => { - expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe( - `//-/raw/DEF/${RED_BOX_IMAGE_URL}`, - ); + await nextTick(); - expect(vm.$el.querySelector('.added img').getAttribute('src')).toBe( - `//-/raw/ABC/${GREEN_BOX_IMAGE_URL}`, - ); + expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe( + `//-/raw/DEF/${RED_BOX_IMAGE_URL}`, + ); - done(); - }); + expect(vm.$el.querySelector('.added img').getAttribute('src')).toBe( + `//-/raw/ABC/${GREEN_BOX_IMAGE_URL}`, + ); }); - it('renders fallback download diff display', (done) => { + it('renders fallback download diff display', async () => { createComponent({ ...requiredProps, diffViewerMode: 'added', @@ -54,22 +52,18 @@ describe('DiffViewer', () => { oldPath: 'testold.abc', }); - setImmediate(() => { - expect(vm.$el.querySelector('.deleted .file-info').textContent.trim()).toContain( - 'testold.abc', - ); + await nextTick(); - expect(vm.$el.querySelector('.deleted .btn.btn-default').textContent.trim()).toContain( - 'Download', - ); + expect(vm.$el.querySelector('.deleted .file-info').textContent.trim()).toContain('testold.abc'); - expect(vm.$el.querySelector('.added .file-info').textContent.trim()).toContain('test.abc'); - expect(vm.$el.querySelector('.added .btn.btn-default').textContent.trim()).toContain( - 'Download', - ); + expect(vm.$el.querySelector('.deleted .btn.btn-default').textContent.trim()).toContain( + 'Download', + ); - done(); - }); + expect(vm.$el.querySelector('.added .file-info').textContent.trim()).toContain('test.abc'); + expect(vm.$el.querySelector('.added .btn.btn-default').textContent.trim()).toContain( + 'Download', + ); }); describe('renamed file', () => { diff --git a/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js b/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js index 8deb466b33c..d0fa8b8dacb 100644 --- a/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js +++ b/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js @@ -1,5 +1,5 @@ import { mount } from '@vue/test-utils'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import { compileToFunctions } from 'vue-template-compiler'; import { GREEN_BOX_IMAGE_URL, RED_BOX_IMAGE_URL } from 'spec/test_constants'; @@ -51,60 +51,53 @@ describe('ImageDiffViewer', () => { wrapper.destroy(); }); - it('renders image diff for replaced', (done) => { + it('renders image diff for replaced', async () => { createComponent({ ...allProps }); - vm.$nextTick(() => { - const metaInfoElements = vm.$el.querySelectorAll('.image-info'); + await nextTick(); + const metaInfoElements = vm.$el.querySelectorAll('.image-info'); - expect(vm.$el.querySelector('.added img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL); + expect(vm.$el.querySelector('.added img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL); - expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe(RED_BOX_IMAGE_URL); + expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe(RED_BOX_IMAGE_URL); - expect(vm.$el.querySelector('.view-modes-menu li.active').textContent.trim()).toBe('2-up'); - expect(vm.$el.querySelector('.view-modes-menu li:nth-child(2)').textContent.trim()).toBe( - 'Swipe', - ); + expect(vm.$el.querySelector('.view-modes-menu li.active').textContent.trim()).toBe('2-up'); + expect(vm.$el.querySelector('.view-modes-menu li:nth-child(2)').textContent.trim()).toBe( + 'Swipe', + ); - expect(vm.$el.querySelector('.view-modes-menu li:nth-child(3)').textContent.trim()).toBe( - 'Onion skin', - ); + expect(vm.$el.querySelector('.view-modes-menu li:nth-child(3)').textContent.trim()).toBe( + 'Onion skin', + ); - expect(metaInfoElements.length).toBe(2); - expect(metaInfoElements[0]).toHaveText('2.00 KiB'); - expect(metaInfoElements[1]).toHaveText('1.00 KiB'); - - done(); - }); + expect(metaInfoElements.length).toBe(2); + expect(metaInfoElements[0]).toHaveText('2.00 KiB'); + expect(metaInfoElements[1]).toHaveText('1.00 KiB'); }); - it('renders image diff for new', (done) => { + it('renders image diff for new', async () => { createComponent({ ...allProps, diffMode: 'new', oldPath: '' }); - setImmediate(() => { - const metaInfoElement = vm.$el.querySelector('.image-info'); + await nextTick(); - expect(vm.$el.querySelector('.added img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL); - expect(metaInfoElement).toHaveText('1.00 KiB'); + const metaInfoElement = vm.$el.querySelector('.image-info'); - done(); - }); + expect(vm.$el.querySelector('.added img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL); + expect(metaInfoElement).toHaveText('1.00 KiB'); }); - it('renders image diff for deleted', (done) => { + it('renders image diff for deleted', async () => { createComponent({ ...allProps, diffMode: 'deleted', newPath: '' }); - setImmediate(() => { - const metaInfoElement = vm.$el.querySelector('.image-info'); + await nextTick(); - expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe(RED_BOX_IMAGE_URL); - expect(metaInfoElement).toHaveText('2.00 KiB'); + const metaInfoElement = vm.$el.querySelector('.image-info'); - done(); - }); + expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe(RED_BOX_IMAGE_URL); + expect(metaInfoElement).toHaveText('2.00 KiB'); }); - it('renders image diff for renamed', (done) => { + it('renders image diff for renamed', async () => { vm = new Vue({ components: { imageDiffViewer, @@ -130,69 +123,56 @@ describe('ImageDiffViewer', () => { `), }).$mount(); - setImmediate(() => { - const metaInfoElement = vm.$el.querySelector('.image-info'); + await nextTick(); - expect(vm.$el.querySelector('img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL); - expect(vm.$el.querySelector('.overlay')).not.toBe(null); + const metaInfoElement = vm.$el.querySelector('.image-info'); - expect(metaInfoElement).toHaveText('2.00 KiB'); + expect(vm.$el.querySelector('img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL); + expect(vm.$el.querySelector('.overlay')).not.toBe(null); - done(); - }); + expect(metaInfoElement).toHaveText('2.00 KiB'); }); describe('swipeMode', () => { - beforeEach((done) => { + beforeEach(() => { createComponent({ ...requiredProps }); - setImmediate(() => { - done(); - }); + return nextTick(); }); - it('switches to Swipe Mode', (done) => { + it('switches to Swipe Mode', async () => { vm.$el.querySelector('.view-modes-menu li:nth-child(2)').click(); - vm.$nextTick(() => { - expect(vm.$el.querySelector('.view-modes-menu li.active').textContent.trim()).toBe('Swipe'); - done(); - }); + await nextTick(); + expect(vm.$el.querySelector('.view-modes-menu li.active').textContent.trim()).toBe('Swipe'); }); }); describe('onionSkin', () => { - beforeEach((done) => { + beforeEach(() => { createComponent({ ...requiredProps }); - setImmediate(() => { - done(); - }); + return nextTick(); }); - it('switches to Onion Skin Mode', (done) => { + it('switches to Onion Skin Mode', async () => { vm.$el.querySelector('.view-modes-menu li:nth-child(3)').click(); - vm.$nextTick(() => { - expect(vm.$el.querySelector('.view-modes-menu li.active').textContent.trim()).toBe( - 'Onion skin', - ); - done(); - }); + await nextTick(); + expect(vm.$el.querySelector('.view-modes-menu li.active').textContent.trim()).toBe( + 'Onion skin', + ); }); - it('has working drag handler', (done) => { + it('has working drag handler', async () => { vm.$el.querySelector('.view-modes-menu li:nth-child(3)').click(); - vm.$nextTick(() => { - dragSlider(vm.$el.querySelector('.dragger'), document, 20); + await nextTick(); + dragSlider(vm.$el.querySelector('.dragger'), document, 20); - vm.$nextTick(() => { - expect(vm.$el.querySelector('.dragger').style.left).toBe('20px'); - expect(vm.$el.querySelector('.added.frame').style.opacity).toBe('0.2'); - done(); - }); - }); + await nextTick(); + expect(vm.$el.querySelector('.dragger').style.left).toBe('20px'); + expect(vm.$el.querySelector('.added.frame').style.opacity).toBe('0.2'); }); }); }); diff --git a/spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js b/spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js index b8d3cbebe16..549388c1a5c 100644 --- a/spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js +++ b/spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js @@ -1,5 +1,5 @@ import { shallowMount, mount } from '@vue/test-utils'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import { TRANSITION_LOAD_START, @@ -126,15 +126,14 @@ describe('Renamed Diff Viewer', () => { store = null; }); - it('calls the switchToFullDiffFromRenamedFile action when the method is triggered', () => { + it('calls the switchToFullDiffFromRenamedFile action when the method is triggered', async () => { store.dispatch.mockResolvedValue(); wrapper.vm.switchToFull(); - return wrapper.vm.$nextTick().then(() => { - expect(store.dispatch).toHaveBeenCalledWith('diffs/switchToFullDiffFromRenamedFile', { - diffFile, - }); + await nextTick(); + expect(store.dispatch).toHaveBeenCalledWith('diffs/switchToFullDiffFromRenamedFile', { + diffFile, }); }); @@ -144,7 +143,7 @@ describe('Renamed Diff Viewer', () => { ${STATE_ERRORED} | ${'mockRejectedValue'} | ${'rejected'} `( 'moves through the correct states during a $resolution request', - ({ after, resolvePromise }) => { + async ({ after, resolvePromise }) => { store.dispatch[resolvePromise](); expect(wrapper.vm.state).toEqual(STATE_IDLING); @@ -153,16 +152,9 @@ describe('Renamed Diff Viewer', () => { expect(wrapper.vm.state).toEqual(STATE_LOADING); - return ( - wrapper.vm - // This tick is needed for when the action (promise) finishes - .$nextTick() - // This tick waits for the state change in the promise .then/.catch to bubble into the component - .then(() => wrapper.vm.$nextTick()) - .then(() => { - expect(wrapper.vm.state).toEqual(after); - }) - ); + await nextTick(); // This tick is needed for when the action (promise) finishes + await nextTick(); // This tick waits for the state change in the promise .then/.catch to bubble into the component + expect(wrapper.vm.state).toEqual(after); }, ); }); diff --git a/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js b/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js index 194681a6138..4b32fbffebe 100644 --- a/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js +++ b/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js @@ -1,5 +1,6 @@ import { GlAlert, GlSprintf } from '@gitlab/ui'; import { mount, shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import { useLocalStorageSpy } from 'helpers/local_storage_helper'; import Component from '~/vue_shared/components/dismissible_feedback_alert.vue'; @@ -64,7 +65,7 @@ describe('Dismissible Feedback Alert', () => { it('should not show the alert once dismissed', async () => { localStorage.setItem(STORAGE_DISMISSAL_KEY, 'true'); createFullComponent(); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findAlert().exists()).toBe(false); }); diff --git a/spec/frontend/vue_shared/components/dropdown/dropdown_search_input_spec.js b/spec/frontend/vue_shared/components/dropdown/dropdown_search_input_spec.js index ec553c52236..b32dbeb8852 100644 --- a/spec/frontend/vue_shared/components/dropdown/dropdown_search_input_spec.js +++ b/spec/frontend/vue_shared/components/dropdown/dropdown_search_input_spec.js @@ -1,4 +1,5 @@ import { mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import DropdownSearchInputComponent from '~/vue_shared/components/dropdown/dropdown_search_input.vue'; describe('DropdownSearchInputComponent', () => { @@ -36,16 +37,15 @@ describe('DropdownSearchInputComponent', () => { expect(findInputEl().attributes('placeholder')).toBe(defaultProps.placeholderText); }); - it('focuses input element when focused property equals true', () => { + it('focuses input element when focused property equals true', async () => { const inputEl = findInputEl().element; jest.spyOn(inputEl, 'focus'); wrapper.setProps({ focused: true }); - return wrapper.vm.$nextTick().then(() => { - expect(inputEl.focus).toHaveBeenCalled(); - }); + await nextTick(); + expect(inputEl.focus).toHaveBeenCalled(); }); }); }); diff --git a/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js b/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js index b3af5fd3feb..084d0559665 100644 --- a/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js +++ b/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js @@ -1,6 +1,7 @@ import { GlDropdown, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import DropdownWidget from '~/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue'; describe('DropdownWidget component', () => { @@ -53,7 +54,7 @@ describe('DropdownWidget component', () => { describe('when dropdown is open', () => { beforeEach(async () => { findDropdown().vm.$emit('show'); - await wrapper.vm.$nextTick(); + await nextTick(); }); it('emits search event when typing in search box', () => { @@ -69,7 +70,7 @@ describe('DropdownWidget component', () => { it('emits set-option event when clicking on an option', async () => { wrapper.findAll('[data-testid="unselected-option"]').at(1).trigger('click'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.emitted('set-option')).toEqual([[wrapper.props().options[1]]]); }); diff --git a/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js b/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js index 996df34f2ff..c34041f9305 100644 --- a/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js +++ b/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js @@ -1,4 +1,5 @@ import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue'; import { UP_KEY_CODE, DOWN_KEY_CODE, TAB_KEY_CODE } from '~/lib/utils/keycodes'; @@ -53,7 +54,7 @@ describe('DropdownKeyboardNavigation', () => { it('should $emit @change with the default index when max changes', async () => { wrapper.setProps({ max: 20 }); - await wrapper.vm.$nextTick(); + await nextTick(); // The first @change`call happens on created() so we test for the second [1] expect(wrapper.emitted('change')[1]).toStrictEqual([MOCK_DEFAULT_INDEX]); }); diff --git a/spec/frontend/vue_shared/components/expand_button_spec.js b/spec/frontend/vue_shared/components/expand_button_spec.js index 7874658cc0f..87d6ed6b21f 100644 --- a/spec/frontend/vue_shared/components/expand_button_spec.js +++ b/spec/frontend/vue_shared/components/expand_button_spec.js @@ -1,5 +1,5 @@ import { mount } from '@vue/test-utils'; -import Vue from 'vue'; +import { nextTick } from 'vue'; import ExpandButton from '~/vue_shared/components/expand_button.vue'; const text = { @@ -66,9 +66,9 @@ describe('Expand button', () => { }); describe('on click', () => { - beforeEach((done) => { + beforeEach(async () => { expanderPrependEl().trigger('click'); - Vue.nextTick(done); + await nextTick(); }); afterEach(() => { @@ -85,7 +85,7 @@ describe('Expand button', () => { }); describe('when short text is provided', () => { - beforeEach((done) => { + beforeEach(async () => { factory({ slots: { expanded: `<p>${text.expanded}</p>`, @@ -94,7 +94,7 @@ describe('Expand button', () => { }); expanderPrependEl().trigger('click'); - Vue.nextTick(done); + await nextTick(); }); it('only renders expanded text', () => { @@ -110,31 +110,29 @@ describe('Expand button', () => { }); describe('append button', () => { - beforeEach((done) => { + beforeEach(async () => { expanderPrependEl().trigger('click'); - Vue.nextTick(done); + await nextTick(); }); - it('clicking hides itself and shows prepend', () => { + it('clicking hides itself and shows prepend', async () => { expect(expanderAppendEl().isVisible()).toBe(true); expanderAppendEl().trigger('click'); - return wrapper.vm.$nextTick().then(() => { - expect(expanderPrependEl().isVisible()).toBe(true); - }); + await nextTick(); + expect(expanderPrependEl().isVisible()).toBe(true); }); - it('clicking hides expanded text', () => { + it('clicking hides expanded text', async () => { expect(wrapper.find(ExpandButton).text().trim()).toBe(text.expanded); expanderAppendEl().trigger('click'); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.find(ExpandButton).text().trim()).not.toBe(text.expanded); - }); + await nextTick(); + expect(wrapper.find(ExpandButton).text().trim()).not.toBe(text.expanded); }); describe('when short text is provided', () => { - beforeEach((done) => { + beforeEach(async () => { factory({ slots: { expanded: `<p>${text.expanded}</p>`, @@ -143,16 +141,15 @@ describe('Expand button', () => { }); expanderPrependEl().trigger('click'); - Vue.nextTick(done); + await nextTick(); }); - it('clicking reveals short text', () => { + it('clicking reveals short text', async () => { expect(wrapper.find(ExpandButton).text().trim()).toBe(text.expanded); expanderAppendEl().trigger('click'); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.find(ExpandButton).text().trim()).toBe(text.short); - }); + await nextTick(); + expect(wrapper.find(ExpandButton).text().trim()).toBe(text.short); }); }); }); diff --git a/spec/frontend/vue_shared/components/file_finder/index_spec.js b/spec/frontend/vue_shared/components/file_finder/index_spec.js index 181fc4017a3..921091c5b84 100644 --- a/spec/frontend/vue_shared/components/file_finder/index_spec.js +++ b/spec/frontend/vue_shared/components/file_finder/index_spec.js @@ -1,6 +1,5 @@ import Mousetrap from 'mousetrap'; -import Vue from 'vue'; -import waitForPromises from 'helpers/wait_for_promises'; +import Vue, { nextTick } from 'vue'; import { file } from 'jest/ide/helpers'; import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes'; import FindFileComponent from '~/vue_shared/components/file_finder/index.vue'; @@ -31,7 +30,7 @@ describe('File finder item spec', () => { }); describe('with entries', () => { - beforeEach((done) => { + beforeEach(() => { createComponent({ files: [ { @@ -48,7 +47,7 @@ describe('File finder item spec', () => { ], }); - setImmediate(done); + return nextTick(); }); it('renders list of blobs', () => { @@ -57,68 +56,48 @@ describe('File finder item spec', () => { expect(vm.$el.textContent).not.toContain('folder'); }); - it('filters entries', (done) => { + it('filters entries', async () => { vm.searchText = 'index'; - setImmediate(() => { - expect(vm.$el.textContent).toContain('index.js'); - expect(vm.$el.textContent).not.toContain('component.js'); + await nextTick(); - done(); - }); + expect(vm.$el.textContent).toContain('index.js'); + expect(vm.$el.textContent).not.toContain('component.js'); }); - it('shows clear button when searchText is not empty', (done) => { + it('shows clear button when searchText is not empty', async () => { vm.searchText = 'index'; - setImmediate(() => { - expect(vm.$el.querySelector('.dropdown-input').classList).toContain('has-value'); - expect(vm.$el.querySelector('.dropdown-input-search').classList).toContain('hidden'); + await nextTick(); - done(); - }); + expect(vm.$el.querySelector('.dropdown-input').classList).toContain('has-value'); + expect(vm.$el.querySelector('.dropdown-input-search').classList).toContain('hidden'); }); - it('clear button resets searchText', (done) => { + it('clear button resets searchText', async () => { vm.searchText = 'index'; - waitForPromises() - .then(() => { - vm.clearSearchInput(); - }) - .then(waitForPromises) - .then(() => { - expect(vm.searchText).toBe(''); - }) - .then(done) - .catch(done.fail); + vm.clearSearchInput(); + + expect(vm.searchText).toBe(''); }); - it('clear button focuses search input', (done) => { + it('clear button focuses search input', async () => { jest.spyOn(vm.$refs.searchInput, 'focus').mockImplementation(() => {}); vm.searchText = 'index'; - waitForPromises() - .then(() => { - vm.clearSearchInput(); - }) - .then(waitForPromises) - .then(() => { - expect(vm.$refs.searchInput.focus).toHaveBeenCalled(); - }) - .then(done) - .catch(done.fail); + vm.clearSearchInput(); + + await nextTick(); + + expect(vm.$refs.searchInput.focus).toHaveBeenCalled(); }); describe('listShowCount', () => { - it('returns 1 when no filtered entries exist', (done) => { + it('returns 1 when no filtered entries exist', () => { vm.searchText = 'testing 123'; - setImmediate(() => { - expect(vm.listShowCount).toBe(1); - - done(); - }); + expect(vm.listShowCount).toBe(1); }); it('returns entries length when not filtered', () => { @@ -131,26 +110,18 @@ describe('File finder item spec', () => { expect(vm.listHeight).toBe(55); }); - it('returns 33 when entries dont exist', (done) => { + it('returns 33 when entries dont exist', () => { vm.searchText = 'testing 123'; - setImmediate(() => { - expect(vm.listHeight).toBe(33); - - done(); - }); + expect(vm.listHeight).toBe(33); }); }); describe('filteredBlobsLength', () => { - it('returns length of filtered blobs', (done) => { + it('returns length of filtered blobs', () => { vm.searchText = 'index'; - setImmediate(() => { - expect(vm.filteredBlobsLength).toBe(1); - - done(); - }); + expect(vm.filteredBlobsLength).toBe(1); }); }); @@ -158,7 +129,7 @@ describe('File finder item spec', () => { it('renders less DOM nodes if not visible by utilizing v-if', async () => { vm.visible = false; - await waitForPromises(); + await nextTick(); expect(vm.$el).toBeInstanceOf(Comment); }); @@ -166,33 +137,24 @@ describe('File finder item spec', () => { describe('watches', () => { describe('searchText', () => { - it('resets focusedIndex when updated', (done) => { + it('resets focusedIndex when updated', async () => { vm.focusedIndex = 1; vm.searchText = 'test'; - setImmediate(() => { - expect(vm.focusedIndex).toBe(0); + await nextTick(); - done(); - }); + expect(vm.focusedIndex).toBe(0); }); }); describe('visible', () => { - it('resets searchText when changed to false', (done) => { + it('resets searchText when changed to false', async () => { vm.searchText = 'test'; - vm.visible = true; - - waitForPromises() - .then(() => { - vm.visible = false; - }) - .then(waitForPromises) - .then(() => { - expect(vm.searchText).toBe(''); - }) - .then(done) - .catch(done.fail); + vm.visible = false; + + await nextTick(); + + expect(vm.searchText).toBe(''); }); }); }); @@ -216,7 +178,7 @@ describe('File finder item spec', () => { }); describe('onKeyup', () => { - it('opens file on enter key', (done) => { + it('opens file on enter key', async () => { const event = new CustomEvent('keyup'); event.keyCode = ENTER_KEY_CODE; @@ -224,14 +186,12 @@ describe('File finder item spec', () => { vm.$refs.searchInput.dispatchEvent(event); - setImmediate(() => { - expect(vm.openFile).toHaveBeenCalledWith(vm.files[0]); + await nextTick(); - done(); - }); + expect(vm.openFile).toHaveBeenCalledWith(vm.files[0]); }); - it('closes file finder on esc key', (done) => { + it('closes file finder on esc key', async () => { const event = new CustomEvent('keyup'); event.keyCode = ESC_KEY_CODE; @@ -239,11 +199,9 @@ describe('File finder item spec', () => { vm.$refs.searchInput.dispatchEvent(event); - setImmediate(() => { - expect(vm.$emit).toHaveBeenCalledWith('toggle', false); + await nextTick(); - done(); - }); + expect(vm.$emit).toHaveBeenCalledWith('toggle', false); }); }); @@ -310,34 +268,26 @@ describe('File finder item spec', () => { }); describe('keyboard shortcuts', () => { - beforeEach((done) => { + beforeEach(async () => { createComponent(); jest.spyOn(vm, 'toggle').mockImplementation(() => {}); - vm.$nextTick(done); + await nextTick(); }); - it('calls toggle on `t` key press', (done) => { + it('calls toggle on `t` key press', async () => { Mousetrap.trigger('t'); - vm.$nextTick() - .then(() => { - expect(vm.toggle).toHaveBeenCalled(); - }) - .then(done) - .catch(done.fail); + await nextTick(); + expect(vm.toggle).toHaveBeenCalled(); }); - it('calls toggle on `mod+p` key press', (done) => { + it('calls toggle on `mod+p` key press', async () => { Mousetrap.trigger('mod+p'); - vm.$nextTick() - .then(() => { - expect(vm.toggle).toHaveBeenCalled(); - }) - .then(done) - .catch(done.fail); + await nextTick(); + expect(vm.toggle).toHaveBeenCalled(); }); it('always allows `mod+p` to trigger toggle', () => { diff --git a/spec/frontend/vue_shared/components/file_finder/item_spec.js b/spec/frontend/vue_shared/components/file_finder/item_spec.js index 1a4a97efb95..b69c33055c1 100644 --- a/spec/frontend/vue_shared/components/file_finder/item_spec.js +++ b/spec/frontend/vue_shared/components/file_finder/item_spec.js @@ -1,4 +1,4 @@ -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import createComponent from 'helpers/vue_mount_component_helper'; import { file } from 'jest/ide/helpers'; import ItemComponent from '~/vue_shared/components/file_finder/item.vue'; @@ -37,14 +37,11 @@ describe('File finder item spec', () => { expect(vm.$el.classList).toContain('is-focused'); }); - it('does not have is-focused class when not focused', (done) => { + it('does not have is-focused class when not focused', async () => { vm.focused = false; - vm.$nextTick(() => { - expect(vm.$el.classList).not.toContain('is-focused'); - - done(); - }); + await nextTick(); + expect(vm.$el.classList).not.toContain('is-focused'); }); }); @@ -53,24 +50,18 @@ describe('File finder item spec', () => { expect(vm.$el.querySelector('.diff-changed-stats')).toBe(null); }); - it('renders when a changed file', (done) => { + it('renders when a changed file', async () => { vm.file.changed = true; - vm.$nextTick(() => { - expect(vm.$el.querySelector('.diff-changed-stats')).not.toBe(null); - - done(); - }); + await nextTick(); + expect(vm.$el.querySelector('.diff-changed-stats')).not.toBe(null); }); - it('renders when a temp file', (done) => { + it('renders when a temp file', async () => { vm.file.tempFile = true; - vm.$nextTick(() => { - expect(vm.$el.querySelector('.diff-changed-stats')).not.toBe(null); - - done(); - }); + await nextTick(); + expect(vm.$el.querySelector('.diff-changed-stats')).not.toBe(null); }); }); @@ -85,56 +76,52 @@ describe('File finder item spec', () => { describe('path', () => { let el; - beforeEach((done) => { + beforeEach(async () => { vm.searchText = 'file'; el = vm.$el.querySelector('.diff-changed-file-path'); - vm.$nextTick(done); + nextTick(); }); it('highlights text', () => { expect(el.querySelectorAll('.highlighted').length).toBe(4); }); - it('adds ellipsis to long text', (done) => { + it('adds ellipsis to long text', async () => { vm.file.path = new Array(70) .fill() .map((_, i) => `${i}-`) .join(''); - vm.$nextTick(() => { - expect(el.textContent).toBe(`...${vm.file.path.substr(vm.file.path.length - 60)}`); - done(); - }); + await nextTick(); + expect(el.textContent).toBe(`...${vm.file.path.substr(vm.file.path.length - 60)}`); }); }); describe('name', () => { let el; - beforeEach((done) => { + beforeEach(async () => { vm.searchText = 'file'; el = vm.$el.querySelector('.diff-changed-file-name'); - vm.$nextTick(done); + await nextTick(); }); it('highlights text', () => { expect(el.querySelectorAll('.highlighted').length).toBe(4); }); - it('does not add ellipsis to long text', (done) => { + it('does not add ellipsis to long text', async () => { vm.file.name = new Array(70) .fill() .map((_, i) => `${i}-`) .join(''); - vm.$nextTick(() => { - expect(el.textContent).not.toBe(`...${vm.file.name.substr(vm.file.name.length - 60)}`); - done(); - }); + await nextTick(); + expect(el.textContent).not.toBe(`...${vm.file.name.substr(vm.file.name.length - 60)}`); }); }); }); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js index 4e9eac2dde2..575e8a73050 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js @@ -8,6 +8,7 @@ import { } from '@gitlab/ui'; import { shallowMount, mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import RecentSearchesService from '~/filtered_search/services/recent_searches_service'; import RecentSearchesStore from '~/filtered_search/stores/recent_searches_store'; import { SortDirection } from '~/vue_shared/components/filtered_search_bar/constants'; @@ -172,7 +173,7 @@ describe('FilteredSearchBarRoot', () => { recentSearches: [{ foo: 'bar' }, 'foo'], }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.filteredRecentSearches).toHaveLength(1); expect(wrapper.vm.filteredRecentSearches[0]).toEqual({ foo: 'bar' }); @@ -188,7 +189,7 @@ describe('FilteredSearchBarRoot', () => { ], }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.filteredRecentSearches).toHaveLength(2); expect(uniqueTokens).toHaveBeenCalled(); @@ -199,7 +200,7 @@ describe('FilteredSearchBarRoot', () => { recentSearchesStorageKey: '', }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.filteredRecentSearches).not.toBeDefined(); }); @@ -208,7 +209,7 @@ describe('FilteredSearchBarRoot', () => { describe('watchers', () => { describe('filterValue', () => { - it('emits component event `onFilter` with empty array and false when filter was never selected', () => { + it('emits component event `onFilter` with empty array and false when filter was never selected', async () => { wrapper = createComponent({ initialFilterValue: [tokenValueEmpty] }); // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details // eslint-disable-next-line no-restricted-syntax @@ -217,12 +218,11 @@ describe('FilteredSearchBarRoot', () => { filterValue: [tokenValueEmpty], }); - return wrapper.vm.$nextTick(() => { - expect(wrapper.emitted('onFilter')[0]).toEqual([[], false]); - }); + await nextTick(); + expect(wrapper.emitted('onFilter')[0]).toEqual([[], false]); }); - it('emits component event `onFilter` with empty array and true when initially selected filter value was cleared', () => { + it('emits component event `onFilter` with empty array and true when initially selected filter value was cleared', async () => { wrapper = createComponent({ initialFilterValue: [tokenValueLabel] }); // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details // eslint-disable-next-line no-restricted-syntax @@ -231,9 +231,8 @@ describe('FilteredSearchBarRoot', () => { filterValue: [tokenValueEmpty], }); - return wrapper.vm.$nextTick(() => { - expect(wrapper.emitted('onFilter')[0]).toEqual([[], true]); - }); + await nextTick(); + expect(wrapper.emitted('onFilter')[0]).toEqual([[], true]); }); }); }); @@ -336,7 +335,7 @@ describe('FilteredSearchBarRoot', () => { filterValue: mockFilters, }); - await wrapper.vm.$nextTick(); + await nextTick(); }); it('calls `uniqueTokens` on `filterValue` prop to remove duplicates', () => { @@ -395,7 +394,7 @@ describe('FilteredSearchBarRoot', () => { }); describe('template', () => { - beforeEach(() => { + beforeEach(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({ @@ -404,7 +403,7 @@ describe('FilteredSearchBarRoot', () => { recentSearches: mockHistoryItems, }); - return wrapper.vm.$nextTick(); + await nextTick(); }); it('renders gl-filtered-search component', () => { @@ -439,7 +438,7 @@ describe('FilteredSearchBarRoot', () => { const wrapperFullMount = createComponent({ sortOptions: mockSortOptions, shallow: false }); wrapperFullMount.vm.recentSearchesStore.addRecentSearch(mockHistoryItems[0]); - await wrapperFullMount.vm.$nextTick(); + await nextTick(); const searchHistoryItemsEl = wrapperFullMount.findAll( '.gl-search-box-by-click-menu .gl-search-box-by-click-history-item', @@ -462,7 +461,7 @@ describe('FilteredSearchBarRoot', () => { wrapperFullMount.vm.recentSearchesStore.addRecentSearch([tokenValueMembership]); - await wrapperFullMount.vm.$nextTick(); + await nextTick(); expect(wrapperFullMount.find(GlDropdownItem).text()).toBe('Membership := Direct'); @@ -480,7 +479,7 @@ describe('FilteredSearchBarRoot', () => { wrapperFullMount.vm.recentSearchesStore.addRecentSearch([tokenValueMembership]); - await wrapperFullMount.vm.$nextTick(); + await nextTick(); expect(wrapperFullMount.find(GlDropdownItem).text()).toBe('Membership := exclude'); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js index 5865c6a41b8..87066b70023 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js @@ -6,6 +6,7 @@ import { } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; +import { nextTick } from 'vue'; import waitForPromises from 'helpers/wait_for_promises'; import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; @@ -167,7 +168,7 @@ describe('AuthorToken', () => { const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); - await wrapper.vm.$nextTick(); + await nextTick(); }; it('renders base-token component', () => { @@ -185,23 +186,22 @@ describe('AuthorToken', () => { }); }); - it('renders token item when value is selected', () => { + it('renders token item when value is selected', async () => { wrapper = createComponent({ value: { data: mockAuthors[0].username }, data: { authors: mockAuthors }, stubs: { Portal: true }, }); - return wrapper.vm.$nextTick(() => { - const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + await nextTick(); + const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); - expect(tokenSegments).toHaveLength(3); // Author, =, "Administrator" + expect(tokenSegments).toHaveLength(3); // Author, =, "Administrator" - const tokenValue = tokenSegments.at(2); + const tokenValue = tokenSegments.at(2); - expect(tokenValue.findComponent(GlAvatar).props('src')).toBe(mockAuthors[0].avatar_url); - expect(tokenValue.text()).toBe(mockAuthors[0].name); // "Administrator" - }); + expect(tokenValue.findComponent(GlAvatar).props('src')).toBe(mockAuthors[0].avatar_url); + expect(tokenValue.text()).toBe(mockAuthors[0].name); // "Administrator" }); it('renders token value with correct avatarUrl from author object', async () => { @@ -220,7 +220,7 @@ describe('AuthorToken', () => { stubs: { Portal: true }, }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(getAvatarEl().props('src')).toBe(mockAuthors[0].avatar_url); @@ -236,7 +236,7 @@ describe('AuthorToken', () => { ], }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(getAvatarEl().props('src')).toBe(mockAuthors[0].avatar_url); }); @@ -268,7 +268,7 @@ describe('AuthorToken', () => { const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.find(GlDropdownDivider).exists()).toBe(false); }); @@ -323,7 +323,7 @@ describe('AuthorToken', () => { it('does not show current user while searching', async () => { wrapper.findComponent(BaseToken).vm.handleInput({ data: 'foo' }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.findComponent(GlFilteredSearchSuggestion).exists()).toBe(false); }); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js index 84f0151d9db..dd9bf2ff598 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js @@ -1,5 +1,6 @@ -import { GlFilteredSearchToken } from '@gitlab/ui'; +import { GlFilteredSearchToken, GlLoadingIcon } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import { mockRegularLabel, mockLabels, @@ -61,13 +62,10 @@ const mockProps = { getActiveTokenValue: (labels, data) => labels.find((label) => label.title === data), }; -function createComponent({ - props = { ...mockProps }, - stubs = defaultStubs, - slots = defaultSlots, -} = {}) { +function createComponent({ props = {}, stubs = defaultStubs, slots = defaultSlots } = {}) { return mount(BaseToken, { propsData: { + ...mockProps, ...props, }, provide: { @@ -83,15 +81,7 @@ function createComponent({ describe('BaseToken', () => { let wrapper; - beforeEach(() => { - wrapper = createComponent({ - props: { - ...mockProps, - value: { data: `"${mockRegularLabel.title}"` }, - suggestions: mockLabels, - }, - }); - }); + const findGlFilteredSearchToken = () => wrapper.findComponent(GlFilteredSearchToken); afterEach(() => { wrapper.destroy(); @@ -99,21 +89,25 @@ describe('BaseToken', () => { describe('data', () => { it('calls `getRecentlyUsedSuggestions` to populate `recentSuggestions` when `recentSuggestionsStorageKey` is defined', () => { + wrapper = createComponent(); + expect(getRecentlyUsedSuggestions).toHaveBeenCalledWith(mockStorageKey); }); }); describe('computed', () => { describe('activeTokenValue', () => { - it('calls `getActiveTokenValue` when it is provided', async () => { + it('calls `getActiveTokenValue` when it is provided', () => { const mockGetActiveTokenValue = jest.fn(); - wrapper.setProps({ - getActiveTokenValue: mockGetActiveTokenValue, + wrapper = createComponent({ + props: { + value: { data: `"${mockRegularLabel.title}"` }, + suggestions: mockLabels, + getActiveTokenValue: mockGetActiveTokenValue, + }, }); - await wrapper.vm.$nextTick(); - expect(mockGetActiveTokenValue).toHaveBeenCalledTimes(1); expect(mockGetActiveTokenValue).toHaveBeenCalledWith( mockLabels, @@ -125,33 +119,19 @@ describe('BaseToken', () => { describe('watch', () => { describe('active', () => { - let wrapperWithTokenActive; - beforeEach(() => { - wrapperWithTokenActive = createComponent({ + wrapper = createComponent({ props: { - ...mockProps, value: { data: `"${mockRegularLabel.title}"` }, active: true, }, }); }); - afterEach(() => { - wrapperWithTokenActive.destroy(); - }); - it('emits `fetch-suggestions` event on the component when value of this prop is changed to false and `suggestions` array is empty', async () => { - wrapperWithTokenActive.setProps({ - active: false, - }); - - await wrapperWithTokenActive.vm.$nextTick(); + await wrapper.setProps({ active: false }); - expect(wrapperWithTokenActive.emitted('fetch-suggestions')).toBeTruthy(); - expect(wrapperWithTokenActive.emitted('fetch-suggestions')).toEqual([ - [`"${mockRegularLabel.title}"`], - ]); + expect(wrapper.emitted('fetch-suggestions')).toEqual([[`"${mockRegularLabel.title}"`]]); }); }); }); @@ -161,17 +141,15 @@ describe('BaseToken', () => { const mockTokenValue = mockLabels[0]; it('calls `setTokenValueToRecentlyUsed` when `recentSuggestionsStorageKey` is defined', () => { + wrapper = createComponent({ props: { suggestions: mockLabels } }); + wrapper.vm.handleTokenValueSelected(mockTokenValue.title); expect(setTokenValueToRecentlyUsed).toHaveBeenCalledWith(mockStorageKey, mockTokenValue); }); - it('does not add token from preloadedSuggestions', async () => { - wrapper.setProps({ - preloadedSuggestions: [mockTokenValue], - }); - - await wrapper.vm.$nextTick(); + it('does not add token from preloadedSuggestions', () => { + wrapper = createComponent({ props: { preloadedSuggestions: [mockTokenValue] } }); wrapper.vm.handleTokenValueSelected(mockTokenValue.title); @@ -182,58 +160,60 @@ describe('BaseToken', () => { describe('template', () => { it('renders gl-filtered-search-token component', () => { - const wrapperWithNoStubs = createComponent({ - stubs: {}, - }); - const glFilteredSearchToken = wrapperWithNoStubs.find(GlFilteredSearchToken); - - expect(glFilteredSearchToken.exists()).toBe(true); - expect(glFilteredSearchToken.props('config')).toEqual(mockProps.config); + wrapper = createComponent({ stubs: {} }); - wrapperWithNoStubs.destroy(); + expect(findGlFilteredSearchToken().props('config')).toEqual(mockProps.config); }); it('renders `view-token` slot when present', () => { + wrapper = createComponent(); + expect(wrapper.find('.js-view-token').exists()).toBe(true); }); it('renders `view` slot when present', () => { + wrapper = createComponent(); + expect(wrapper.find('.js-view').exists()).toBe(true); }); - describe('events', () => { - let wrapperWithNoStubs; - - afterEach(() => { - wrapperWithNoStubs.destroy(); + it('renders loading spinner when loading', () => { + wrapper = createComponent({ + props: { + active: true, + config: mockLabelToken, + suggestionsLoading: true, + }, + stubs: { Portal: true }, }); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); + }); + + describe('events', () => { describe('when activeToken has been selected', () => { beforeEach(() => { - wrapperWithNoStubs = createComponent({ - props: { - ...mockProps, - getActiveTokenValue: () => ({ title: '' }), - suggestionsLoading: true, - }, + wrapper = createComponent({ + props: { getActiveTokenValue: () => ({ title: '' }) }, stubs: { Portal: true }, }); }); + it('does not emit `fetch-suggestions` event on component after a delay when component emits `input` event', async () => { jest.useFakeTimers(); - wrapperWithNoStubs.find(GlFilteredSearchToken).vm.$emit('input', { data: 'foo' }); - await wrapperWithNoStubs.vm.$nextTick(); + findGlFilteredSearchToken().vm.$emit('input', { data: 'foo' }); + await nextTick(); jest.runAllTimers(); - expect(wrapperWithNoStubs.emitted('fetch-suggestions')).toEqual([['']]); + expect(wrapper.emitted('fetch-suggestions')).toEqual([['']]); }); }); describe('when activeToken has not been selected', () => { beforeEach(() => { - wrapperWithNoStubs = createComponent({ + wrapper = createComponent({ stubs: { Portal: true }, }); }); @@ -241,38 +221,27 @@ describe('BaseToken', () => { it('emits `fetch-suggestions` event on component after a delay when component emits `input` event', async () => { jest.useFakeTimers(); - wrapperWithNoStubs.find(GlFilteredSearchToken).vm.$emit('input', { data: 'foo' }); - await wrapperWithNoStubs.vm.$nextTick(); + findGlFilteredSearchToken().vm.$emit('input', { data: 'foo' }); + await nextTick(); jest.runAllTimers(); - expect(wrapperWithNoStubs.emitted('fetch-suggestions')).toBeTruthy(); - expect(wrapperWithNoStubs.emitted('fetch-suggestions')[2]).toEqual(['foo']); + expect(wrapper.emitted('fetch-suggestions')[2]).toEqual(['foo']); }); describe('when search is started with a quote', () => { - it('emits `fetch-suggestions` with filtered value', async () => { - jest.useFakeTimers(); - - wrapperWithNoStubs.find(GlFilteredSearchToken).vm.$emit('input', { data: '"foo' }); - await wrapperWithNoStubs.vm.$nextTick(); + it('emits `fetch-suggestions` with filtered value', () => { + findGlFilteredSearchToken().vm.$emit('input', { data: '"foo' }); - jest.runAllTimers(); - - expect(wrapperWithNoStubs.emitted('fetch-suggestions')[2]).toEqual(['foo']); + expect(wrapper.emitted('fetch-suggestions')[2]).toEqual(['foo']); }); }); describe('when search starts and ends with a quote', () => { - it('emits `fetch-suggestions` with filtered value', async () => { - jest.useFakeTimers(); - - wrapperWithNoStubs.find(GlFilteredSearchToken).vm.$emit('input', { data: '"foo"' }); - await wrapperWithNoStubs.vm.$nextTick(); - - jest.runAllTimers(); + it('emits `fetch-suggestions` with filtered value', () => { + findGlFilteredSearchToken().vm.$emit('input', { data: '"foo"' }); - expect(wrapperWithNoStubs.emitted('fetch-suggestions')[2]).toEqual(['foo']); + expect(wrapper.emitted('fetch-suggestions')[2]).toEqual(['foo']); }); }); }); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js index cd8be765fb5..7a7db434052 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js @@ -6,6 +6,7 @@ import { } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; +import { nextTick } from 'vue'; import waitForPromises from 'helpers/wait_for_promises'; import createFlash from '~/flash'; @@ -115,7 +116,7 @@ describe('BranchToken', () => { const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); - await wrapper.vm.$nextTick(); + await nextTick(); } beforeEach(async () => { @@ -127,7 +128,7 @@ describe('BranchToken', () => { branches: mockBranches, }); - await wrapper.vm.$nextTick(); + await nextTick(); }); it('renders gl-filtered-search-token component', () => { diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js index ed9ac7c271e..b163563cea4 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js @@ -6,6 +6,7 @@ import { } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; +import { nextTick } from 'vue'; import waitForPromises from 'helpers/wait_for_promises'; import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; @@ -129,7 +130,7 @@ describe('EmojiToken', () => { emojis: mockEmojis, }); - await wrapper.vm.$nextTick(); + await nextTick(); }); it('renders gl-filtered-search-token component', () => { @@ -152,7 +153,7 @@ describe('EmojiToken', () => { const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); - await wrapper.vm.$nextTick(); + await nextTick(); const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); @@ -171,7 +172,7 @@ describe('EmojiToken', () => { const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.find(GlFilteredSearchSuggestion).exists()).toBe(false); expect(wrapper.find(GlDropdownDivider).exists()).toBe(false); @@ -186,7 +187,7 @@ describe('EmojiToken', () => { const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); - await wrapper.vm.$nextTick(); + await nextTick(); const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js index b9af71ad8a7..52df27c2d00 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js @@ -5,6 +5,7 @@ import { } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; +import { nextTick } from 'vue'; import waitForPromises from 'helpers/wait_for_promises'; import { mockRegularLabel, @@ -150,7 +151,7 @@ describe('LabelToken', () => { labels: mockLabels, }); - await wrapper.vm.$nextTick(); + await nextTick(); }); it('renders base-token component', () => { @@ -182,7 +183,7 @@ describe('LabelToken', () => { const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); - await wrapper.vm.$nextTick(); + await nextTick(); const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); @@ -201,7 +202,7 @@ describe('LabelToken', () => { const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.find(GlFilteredSearchSuggestion).exists()).toBe(false); expect(wrapper.find(GlDropdownDivider).exists()).toBe(false); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js index c0d8b5fd139..de9ec863dd5 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js @@ -6,6 +6,7 @@ import { } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; +import { nextTick } from 'vue'; import waitForPromises from 'helpers/wait_for_promises'; import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; @@ -31,7 +32,7 @@ const defaultStubs = { function createComponent(options = {}) { const { - config = mockMilestoneToken, + config = { ...mockMilestoneToken, shouldSkipSort: true }, value = { data: '' }, active = false, stubs = defaultStubs, @@ -67,6 +68,27 @@ describe('MilestoneToken', () => { describe('methods', () => { describe('fetchMilestones', () => { + describe('when config.shouldSkipSort is true', () => { + beforeEach(() => { + wrapper.vm.config.shouldSkipSort = true; + }); + + afterEach(() => { + wrapper.vm.config.shouldSkipSort = false; + }); + it('does not call sortMilestonesByDueDate', async () => { + jest.spyOn(wrapper.vm.config, 'fetchMilestones').mockResolvedValue({ + data: mockMilestones, + }); + + wrapper.vm.fetchMilestones(); + + await waitForPromises(); + + expect(sortMilestonesByDueDate).toHaveBeenCalledTimes(0); + }); + }); + it('calls `config.fetchMilestones` with provided searchTerm param', () => { jest.spyOn(wrapper.vm.config, 'fetchMilestones'); @@ -76,10 +98,11 @@ describe('MilestoneToken', () => { }); it('sets response to `milestones` when request is successful', () => { + wrapper.vm.config.shouldSkipSort = false; + jest.spyOn(wrapper.vm.config, 'fetchMilestones').mockResolvedValue({ data: mockMilestones, }); - wrapper.vm.fetchMilestones(); return waitForPromises().then(() => { @@ -127,7 +150,7 @@ describe('MilestoneToken', () => { milestones: mockMilestones, }); - await wrapper.vm.$nextTick(); + await nextTick(); }); it('renders gl-filtered-search-token component', () => { @@ -150,7 +173,7 @@ describe('MilestoneToken', () => { const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); - await wrapper.vm.$nextTick(); + await nextTick(); const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); @@ -169,7 +192,7 @@ describe('MilestoneToken', () => { const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.find(GlFilteredSearchSuggestion).exists()).toBe(false); expect(wrapper.find(GlDropdownDivider).exists()).toBe(false); @@ -184,7 +207,7 @@ describe('MilestoneToken', () => { const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); - await wrapper.vm.$nextTick(); + await nextTick(); const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js index b2f246a5985..8be21b35414 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js @@ -1,5 +1,6 @@ import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import waitForPromises from 'helpers/wait_for_promises'; import createFlash from '~/flash'; import ReleaseToken from '~/vue_shared/components/filtered_search_bar/tokens/release_token.vue'; @@ -31,7 +32,7 @@ describe('ReleaseToken', () => { it('renders release value', async () => { wrapper = createComponent({ value: { data: id } }); - await wrapper.vm.$nextTick(); + await nextTick(); const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment); diff --git a/spec/frontend/vue_shared/components/gfm_autocomplete/__snapshots__/utils_spec.js.snap b/spec/frontend/vue_shared/components/gfm_autocomplete/__snapshots__/utils_spec.js.snap deleted file mode 100644 index 370b6eb01bc..00000000000 --- a/spec/frontend/vue_shared/components/gfm_autocomplete/__snapshots__/utils_spec.js.snap +++ /dev/null @@ -1,54 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`gfm_autocomplete/utils emojis config shows the emoji name and icon in the menu item 1`] = `"raised_hands <gl-emoji data-name=\\"raised_hands\\"></gl-emoji>"`; - -exports[`gfm_autocomplete/utils issues config shows the iid and title in the menu item within a project context 1`] = `"<small>123456</small> Project context issue title <script>alert('hi')</script>"`; - -exports[`gfm_autocomplete/utils issues config shows the reference and title in the menu item within a group context 1`] = `"<small>gitlab#987654</small> Group context issue title <script>alert('hi')</script>"`; - -exports[`gfm_autocomplete/utils labels config shows the title in the menu item 1`] = ` -" - <span class=\\"dropdown-label-box\\" style=\\"background: #123456;\\"></span> - bug <script>alert('hi')</script>" -`; - -exports[`gfm_autocomplete/utils members config shows an avatar character, name, parent name, and count in the menu item for a group 1`] = ` -" - <div class=\\"gl-display-flex gl-align-items-center\\"> - <div class=\\"gl-avatar gl-avatar-s32 gl-flex-shrink-0 gl-rounded-small - gl-display-flex gl-align-items-center gl-justify-content-center\\" aria-hidden=\\"true\\"> - G</div> - <div class=\\"gl-line-height-normal gl-ml-4\\"> - <div>1-1s <script>alert('hi')</script> (2)</div> - <div class=\\"gl-text-gray-700\\">GitLab Support Team</div> - </div> - - </div> - " -`; - -exports[`gfm_autocomplete/utils members config shows the avatar, name and username in the menu item for a user 1`] = ` -" - <div class=\\"gl-display-flex gl-align-items-center\\"> - <img class=\\"gl-avatar gl-avatar-s32 gl-flex-shrink-0 gl-avatar-circle\\" src=\\"/uploads/-/system/user/avatar/123456/avatar.png\\" alt=\\"\\" /> - <div class=\\"gl-line-height-normal gl-ml-4\\"> - <div>My Name <script>alert('hi')</script></div> - <div class=\\"gl-text-gray-700\\">@myusername</div> - </div> - - </div> - " -`; - -exports[`gfm_autocomplete/utils merge requests config shows the iid and title in the menu item within a project context 1`] = `"<small>123456</small> Project context merge request title <script>alert('hi')</script>"`; - -exports[`gfm_autocomplete/utils merge requests config shows the reference and title in the menu item within a group context 1`] = `"<small>gitlab!456789</small> Group context merge request title <script>alert('hi')</script>"`; - -exports[`gfm_autocomplete/utils milestones config shows the title in the menu item 1`] = `"13.2 <script>alert('hi')</script>"`; - -exports[`gfm_autocomplete/utils quick actions config shows the name, aliases, params and description in the menu item 1`] = ` -"<div>/unlabel <small>(or /remove_label)</small> <small>~label1 ~\\"label 2\\"</small></div> - <div><small><em>Remove all or specific label(s)</em></small></div>" -`; - -exports[`gfm_autocomplete/utils snippets config shows the id and title in the menu item 1`] = `"<small>123456</small> Snippet title <script>alert('hi')</script>"`; diff --git a/spec/frontend/vue_shared/components/gfm_autocomplete/gfm_autocomplete_spec.js b/spec/frontend/vue_shared/components/gfm_autocomplete/gfm_autocomplete_spec.js deleted file mode 100644 index b4002fdf4ec..00000000000 --- a/spec/frontend/vue_shared/components/gfm_autocomplete/gfm_autocomplete_spec.js +++ /dev/null @@ -1,34 +0,0 @@ -import Tribute from '@gitlab/tributejs'; -import { shallowMount } from '@vue/test-utils'; -import GfmAutocomplete from '~/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue'; - -describe('GfmAutocomplete', () => { - let wrapper; - - describe('tribute', () => { - const mentions = '/gitlab-org/gitlab-test/-/autocomplete_sources/members?type=Issue&type_id=1'; - - beforeEach(() => { - wrapper = shallowMount(GfmAutocomplete, { - propsData: { - dataSources: { - mentions, - }, - }, - slots: { - default: ['<input/>'], - }, - }); - }); - - it('is set to tribute instance variable', () => { - expect(wrapper.vm.tribute instanceof Tribute).toBe(true); - }); - - it('contains the slot input element', () => { - wrapper.find('input').setValue('@'); - - expect(wrapper.vm.tribute.current.element).toBe(wrapper.find('input').element); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/gfm_autocomplete/utils_spec.js b/spec/frontend/vue_shared/components/gfm_autocomplete/utils_spec.js deleted file mode 100644 index 7ec3fbd4e3b..00000000000 --- a/spec/frontend/vue_shared/components/gfm_autocomplete/utils_spec.js +++ /dev/null @@ -1,427 +0,0 @@ -import { escape, last } from 'lodash'; -import { GfmAutocompleteType, tributeConfig } from '~/vue_shared/components/gfm_autocomplete/utils'; - -describe('gfm_autocomplete/utils', () => { - describe('emojis config', () => { - const emojisConfig = tributeConfig[GfmAutocompleteType.Emojis].config; - const emoji = 'raised_hands'; - - it('uses : as the trigger', () => { - expect(emojisConfig.trigger).toBe(':'); - }); - - it('searches using the emoji name', () => { - expect(emojisConfig.lookup(emoji)).toBe(emoji); - }); - - it('limits the number of rendered items to 100', () => { - expect(emojisConfig.menuItemLimit).toBe(100); - }); - - it('shows the emoji name and icon in the menu item', () => { - expect(emojisConfig.menuItemTemplate({ original: emoji })).toMatchSnapshot(); - }); - - it('inserts the emoji name on autocomplete selection', () => { - expect(emojisConfig.selectTemplate({ original: emoji })).toBe(`:${emoji}:`); - }); - }); - - describe('issues config', () => { - const issuesConfig = tributeConfig[GfmAutocompleteType.Issues].config; - const groupContextIssue = { - iid: 987654, - reference: 'gitlab#987654', - title: "Group context issue title <script>alert('hi')</script>", - }; - const projectContextIssue = { - id: null, - iid: 123456, - time_estimate: 0, - title: "Project context issue title <script>alert('hi')</script>", - }; - - it('uses # as the trigger', () => { - expect(issuesConfig.trigger).toBe('#'); - }); - - it('searches using both the iid and title', () => { - expect(issuesConfig.lookup(projectContextIssue)).toBe( - `${projectContextIssue.iid}${projectContextIssue.title}`, - ); - }); - - it('limits the number of rendered items to 100', () => { - expect(issuesConfig.menuItemLimit).toBe(100); - }); - - it('shows the reference and title in the menu item within a group context', () => { - expect(issuesConfig.menuItemTemplate({ original: groupContextIssue })).toMatchSnapshot(); - }); - - it('shows the iid and title in the menu item within a project context', () => { - expect(issuesConfig.menuItemTemplate({ original: projectContextIssue })).toMatchSnapshot(); - }); - - it('inserts the reference on autocomplete selection within a group context', () => { - expect(issuesConfig.selectTemplate({ original: groupContextIssue })).toBe( - groupContextIssue.reference, - ); - }); - - it('inserts the iid on autocomplete selection within a project context', () => { - expect(issuesConfig.selectTemplate({ original: projectContextIssue })).toBe( - `#${projectContextIssue.iid}`, - ); - }); - }); - - describe('labels config', () => { - const labelsConfig = tributeConfig[GfmAutocompleteType.Labels].config; - const labelsFilter = tributeConfig[GfmAutocompleteType.Labels].filterValues; - const label = { - color: '#123456', - textColor: '#FFFFFF', - title: `bug <script>alert('hi')</script>`, - type: 'GroupLabel', - }; - const singleWordLabel = { - color: '#456789', - textColor: '#DDD', - title: `bug`, - type: 'GroupLabel', - }; - const numericalLabel = { - color: '#abcdef', - textColor: '#AAA', - title: 123456, - type: 'ProjectLabel', - }; - - it('uses ~ as the trigger', () => { - expect(labelsConfig.trigger).toBe('~'); - }); - - it('searches using `title`', () => { - expect(labelsConfig.lookup).toBe('title'); - }); - - it('limits the number of rendered items to 100', () => { - expect(labelsConfig.menuItemLimit).toBe(100); - }); - - it('shows the title in the menu item', () => { - expect(labelsConfig.menuItemTemplate({ original: label })).toMatchSnapshot(); - }); - - it('inserts the title on autocomplete selection', () => { - expect(labelsConfig.selectTemplate({ original: singleWordLabel })).toBe( - `~${escape(singleWordLabel.title)}`, - ); - }); - - it('inserts the title enclosed with quotes on autocomplete selection when the title is numerical', () => { - expect(labelsConfig.selectTemplate({ original: numericalLabel })).toBe( - `~"${escape(numericalLabel.title)}"`, - ); - }); - - it('inserts the title enclosed with quotes on autocomplete selection when the title contains multiple words', () => { - expect(labelsConfig.selectTemplate({ original: label })).toBe(`~"${escape(label.title)}"`); - }); - - describe('filter', () => { - const collection = [label, singleWordLabel, { ...numericalLabel, set: true }]; - - describe('/label quick action', () => { - describe('when the line starts with `/label`', () => { - it('shows labels that are not currently selected', () => { - const fullText = '/label ~'; - const selectionStart = 8; - - expect(labelsFilter({ collection, fullText, selectionStart })).toEqual([ - collection[0], - collection[1], - ]); - }); - }); - - describe('when the line does not start with `/label`', () => { - it('shows all labels', () => { - const fullText = '~'; - const selectionStart = 1; - - expect(labelsFilter({ collection, fullText, selectionStart })).toEqual(collection); - }); - }); - }); - - describe('/unlabel quick action', () => { - describe('when the line starts with `/unlabel`', () => { - it('shows labels that are currently selected', () => { - const fullText = '/unlabel ~'; - const selectionStart = 10; - - expect(labelsFilter({ collection, fullText, selectionStart })).toEqual([collection[2]]); - }); - }); - - describe('when the line does not start with `/unlabel`', () => { - it('shows all labels', () => { - const fullText = '~'; - const selectionStart = 1; - - expect(labelsFilter({ collection, fullText, selectionStart })).toEqual(collection); - }); - }); - }); - }); - }); - - describe('members config', () => { - const membersConfig = tributeConfig[GfmAutocompleteType.Members].config; - const membersFilter = tributeConfig[GfmAutocompleteType.Members].filterValues; - const userMember = { - type: 'User', - username: 'myusername', - name: "My Name <script>alert('hi')</script>", - avatar_url: '/uploads/-/system/user/avatar/123456/avatar.png', - availability: null, - }; - const groupMember = { - type: 'Group', - username: 'gitlab-com/support/1-1s', - name: "GitLab.com / GitLab Support Team / 1-1s <script>alert('hi')</script>", - avatar_url: null, - count: 2, - mentionsDisabled: null, - }; - - it('uses @ as the trigger', () => { - expect(membersConfig.trigger).toBe('@'); - }); - - it('inserts the username on autocomplete selection', () => { - expect(membersConfig.fillAttr).toBe('username'); - }); - - it('searches using both the name and username for a user', () => { - expect(membersConfig.lookup(userMember)).toBe(`${userMember.name}${userMember.username}`); - }); - - it('searches using only its own name and not its ancestors for a group', () => { - expect(membersConfig.lookup(groupMember)).toBe(last(groupMember.name.split(' / '))); - }); - - it('limits the items in the autocomplete menu to 10', () => { - expect(membersConfig.menuItemLimit).toBe(10); - }); - - it('shows the avatar, name and username in the menu item for a user', () => { - expect(membersConfig.menuItemTemplate({ original: userMember })).toMatchSnapshot(); - }); - - it('shows an avatar character, name, parent name, and count in the menu item for a group', () => { - expect(membersConfig.menuItemTemplate({ original: groupMember })).toMatchSnapshot(); - }); - - describe('filter', () => { - const assignees = [userMember.username]; - const collection = [userMember, groupMember]; - - describe('/assign quick action', () => { - describe('when the line starts with `/assign`', () => { - it('shows members that are not currently selected', () => { - const fullText = '/assign @'; - const selectionStart = 9; - - expect(membersFilter({ assignees, collection, fullText, selectionStart })).toEqual([ - collection[1], - ]); - }); - }); - - describe('when the line does not start with `/assign`', () => { - it('shows all labels', () => { - const fullText = '@'; - const selectionStart = 1; - - expect(membersFilter({ assignees, collection, fullText, selectionStart })).toEqual( - collection, - ); - }); - }); - }); - - describe('/unassign quick action', () => { - describe('when the line starts with `/unassign`', () => { - it('shows members that are currently selected', () => { - const fullText = '/unassign @'; - const selectionStart = 11; - - expect(membersFilter({ assignees, collection, fullText, selectionStart })).toEqual([ - collection[0], - ]); - }); - }); - - describe('when the line does not start with `/unassign`', () => { - it('shows all members', () => { - const fullText = '@'; - const selectionStart = 1; - - expect(membersFilter({ assignees, collection, fullText, selectionStart })).toEqual( - collection, - ); - }); - }); - }); - }); - }); - - describe('merge requests config', () => { - const mergeRequestsConfig = tributeConfig[GfmAutocompleteType.MergeRequests].config; - const groupContextMergeRequest = { - iid: 456789, - reference: 'gitlab!456789', - title: "Group context merge request title <script>alert('hi')</script>", - }; - const projectContextMergeRequest = { - id: null, - iid: 123456, - time_estimate: 0, - title: "Project context merge request title <script>alert('hi')</script>", - }; - - it('uses ! as the trigger', () => { - expect(mergeRequestsConfig.trigger).toBe('!'); - }); - - it('searches using both the iid and title', () => { - expect(mergeRequestsConfig.lookup(projectContextMergeRequest)).toBe( - `${projectContextMergeRequest.iid}${projectContextMergeRequest.title}`, - ); - }); - - it('limits the number of rendered items to 100', () => { - expect(mergeRequestsConfig.menuItemLimit).toBe(100); - }); - - it('shows the reference and title in the menu item within a group context', () => { - expect( - mergeRequestsConfig.menuItemTemplate({ original: groupContextMergeRequest }), - ).toMatchSnapshot(); - }); - - it('shows the iid and title in the menu item within a project context', () => { - expect( - mergeRequestsConfig.menuItemTemplate({ original: projectContextMergeRequest }), - ).toMatchSnapshot(); - }); - - it('inserts the reference on autocomplete selection within a group context', () => { - expect(mergeRequestsConfig.selectTemplate({ original: groupContextMergeRequest })).toBe( - groupContextMergeRequest.reference, - ); - }); - - it('inserts the iid on autocomplete selection within a project context', () => { - expect(mergeRequestsConfig.selectTemplate({ original: projectContextMergeRequest })).toBe( - `!${projectContextMergeRequest.iid}`, - ); - }); - }); - - describe('milestones config', () => { - const milestonesConfig = tributeConfig[GfmAutocompleteType.Milestones].config; - const milestone = { - id: null, - iid: 49, - title: "13.2 <script>alert('hi')</script>", - }; - - it('uses % as the trigger', () => { - expect(milestonesConfig.trigger).toBe('%'); - }); - - it('searches using the title', () => { - expect(milestonesConfig.lookup).toBe('title'); - }); - - it('limits the number of rendered items to 100', () => { - expect(milestonesConfig.menuItemLimit).toBe(100); - }); - - it('shows the title in the menu item', () => { - expect(milestonesConfig.menuItemTemplate({ original: milestone })).toMatchSnapshot(); - }); - - it('inserts the title on autocomplete selection', () => { - expect(milestonesConfig.selectTemplate({ original: milestone })).toBe( - `%"${escape(milestone.title)}"`, - ); - }); - }); - - describe('quick actions config', () => { - const quickActionsConfig = tributeConfig[GfmAutocompleteType.QuickActions].config; - const quickAction = { - name: 'unlabel', - aliases: ['remove_label'], - description: 'Remove all or specific label(s)', - warning: '', - icon: '', - params: ['~label1 ~"label 2"'], - }; - - it('uses / as the trigger', () => { - expect(quickActionsConfig.trigger).toBe('/'); - }); - - it('inserts the name on autocomplete selection', () => { - expect(quickActionsConfig.fillAttr).toBe('name'); - }); - - it('searches using both the name and aliases', () => { - expect(quickActionsConfig.lookup(quickAction)).toBe( - `${quickAction.name}${quickAction.aliases.join(', /')}`, - ); - }); - - it('limits the number of rendered items to 100', () => { - expect(quickActionsConfig.menuItemLimit).toBe(100); - }); - - it('shows the name, aliases, params and description in the menu item', () => { - expect(quickActionsConfig.menuItemTemplate({ original: quickAction })).toMatchSnapshot(); - }); - }); - - describe('snippets config', () => { - const snippetsConfig = tributeConfig[GfmAutocompleteType.Snippets].config; - const snippet = { - id: 123456, - title: "Snippet title <script>alert('hi')</script>", - }; - - it('uses $ as the trigger', () => { - expect(snippetsConfig.trigger).toBe('$'); - }); - - it('inserts the id on autocomplete selection', () => { - expect(snippetsConfig.fillAttr).toBe('id'); - }); - - it('searches using both the id and title', () => { - expect(snippetsConfig.lookup(snippet)).toBe(`${snippet.id}${snippet.title}`); - }); - - it('limits the number of rendered items to 100', () => { - expect(snippetsConfig.menuItemLimit).toBe(100); - }); - - it('shows the id and title in the menu item', () => { - expect(snippetsConfig.menuItemTemplate({ original: snippet })).toMatchSnapshot(); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/gl_countdown_spec.js b/spec/frontend/vue_shared/components/gl_countdown_spec.js index 82d18c7fd3f..0d1d42082ab 100644 --- a/spec/frontend/vue_shared/components/gl_countdown_spec.js +++ b/spec/frontend/vue_shared/components/gl_countdown_spec.js @@ -1,4 +1,4 @@ -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import mountComponent from 'helpers/vue_mount_component_helper'; import GlCountdown from '~/vue_shared/components/gl_countdown.vue'; @@ -17,38 +17,34 @@ describe('GlCountdown', () => { }); describe('when there is time remaining', () => { - beforeEach((done) => { + beforeEach(async () => { vm = mountComponent(Component, { endDateString: '2000-01-01T01:02:03Z', }); - Vue.nextTick().then(done).catch(done.fail); + await nextTick(); }); it('displays remaining time', () => { expect(vm.$el.textContent).toContain('01:02:03'); }); - it('updates remaining time', (done) => { + it('updates remaining time', async () => { now = '2000-01-01T00:00:01Z'; jest.advanceTimersByTime(1000); - Vue.nextTick() - .then(() => { - expect(vm.$el.textContent).toContain('01:02:02'); - done(); - }) - .catch(done.fail); + await nextTick(); + expect(vm.$el.textContent).toContain('01:02:02'); }); }); describe('when there is no time remaining', () => { - beforeEach((done) => { + beforeEach(async () => { vm = mountComponent(Component, { endDateString: '1900-01-01T00:00:00Z', }); - Vue.nextTick().then(done).catch(done.fail); + await nextTick(); }); it('displays 00:00:00', () => { diff --git a/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js b/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js index b837a998cd6..c0a6588833e 100644 --- a/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js +++ b/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js @@ -1,6 +1,6 @@ import { GlModal } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants'; import GlModalVuex from '~/vue_shared/components/gl_modal_vuex.vue'; @@ -118,7 +118,7 @@ describe('GlModalVuex', () => { expect(actions.hide).toHaveBeenCalledTimes(1); }); - it('calls bootstrap show when isVisible changes', (done) => { + it('calls bootstrap show when isVisible changes', async () => { state.isVisible = false; factory(); @@ -126,16 +126,11 @@ describe('GlModalVuex', () => { state.isVisible = true; - wrapper.vm - .$nextTick() - .then(() => { - expect(rootEmit).toHaveBeenCalledWith(BV_SHOW_MODAL, TEST_MODAL_ID); - }) - .then(done) - .catch(done.fail); + await nextTick(); + expect(rootEmit).toHaveBeenCalledWith(BV_SHOW_MODAL, TEST_MODAL_ID); }); - it('calls bootstrap hide when isVisible changes', (done) => { + it('calls bootstrap hide when isVisible changes', async () => { state.isVisible = true; factory(); @@ -143,13 +138,8 @@ describe('GlModalVuex', () => { state.isVisible = false; - wrapper.vm - .$nextTick() - .then(() => { - expect(rootEmit).toHaveBeenCalledWith(BV_HIDE_MODAL, TEST_MODAL_ID); - }) - .then(done) - .catch(done.fail); + await nextTick(); + expect(rootEmit).toHaveBeenCalledWith(BV_HIDE_MODAL, TEST_MODAL_ID); }); it.each(['ok', 'cancel'])( diff --git a/spec/frontend/vue_shared/components/help_popover_spec.js b/spec/frontend/vue_shared/components/help_popover_spec.js index 30c6fa04032..597fb63d95c 100644 --- a/spec/frontend/vue_shared/components/help_popover_spec.js +++ b/spec/frontend/vue_shared/components/help_popover_spec.js @@ -9,59 +9,117 @@ describe('HelpPopover', () => { const findQuestionButton = () => wrapper.find(GlButton); const findPopover = () => wrapper.find(GlPopover); - const buildWrapper = (options = {}) => { + + const createComponent = ({ props, ...opts } = {}) => { wrapper = mount(HelpPopover, { propsData: { options: { title, content, - ...options, }, + ...props, }, + ...opts, }); }; - beforeEach(() => { - buildWrapper(); - }); - afterEach(() => { wrapper.destroy(); }); - it('renders a link button with an icon question', () => { - expect(findQuestionButton().props()).toMatchObject({ - icon: 'question', - variant: 'link', + describe('with title and content', () => { + beforeEach(() => { + createComponent(); }); - }); - it('renders popover that uses the question button as target', () => { - expect(findPopover().props().target()).toBe(findQuestionButton().vm.$el); - }); + it('renders a link button with an icon question', () => { + expect(findQuestionButton().props()).toMatchObject({ + icon: 'question', + variant: 'link', + }); + }); - it('allows rendering title with HTML tags', () => { - expect(findPopover().find('strong').exists()).toBe(true); - }); + it('renders popover that uses the question button as target', () => { + expect(findPopover().props().target()).toBe(findQuestionButton().vm.$el); + }); - it('allows rendering content with HTML tags', () => { - expect(findPopover().find('b').exists()).toBe(true); + it('shows title and content', () => { + expect(findPopover().html()).toContain(title); + expect(findPopover().html()).toContain(content); + }); + + it('allows rendering title with HTML tags', () => { + expect(findPopover().find('strong').exists()).toBe(true); + }); + + it('allows rendering content with HTML tags', () => { + expect(findPopover().find('b').exists()).toBe(true); + }); }); describe('without title', () => { - it('does not render title', () => { - buildWrapper({ title: null }); + beforeEach(() => { + createComponent({ + props: { + options: { + title: null, + content, + }, + }, + }); + }); + + it('does not show title', () => { + expect(findPopover().html()).not.toContain(title); + }); - expect(findPopover().find('span').exists()).toBe(false); + it('shows content', () => { + expect(findPopover().html()).toContain(content); }); }); - it('binds other popover options to the popover instance', () => { + describe('with other options', () => { const placement = 'bottom'; - wrapper.destroy(); - buildWrapper({ placement }); + beforeEach(() => { + createComponent({ + props: { + options: { + placement, + }, + }, + }); + }); + + it('options bind to the popover', () => { + expect(findPopover().props().placement).toBe(placement); + }); + }); + + describe('with custom slots', () => { + const titleSlot = '<h1>title</h1>'; + const defaultSlot = '<strong>content</strong>'; - expect(findPopover().props().placement).toBe(placement); + beforeEach(() => { + createComponent({ + slots: { + title: titleSlot, + default: defaultSlot, + }, + }); + }); + + it('shows title slot', () => { + expect(findPopover().html()).toContain(titleSlot); + }); + + it('shows default content slot', () => { + expect(findPopover().html()).toContain(defaultSlot); + }); + + it('overrides title and content from options', () => { + expect(findPopover().html()).not.toContain(title); + expect(findPopover().html()).toContain(content); + }); }); }); diff --git a/spec/frontend/vue_shared/components/local_storage_sync_spec.js b/spec/frontend/vue_shared/components/local_storage_sync_spec.js index 4c5a0c1e601..dac633fe6c8 100644 --- a/spec/frontend/vue_shared/components/local_storage_sync_spec.js +++ b/spec/frontend/vue_shared/components/local_storage_sync_spec.js @@ -1,4 +1,5 @@ import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; describe('Local Storage Sync', () => { @@ -49,7 +50,7 @@ describe('Local Storage Sync', () => { it.each('foo', 3, true, ['foo', 'bar'], { foo: 'bar' })( 'saves updated value to localStorage', - (newValue) => { + async (newValue) => { createComponent({ props: { storageKey, @@ -59,9 +60,8 @@ describe('Local Storage Sync', () => { wrapper.setProps({ value: newValue }); - return wrapper.vm.$nextTick().then(() => { - expect(localStorage.getItem(storageKey)).toBe(String(newValue)); - }); + await nextTick(); + expect(localStorage.getItem(storageKey)).toBe(String(newValue)); }, ); @@ -109,7 +109,7 @@ describe('Local Storage Sync', () => { expect(localStorage.getItem(storageKey)).toBe(savedValue); }); - it('updating the value updates localStorage', () => { + it('updating the value updates localStorage', async () => { createComponent({ props: { storageKey, @@ -122,9 +122,8 @@ describe('Local Storage Sync', () => { value: newValue, }); - return wrapper.vm.$nextTick().then(() => { - expect(localStorage.getItem(storageKey)).toBe(newValue); - }); + await nextTick(); + expect(localStorage.getItem(storageKey)).toBe(newValue); }); it('persists the value by default', async () => { @@ -137,7 +136,7 @@ describe('Local Storage Sync', () => { }); wrapper.setProps({ value: persistedValue }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(localStorage.getItem(storageKey)).toBe(persistedValue); }); @@ -151,7 +150,7 @@ describe('Local Storage Sync', () => { }); wrapper.setProps({ persist: false, value: notPersistedValue }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(localStorage.getItem(storageKey)).not.toBe(notPersistedValue); }); }); @@ -172,7 +171,7 @@ describe('Local Storage Sync', () => { ${{ foo: 'bar' }} | ${'{"foo":"bar"}'} `('given $value', ({ value, serializedValue }) => { describe('is a new value', () => { - beforeEach(() => { + beforeEach(async () => { createComponent({ props: { storageKey, @@ -183,7 +182,7 @@ describe('Local Storage Sync', () => { wrapper.setProps({ value }); - return wrapper.vm.$nextTick(); + await nextTick(); }); it('serializes the value correctly to localStorage', () => { @@ -253,7 +252,7 @@ describe('Local Storage Sync', () => { value, }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(localStorage.getItem(storageKey)).toBe(value); @@ -261,7 +260,7 @@ describe('Local Storage Sync', () => { clear: true, }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(localStorage.getItem(storageKey)).toBe(null); }); diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js index 0d90ca7f1f6..c7ad47b6ef7 100644 --- a/spec/frontend/vue_shared/components/markdown/field_spec.js +++ b/spec/frontend/vue_shared/components/markdown/field_spec.js @@ -1,10 +1,10 @@ -import { mount } from '@vue/test-utils'; import { nextTick } from 'vue'; import AxiosMockAdapter from 'axios-mock-adapter'; import $ from 'jquery'; import { TEST_HOST, FIXTURES_PATH } from 'spec/test_constants'; import axios from '~/lib/utils/axios_utils'; import MarkdownField from '~/vue_shared/components/markdown/field.vue'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; const markdownPreviewPath = `${TEST_HOST}/preview`; const markdownDocsPath = `${TEST_HOST}/docs`; @@ -12,8 +12,8 @@ const textareaValue = 'testing\n123'; const uploadsPath = 'test/uploads'; function assertMarkdownTabs(isWrite, writeLink, previewLink, wrapper) { - expect(writeLink.element.parentNode.classList.contains('active')).toBe(isWrite); - expect(previewLink.element.parentNode.classList.contains('active')).toBe(!isWrite); + expect(writeLink.element.children[0].classList.contains('active')).toBe(isWrite); + expect(previewLink.element.children[0].classList.contains('active')).toBe(!isWrite); expect(wrapper.find('.md-preview-holder').element.style.display).toBe(isWrite ? 'none' : ''); } @@ -29,14 +29,13 @@ describe('Markdown field component', () => { afterEach(() => { subject.destroy(); - subject = null; axiosMock.restore(); }); function createSubject(lines = []) { // We actually mount a wrapper component so that we can force Vue to rerender classes in order to test a regression // caused by mixing Vanilla JS and Vue. - subject = mount( + subject = mountExtended( { components: { MarkdownField, @@ -63,12 +62,17 @@ describe('Markdown field component', () => { textareaValue, lines, }, + provide: { + glFeatures: { + contactsAutocomplete: true, + }, + }, }, ); } - const getPreviewLink = () => subject.find('.nav-links .js-preview-link'); - const getWriteLink = () => subject.find('.nav-links .js-write-link'); + const getPreviewLink = () => subject.findByTestId('preview-tab'); + const getWriteLink = () => subject.findByTestId('write-tab'); const getMarkdownButton = () => subject.find('.js-md'); const getAllMarkdownButtons = () => subject.findAll('.js-md'); const getVideo = () => subject.find('video'); @@ -100,115 +104,100 @@ describe('Markdown field component', () => { axiosMock.onPost(markdownPreviewPath).reply(200, { body: previewHTML }); }); - it('sets preview link as active', () => { + it('sets preview link as active', async () => { previewLink = getPreviewLink(); - previewLink.trigger('click'); + previewLink.vm.$emit('click', { target: {} }); - return subject.vm.$nextTick().then(() => { - expect(previewLink.element.parentNode.classList.contains('active')).toBeTruthy(); - }); + await nextTick(); + expect(previewLink.element.children[0].classList.contains('active')).toBe(true); }); - it('shows preview loading text', () => { + it('shows preview loading text', async () => { previewLink = getPreviewLink(); - previewLink.trigger('click'); + previewLink.vm.$emit('click', { target: {} }); - return subject.vm.$nextTick(() => { - expect(subject.find('.md-preview-holder').element.textContent.trim()).toContain( - 'Loading…', - ); - }); + await nextTick(); + expect(subject.find('.md-preview-holder').element.textContent.trim()).toContain('Loading…'); }); - it('renders markdown preview and GFM', () => { + it('renders markdown preview and GFM', async () => { const renderGFMSpy = jest.spyOn($.fn, 'renderGFM'); previewLink = getPreviewLink(); - previewLink.trigger('click'); + previewLink.vm.$emit('click', { target: {} }); - return axios.waitFor(markdownPreviewPath).then(() => { - expect(subject.find('.md-preview-holder').element.innerHTML).toContain(previewHTML); - expect(renderGFMSpy).toHaveBeenCalled(); - }); + await axios.waitFor(markdownPreviewPath); + expect(subject.find('.md-preview-holder').element.innerHTML).toContain(previewHTML); + expect(renderGFMSpy).toHaveBeenCalled(); }); - it('calls video.pause() on comment input when isSubmitting is changed to true', () => { + it('calls video.pause() on comment input when isSubmitting is changed to true', async () => { previewLink = getPreviewLink(); - previewLink.trigger('click'); + previewLink.vm.$emit('click', { target: {} }); - let callPause; + await axios.waitFor(markdownPreviewPath); + const video = getVideo(); + const callPause = jest.spyOn(video.element, 'pause').mockImplementation(() => true); - return axios - .waitFor(markdownPreviewPath) - .then(() => { - const video = getVideo(); - callPause = jest.spyOn(video.element, 'pause').mockImplementation(() => true); + subject.setProps({ isSubmitting: true }); - subject.setProps({ isSubmitting: true }); - - return subject.vm.$nextTick(); - }) - .then(() => { - expect(callPause).toHaveBeenCalled(); - }); + await nextTick(); + expect(callPause).toHaveBeenCalled(); }); it('clicking already active write or preview link does nothing', async () => { writeLink = getWriteLink(); previewLink = getPreviewLink(); - writeLink.trigger('click'); - await subject.vm.$nextTick(); + writeLink.vm.$emit('click', { target: {} }); + await nextTick(); assertMarkdownTabs(true, writeLink, previewLink, subject); - writeLink.trigger('click'); - await subject.vm.$nextTick(); + writeLink.vm.$emit('click', { target: {} }); + await nextTick(); assertMarkdownTabs(true, writeLink, previewLink, subject); - previewLink.trigger('click'); - await subject.vm.$nextTick(); + previewLink.vm.$emit('click', { target: {} }); + await nextTick(); assertMarkdownTabs(false, writeLink, previewLink, subject); - previewLink.trigger('click'); - await subject.vm.$nextTick(); + previewLink.vm.$emit('click', { target: {} }); + await nextTick(); assertMarkdownTabs(false, writeLink, previewLink, subject); }); }); describe('markdown buttons', () => { - it('converts single words', () => { + it('converts single words', async () => { const textarea = subject.find('textarea').element; textarea.setSelectionRange(0, 7); const markdownButton = getMarkdownButton(); markdownButton.trigger('click'); - return subject.vm.$nextTick(() => { - expect(textarea.value).toContain('**testing**'); - }); + await nextTick(); + expect(textarea.value).toContain('**testing**'); }); - it('converts a line', () => { + it('converts a line', async () => { const textarea = subject.find('textarea').element; textarea.setSelectionRange(0, 0); const markdownButton = getAllMarkdownButtons().wrappers[5]; markdownButton.trigger('click'); - return subject.vm.$nextTick(() => { - expect(textarea.value).toContain('- testing'); - }); + await nextTick(); + expect(textarea.value).toContain('- testing'); }); - it('converts multiple lines', () => { + it('converts multiple lines', async () => { const textarea = subject.find('textarea').element; textarea.setSelectionRange(0, 50); const markdownButton = getAllMarkdownButtons().wrappers[5]; markdownButton.trigger('click'); - return subject.vm.$nextTick(() => { - expect(textarea.value).toContain('- testing\n- 123'); - }); + await nextTick(); + expect(textarea.value).toContain('- testing\n- 123'); }); }); @@ -229,7 +218,7 @@ describe('Markdown field component', () => { // Do something to trigger rerendering the class subject.setProps({ wrapperClasses: 'foo' }); - await subject.vm.$nextTick(); + await nextTick(); }); it('should have rerendered classes and kept gfm-form', () => { diff --git a/spec/frontend/vue_shared/components/markdown/header_spec.js b/spec/frontend/vue_shared/components/markdown/header_spec.js index fec6abc9639..93ce3935fab 100644 --- a/spec/frontend/vue_shared/components/markdown/header_spec.js +++ b/spec/frontend/vue_shared/components/markdown/header_spec.js @@ -1,20 +1,25 @@ -import { shallowMount } from '@vue/test-utils'; import $ from 'jquery'; +import { nextTick } from 'vue'; +import { GlTabs } from '@gitlab/ui'; import HeaderComponent from '~/vue_shared/components/markdown/header.vue'; import ToolbarButton from '~/vue_shared/components/markdown/toolbar_button.vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; describe('Markdown field header component', () => { let wrapper; const createWrapper = (props) => { - wrapper = shallowMount(HeaderComponent, { + wrapper = shallowMountExtended(HeaderComponent, { propsData: { previewMarkdown: false, ...props, }, + stubs: { GlTabs }, }); }; + const findWriteTab = () => wrapper.findByTestId('write-tab'); + const findPreviewTab = () => wrapper.findByTestId('preview-tab'); const findToolbarButtons = () => wrapper.findAll(ToolbarButton); const findToolbarButtonByProp = (prop, value) => findToolbarButtons() @@ -33,7 +38,6 @@ describe('Markdown field header component', () => { afterEach(() => { wrapper.destroy(); - wrapper = null; }); describe('markdown header buttons', () => { @@ -74,30 +78,29 @@ describe('Markdown field header component', () => { }); }); - it('renders `write` link as active when previewMarkdown is false', () => { - expect(wrapper.find('li:nth-child(1)').classes()).toContain('active'); + it('activates `write` tab when previewMarkdown is false', () => { + expect(findWriteTab().attributes('active')).toBe('true'); + expect(findPreviewTab().attributes('active')).toBeUndefined(); }); - it('renders `preview` link as active when previewMarkdown is true', () => { + it('activates `preview` tab when previewMarkdown is true', () => { createWrapper({ previewMarkdown: true }); - expect(wrapper.find('li:nth-child(2)').classes()).toContain('active'); + expect(findWriteTab().attributes('active')).toBeUndefined(); + expect(findPreviewTab().attributes('active')).toBe('true'); }); - it('emits toggle markdown event when clicking preview', () => { - wrapper.find('.js-preview-link').trigger('click'); + it('emits toggle markdown event when clicking preview tab', async () => { + const eventData = { target: {} }; + findPreviewTab().vm.$emit('click', eventData); - return wrapper.vm - .$nextTick() - .then(() => { - expect(wrapper.emitted('preview-markdown').length).toEqual(1); + await nextTick(); + expect(wrapper.emitted('preview-markdown').length).toEqual(1); - wrapper.find('.js-write-link').trigger('click'); - return wrapper.vm.$nextTick(); - }) - .then(() => { - expect(wrapper.emitted('write-markdown').length).toEqual(1); - }); + findWriteTab().vm.$emit('click', eventData); + + await nextTick(); + expect(wrapper.emitted('write-markdown').length).toEqual(1); }); it('does not emit toggle markdown event when triggered from another form', () => { @@ -112,12 +115,10 @@ describe('Markdown field header component', () => { }); it('blurs preview link after click', () => { - const link = wrapper.find('li:nth-child(2) button'); - jest.spyOn(HTMLElement.prototype, 'blur').mockImplementation(); - - link.trigger('click'); + const target = { blur: jest.fn() }; + findPreviewTab().vm.$emit('click', { target }); - expect(link.element.blur).toHaveBeenCalled(); + expect(target.blur).toHaveBeenCalled(); }); it('renders markdown table template', () => { diff --git a/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js b/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js index 9bc2aad1895..9944267cf24 100644 --- a/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js +++ b/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js @@ -1,5 +1,6 @@ import { GlLoadingIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import ApplySuggestion from '~/vue_shared/components/markdown/apply_suggestion.vue'; import SuggestionDiffHeader from '~/vue_shared/components/markdown/suggestion_diff_header.vue'; @@ -103,15 +104,14 @@ describe('Suggestion Diff component', () => { expect(wrapper.text()).toContain('Applying suggestion...'); }); - it('when callback of apply is called, hides loading', () => { + it('when callback of apply is called, hides loading', async () => { const [callback] = wrapper.emitted().apply[0]; callback(); - return wrapper.vm.$nextTick().then(() => { - expect(findApplyButton().exists()).toBe(true); - expect(findLoading().exists()).toBe(false); - }); + await nextTick(); + expect(findApplyButton().exists()).toBe(true); + expect(findLoading().exists()).toBe(false); }); }); diff --git a/spec/frontend/vue_shared/components/markdown/suggestions_spec.js b/spec/frontend/vue_shared/components/markdown/suggestions_spec.js index 6fcac2df0b6..8f4235cfe41 100644 --- a/spec/frontend/vue_shared/components/markdown/suggestions_spec.js +++ b/spec/frontend/vue_shared/components/markdown/suggestions_spec.js @@ -1,4 +1,4 @@ -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import SuggestionsComponent from '~/vue_shared/components/markdown/suggestions.vue'; const MOCK_DATA = { @@ -51,7 +51,7 @@ describe('Suggestion component', () => { let vm; let diffTable; - beforeEach((done) => { + beforeEach(async () => { const Component = Vue.extend(SuggestionsComponent); vm = new Component({ @@ -62,7 +62,7 @@ describe('Suggestion component', () => { jest.spyOn(vm, 'renderSuggestions').mockImplementation(() => {}); vm.renderSuggestions(); - Vue.nextTick(done); + await nextTick(); }); describe('mounted', () => { diff --git a/spec/frontend/vue_shared/components/modal_copy_button_spec.js b/spec/frontend/vue_shared/components/modal_copy_button_spec.js index adb72c3ef85..b57efc88d57 100644 --- a/spec/frontend/vue_shared/components/modal_copy_button_spec.js +++ b/spec/frontend/vue_shared/components/modal_copy_button_spec.js @@ -1,4 +1,5 @@ import { shallowMount, createWrapper } from '@vue/test-utils'; +import { nextTick } from 'vue'; import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue'; @@ -20,7 +21,7 @@ describe('modal copy button', () => { }); describe('clipboard', () => { - it('should fire a `success` event on click', () => { + it('should fire a `success` event on click', async () => { const root = createWrapper(wrapper.vm.$root); document.execCommand = jest.fn(() => true); window.getSelection = jest.fn(() => ({ @@ -29,20 +30,18 @@ describe('modal copy button', () => { })); wrapper.trigger('click'); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.emitted().success).not.toBeEmpty(); - expect(document.execCommand).toHaveBeenCalledWith('copy'); - expect(root.emitted(BV_HIDE_TOOLTIP)).toEqual([['test-id']]); - }); + await nextTick(); + expect(wrapper.emitted().success).not.toBeEmpty(); + expect(document.execCommand).toHaveBeenCalledWith('copy'); + expect(root.emitted(BV_HIDE_TOOLTIP)).toEqual([['test-id']]); }); - it("should propagate the clipboard error event if execCommand doesn't work", () => { + it("should propagate the clipboard error event if execCommand doesn't work", async () => { document.execCommand = jest.fn(() => false); wrapper.trigger('click'); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.emitted().error).not.toBeEmpty(); - expect(document.execCommand).toHaveBeenCalledWith('copy'); - }); + await nextTick(); + expect(wrapper.emitted().error).not.toBeEmpty(); + expect(document.execCommand).toHaveBeenCalledWith('copy'); }); }); }); diff --git a/spec/frontend/vue_shared/components/multiselect_dropdown_spec.js b/spec/frontend/vue_shared/components/multiselect_dropdown_spec.js deleted file mode 100644 index 566ca1817f2..00000000000 --- a/spec/frontend/vue_shared/components/multiselect_dropdown_spec.js +++ /dev/null @@ -1,35 +0,0 @@ -import { GlDropdown } from '@gitlab/ui'; -import { getByText } from '@testing-library/dom'; -import { shallowMount } from '@vue/test-utils'; -import MultiSelectDropdown from '~/vue_shared/components/sidebar/multiselect_dropdown.vue'; - -describe('MultiSelectDropdown Component', () => { - it('renders items slot', () => { - const wrapper = shallowMount(MultiSelectDropdown, { - propsData: { - text: '', - headerText: '', - }, - slots: { - items: '<p>Test</p>', - }, - }); - expect(getByText(wrapper.element, 'Test')).toBeDefined(); - }); - - it('renders search slot', () => { - const wrapper = shallowMount(MultiSelectDropdown, { - propsData: { - text: '', - headerText: '', - }, - slots: { - search: '<p>Search</p>', - }, - stubs: { - GlDropdown, - }, - }); - expect(getByText(wrapper.element, 'Search')).toBeDefined(); - }); -}); diff --git a/spec/frontend/vue_shared/components/namespace_select/mock_data.js b/spec/frontend/vue_shared/components/namespace_select/mock_data.js index c9d96672e85..cfd521c67cb 100644 --- a/spec/frontend/vue_shared/components/namespace_select/mock_data.js +++ b/spec/frontend/vue_shared/components/namespace_select/mock_data.js @@ -1,11 +1,6 @@ -export const group = [ +export const groupNamespaces = [ { id: 1, name: 'Group 1', humanName: 'Group 1' }, { id: 2, name: 'Subgroup 1', humanName: 'Group 1 / Subgroup 1' }, ]; -export const user = [{ id: 3, name: 'User namespace 1', humanName: 'User namespace 1' }]; - -export const namespaces = { - group, - user, -}; +export const userNamespaces = [{ id: 3, name: 'User namespace 1', humanName: 'User namespace 1' }]; diff --git a/spec/frontend/vue_shared/components/namespace_select/namespace_select_spec.js b/spec/frontend/vue_shared/components/namespace_select/namespace_select_spec.js index 8f07f63993d..c11b20a692e 100644 --- a/spec/frontend/vue_shared/components/namespace_select/namespace_select_spec.js +++ b/spec/frontend/vue_shared/components/namespace_select/namespace_select_spec.js @@ -1,9 +1,15 @@ -import { GlDropdown, GlDropdownItem, GlDropdownSectionHeader } from '@gitlab/ui'; +import { nextTick } from 'vue'; +import { GlDropdown, GlDropdownItem, GlDropdownSectionHeader, GlSearchBoxByType } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import NamespaceSelect, { i18n, + EMPTY_NAMESPACE_ID, } from '~/vue_shared/components/namespace_select/namespace_select.vue'; -import { user, group, namespaces } from './mock_data'; +import { userNamespaces, groupNamespaces } from './mock_data'; + +const FLAT_NAMESPACES = [...groupNamespaces, ...userNamespaces]; +const EMPTY_NAMESPACE_TITLE = 'Empty namespace TEST'; +const EMPTY_NAMESPACE_ITEM = { id: EMPTY_NAMESPACE_ID, humanName: EMPTY_NAMESPACE_TITLE }; describe('Namespace Select', () => { let wrapper; @@ -11,71 +17,115 @@ describe('Namespace Select', () => { const createComponent = (props = {}) => shallowMountExtended(NamespaceSelect, { propsData: { - data: namespaces, + userNamespaces, + groupNamespaces, ...props, }, + stubs: { + // We have to "full" mount GlDropdown so that slot children will render + GlDropdown, + }, }); const wrappersText = (arr) => arr.wrappers.map((w) => w.text()); - const flatNamespaces = () => [...group, ...user]; const findDropdown = () => wrapper.findComponent(GlDropdown); - const findDropdownAttributes = (attr) => findDropdown().attributes(attr); - const selectedDropdownItemText = () => findDropdownAttributes('text'); + const findDropdownText = () => findDropdown().props('text'); const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); + const findDropdownItemsTexts = () => findDropdownItems().wrappers.map((x) => x.text()); const findSectionHeaders = () => wrapper.findAllComponents(GlDropdownSectionHeader); - - beforeEach(() => { - wrapper = createComponent(); - }); + const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType); + const search = (term) => findSearchBox().vm.$emit('input', term); afterEach(() => { wrapper.destroy(); }); - it('renders the dropdown', () => { - expect(findDropdown().exists()).toBe(true); - }); + describe('default', () => { + beforeEach(() => { + wrapper = createComponent(); + }); - it('renders each dropdown item', () => { - const items = findDropdownItems().wrappers; - expect(items).toHaveLength(flatNamespaces().length); - }); + it('renders the dropdown', () => { + expect(findDropdown().exists()).toBe(true); + }); - it('renders the human name for each item', () => { - const dropdownItems = wrappersText(findDropdownItems()); - const flatNames = flatNamespaces().map(({ humanName }) => humanName); - expect(dropdownItems).toEqual(flatNames); - }); + it('renders each dropdown item', () => { + expect(findDropdownItemsTexts()).toEqual(FLAT_NAMESPACES.map((x) => x.humanName)); + }); + + it('renders default dropdown text', () => { + expect(findDropdownText()).toBe(i18n.DEFAULT_TEXT); + }); + + it('splits group and user namespaces', () => { + const headers = findSectionHeaders(); + expect(wrappersText(headers)).toEqual([i18n.GROUPS, i18n.USERS]); + }); - it('sets the initial dropdown text', () => { - expect(selectedDropdownItemText()).toBe(i18n.DEFAULT_TEXT); + it('does not render wrapper as full width', () => { + expect(findDropdown().attributes('block')).toBeUndefined(); + }); }); - it('splits group and user namespaces', () => { - const headers = findSectionHeaders(); - expect(headers).toHaveLength(2); - expect(wrappersText(headers)).toEqual([i18n.GROUPS, i18n.USERS]); + it('with defaultText, it overrides dropdown text', () => { + const textOverride = 'Select an option'; + + wrapper = createComponent({ defaultText: textOverride }); + + expect(findDropdownText()).toBe(textOverride); }); - it('sets the dropdown to full width', () => { - expect(findDropdownAttributes('block')).toBeUndefined(); + it('with includeHeaders=false, hides group/user headers', () => { + wrapper = createComponent({ includeHeaders: false }); + + expect(findSectionHeaders()).toHaveLength(0); + }); + it('with fullWidth=true, sets the dropdown to full width', () => { wrapper = createComponent({ fullWidth: true }); - expect(findDropdownAttributes('block')).not.toBeUndefined(); - expect(findDropdownAttributes('block')).toBe('true'); + expect(findDropdown().attributes('block')).toBe('true'); + }); + + describe('with search', () => { + it.each` + term | includeEmptyNamespace | expectedItems + ${''} | ${false} | ${[...groupNamespaces, ...userNamespaces]} + ${'sub'} | ${false} | ${[groupNamespaces[1]]} + ${'User'} | ${false} | ${[...userNamespaces]} + ${'User'} | ${true} | ${[...userNamespaces]} + ${'namespace'} | ${true} | ${[EMPTY_NAMESPACE_ITEM, ...userNamespaces]} + `( + 'with term=$term and includeEmptyNamespace=$includeEmptyNamespace, should show $expectedItems.length', + async ({ term, includeEmptyNamespace, expectedItems }) => { + wrapper = createComponent({ + includeEmptyNamespace, + emptyNamespaceTitle: EMPTY_NAMESPACE_TITLE, + }); + + search(term); + + await nextTick(); + + const expected = expectedItems.map((x) => x.humanName); + + expect(findDropdownItemsTexts()).toEqual(expected); + }, + ); }); describe('with a selected namespace', () => { const selectedGroupIndex = 1; - const selectedItem = group[selectedGroupIndex]; + const selectedItem = groupNamespaces[selectedGroupIndex]; beforeEach(() => { + wrapper = createComponent(); + findDropdownItems().at(selectedGroupIndex).vm.$emit('click'); }); it('sets the dropdown text', () => { - expect(selectedDropdownItemText()).toBe(selectedItem.humanName); + expect(findDropdownText()).toBe(selectedItem.humanName); }); it('emits the `select` event when a namespace is selected', () => { @@ -83,4 +133,37 @@ describe('Namespace Select', () => { expect(wrapper.emitted('select')).toEqual([args]); }); }); + + describe('with an empty namespace option', () => { + beforeEach(() => { + wrapper = createComponent({ + includeEmptyNamespace: true, + emptyNamespaceTitle: EMPTY_NAMESPACE_TITLE, + }); + }); + + it('includes the empty namespace', () => { + const first = findDropdownItems().at(0); + + expect(first.text()).toBe(EMPTY_NAMESPACE_TITLE); + }); + + it('emits the `select` event when a namespace is selected', () => { + findDropdownItems().at(0).vm.$emit('click'); + + expect(wrapper.emitted('select')).toEqual([[EMPTY_NAMESPACE_ITEM]]); + }); + + it.each` + desc | term | shouldShow + ${'should hide empty option'} | ${'group'} | ${false} + ${'should show empty option'} | ${'Empty'} | ${true} + `('when search for $term, $desc', async ({ term, shouldShow }) => { + search(term); + + await nextTick(); + + expect(findDropdownItemsTexts().includes(EMPTY_NAMESPACE_TITLE)).toBe(shouldShow); + }); + }); }); diff --git a/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js b/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js index 835759b1f20..accbf14572d 100644 --- a/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js +++ b/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js @@ -1,5 +1,6 @@ import { GlIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue'; describe('Issue Warning Component', () => { @@ -64,7 +65,7 @@ describe('Issue Warning Component', () => { expect(findConfidentialBlock().exists()).toBe(true); expect(findConfidentialBlock().element).toMatchSnapshot(); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findConfidentialBlock(wrapper).text()).toContain('This is a confidential issue.'); }); @@ -154,15 +155,15 @@ describe('Issue Warning Component', () => { noteableType: 'Epic', }); - await wrapperLocked.vm.$nextTick(); + await nextTick(); expect(findLockedBlock(wrapperLocked).text()).toContain('This epic is locked.'); - await wrapperConfidential.vm.$nextTick(); + await nextTick(); expect(findConfidentialBlock(wrapperConfidential).text()).toContain( 'This is a confidential epic.', ); - await wrapperLockedAndConfidential.vm.$nextTick(); + await nextTick(); expect(findLockedAndConfidentialBlock(wrapperLockedAndConfidential).text()).toContain( 'This epic is confidential and locked.', ); @@ -179,15 +180,15 @@ describe('Issue Warning Component', () => { noteableType: 'MergeRequest', }); - await wrapperLocked.vm.$nextTick(); + await nextTick(); expect(findLockedBlock(wrapperLocked).text()).toContain('This merge request is locked.'); - await wrapperConfidential.vm.$nextTick(); + await nextTick(); expect(findConfidentialBlock(wrapperConfidential).text()).toContain( 'This is a confidential merge request.', ); - await wrapperLockedAndConfidential.vm.$nextTick(); + await nextTick(); expect(findLockedAndConfidentialBlock(wrapperLockedAndConfidential).text()).toContain( 'This merge request is confidential and locked.', ); diff --git a/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js b/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js index b330b4f5657..36050a42da7 100644 --- a/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js +++ b/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js @@ -1,5 +1,6 @@ import { GlAlert, GlBadge, GlPagination, GlTabs, GlTab } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import Tracking from '~/tracking'; import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants'; import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; @@ -219,21 +220,21 @@ describe('AlertManagementEmptyState', () => { it('returns prevPage button', async () => { findPagination().vm.$emit('input', 3); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findPagination().findAll('.page-item').at(0).text()).toBe('Prev'); }); it('returns prevPage number', async () => { findPagination().vm.$emit('input', 3); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.previousPage).toBe(2); }); it('returns 0 when it is the first page', async () => { findPagination().vm.$emit('input', 1); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.previousPage).toBe(0); }); }); @@ -242,7 +243,7 @@ describe('AlertManagementEmptyState', () => { it('returns nextPage button', async () => { findPagination().vm.$emit('input', 3); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findPagination().findAll('.page-item').at(1).text()).toBe('Next'); }); @@ -257,14 +258,14 @@ describe('AlertManagementEmptyState', () => { }); findPagination().vm.$emit('input', 1); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.nextPage).toBe(2); }); it('returns `null` when currentPage is already last page', async () => { findStatusTabs().vm.$emit('input', 1); findPagination().vm.$emit('input', 1); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.nextPage).toBeNull(); }); }); @@ -319,7 +320,7 @@ describe('AlertManagementEmptyState', () => { searchTerm, }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.filteredSearchValue).toEqual([searchTerm]); }); diff --git a/spec/frontend/vue_shared/components/pikaday_spec.js b/spec/frontend/vue_shared/components/pikaday_spec.js deleted file mode 100644 index fed4ce5e696..00000000000 --- a/spec/frontend/vue_shared/components/pikaday_spec.js +++ /dev/null @@ -1,41 +0,0 @@ -import { GlDatepicker } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import datePicker from '~/vue_shared/components/pikaday.vue'; - -describe('datePicker', () => { - let wrapper; - - const buildWrapper = (propsData = {}) => { - wrapper = shallowMount(datePicker, { - propsData, - }); - }; - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - it('should emit newDateSelected when GlDatePicker emits the input event', () => { - const minDate = new Date(); - const maxDate = new Date(); - const selectedDate = new Date(); - const theDate = selectedDate.toISOString().slice(0, 10); - - buildWrapper({ minDate, maxDate, selectedDate }); - - expect(wrapper.find(GlDatepicker).props()).toMatchObject({ - minDate, - maxDate, - value: selectedDate, - }); - wrapper.find(GlDatepicker).vm.$emit('input', selectedDate); - expect(wrapper.emitted('newDateSelected')[0][0]).toBe(theDate); - }); - it('should emit the hidePicker event when GlDatePicker emits the close event', () => { - buildWrapper(); - - wrapper.find(GlDatepicker).vm.$emit('close'); - - expect(wrapper.emitted('hidePicker')).toHaveLength(1); - }); -}); diff --git a/spec/frontend/vue_shared/components/project_avatar/default_spec.js b/spec/frontend/vue_shared/components/project_avatar/default_spec.js index 84dad2374cb..d042db6051c 100644 --- a/spec/frontend/vue_shared/components/project_avatar/default_spec.js +++ b/spec/frontend/vue_shared/components/project_avatar/default_spec.js @@ -1,4 +1,4 @@ -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import mountComponent from 'helpers/vue_mount_component_helper'; import { projectData } from 'jest/ide/mock_data'; import { TEST_HOST } from 'spec/test_constants'; @@ -19,7 +19,7 @@ describe('ProjectAvatarDefault component', () => { vm.$destroy(); }); - it('renders identicon if project has no avatar_url', (done) => { + it('renders identicon if project has no avatar_url', async () => { const expectedText = getFirstCharacterCapitalized(projectData.name); vm.project = { @@ -27,18 +27,14 @@ describe('ProjectAvatarDefault component', () => { avatar_url: null, }; - vm.$nextTick() - .then(() => { - const identiconEl = vm.$el.querySelector('.identicon'); + await nextTick(); + const identiconEl = vm.$el.querySelector('.identicon'); - expect(identiconEl).not.toBe(null); - expect(identiconEl.textContent.trim()).toEqual(expectedText); - }) - .then(done) - .catch(done.fail); + expect(identiconEl).not.toBe(null); + expect(identiconEl.textContent.trim()).toEqual(expectedText); }); - it('renders avatar image if project has avatar_url', (done) => { + it('renders avatar image if project has avatar_url', async () => { const avatarUrl = `${TEST_HOST}/images/home/nasa.svg`; vm.project = { @@ -46,13 +42,9 @@ describe('ProjectAvatarDefault component', () => { avatar_url: avatarUrl, }; - vm.$nextTick() - .then(() => { - expect(vm.$el.querySelector('.avatar')).not.toBeNull(); - expect(vm.$el.querySelector('.identicon')).toBeNull(); - expect(vm.$el.querySelector('img')).toHaveAttr('src', avatarUrl); - }) - .then(done) - .catch(done.fail); + await nextTick(); + expect(vm.$el.querySelector('.avatar')).not.toBeNull(); + expect(vm.$el.querySelector('.identicon')).toBeNull(); + expect(vm.$el.querySelector('img')).toHaveAttr('src', avatarUrl); }); }); diff --git a/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js b/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js index 34cee10392d..379e60c1b2d 100644 --- a/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js +++ b/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js @@ -1,7 +1,7 @@ import { GlSearchBoxByType, GlInfiniteScroll } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import { head } from 'lodash'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import mockProjects from 'test_fixtures_static/projects.json'; import { trimText } from 'helpers/text_helper'; import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue'; @@ -77,39 +77,36 @@ describe('ProjectSelector component', () => { expect(vm.$emit).toHaveBeenCalledWith('projectClicked', head(searchResults)); }); - it(`shows a "no results" message if showNoResultsMessage === true`, () => { + it(`shows a "no results" message if showNoResultsMessage === true`, async () => { wrapper.setProps({ showNoResultsMessage: true }); - return vm.$nextTick().then(() => { - const noResultsEl = wrapper.find('.js-no-results-message'); + await nextTick(); + const noResultsEl = wrapper.find('.js-no-results-message'); - expect(noResultsEl.exists()).toBe(true); - expect(trimText(noResultsEl.text())).toEqual('Sorry, no projects matched your search'); - }); + expect(noResultsEl.exists()).toBe(true); + expect(trimText(noResultsEl.text())).toEqual('Sorry, no projects matched your search'); }); - it(`shows a "minimum search query" message if showMinimumSearchQueryMessage === true`, () => { + it(`shows a "minimum search query" message if showMinimumSearchQueryMessage === true`, async () => { wrapper.setProps({ showMinimumSearchQueryMessage: true }); - return vm.$nextTick().then(() => { - const minimumSearchEl = wrapper.find('.js-minimum-search-query-message'); + await nextTick(); + const minimumSearchEl = wrapper.find('.js-minimum-search-query-message'); - expect(minimumSearchEl.exists()).toBe(true); - expect(trimText(minimumSearchEl.text())).toEqual('Enter at least three characters to search'); - }); + expect(minimumSearchEl.exists()).toBe(true); + expect(trimText(minimumSearchEl.text())).toEqual('Enter at least three characters to search'); }); - it(`shows a error message if showSearchErrorMessage === true`, () => { + it(`shows a error message if showSearchErrorMessage === true`, async () => { wrapper.setProps({ showSearchErrorMessage: true }); - return vm.$nextTick().then(() => { - const errorMessageEl = wrapper.find('.js-search-error-message'); + await nextTick(); + const errorMessageEl = wrapper.find('.js-search-error-message'); - expect(errorMessageEl.exists()).toBe(true); - expect(trimText(errorMessageEl.text())).toEqual( - 'Something went wrong, unable to search projects', - ); - }); + expect(errorMessageEl.exists()).toBe(true); + expect(trimText(errorMessageEl.text())).toEqual( + 'Something went wrong, unable to search projects', + ); }); describe('the search results legend', () => { @@ -121,7 +118,7 @@ describe('ProjectSelector component', () => { ${2} | ${3} | ${'Showing 2 of 3 projects'} `( 'is "$expected" given $count results are showing out of $total', - ({ count, total, expected }) => { + async ({ count, total, expected }) => { search('gitlab ui'); wrapper.setProps({ @@ -129,9 +126,8 @@ describe('ProjectSelector component', () => { totalResults: total, }); - return wrapper.vm.$nextTick().then(() => { - expect(findLegendText()).toBe(expected); - }); + await nextTick(); + expect(findLegendText()).toBe(expected); }, ); diff --git a/spec/frontend/vue_shared/components/registry/list_item_spec.js b/spec/frontend/vue_shared/components/registry/list_item_spec.js index ca4bf0b0652..1b93292e37b 100644 --- a/spec/frontend/vue_shared/components/registry/list_item_spec.js +++ b/spec/frontend/vue_shared/components/registry/list_item_spec.js @@ -1,5 +1,6 @@ import { GlButton } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import component from '~/vue_shared/components/registry/list_item.vue'; describe('list item', () => { @@ -70,10 +71,10 @@ describe('list item', () => { it('are visible when details is shown', async () => { mountComponent({}, slotMocks); - await wrapper.vm.$nextTick(); + await nextTick(); findToggleDetailsButton().vm.$emit('click'); - await wrapper.vm.$nextTick(); + await nextTick(); slotNames.forEach((name) => { expect(findDetailsSlot(name).exists()).toBe(true); }); @@ -90,7 +91,7 @@ describe('list item', () => { describe('details toggle button', () => { it('is visible when at least one details slot exists', async () => { mountComponent({}, { 'details-foo': '<span></span>' }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findToggleDetailsButton().exists()).toBe(true); }); diff --git a/spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js b/spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js index 40f0c0f29f2..7536df24ac6 100644 --- a/spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js +++ b/spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js @@ -1,6 +1,6 @@ import { mount } from '@vue/test-utils'; import $ from 'jquery'; -import Vue from 'vue'; +import { nextTick } from 'vue'; import ResizableChartContainer from '~/vue_shared/components/resizable_chart/resizable_chart_container.vue'; jest.mock('~/lib/utils/common_utils', () => ({ @@ -35,7 +35,7 @@ describe('Resizable Chart Container', () => { expect(wrapper.element).toMatchSnapshot(); }); - it('updates the slot width and height props', () => { + it('updates the slot width and height props', async () => { const width = 1920; const height = 1080; @@ -44,13 +44,12 @@ describe('Resizable Chart Container', () => { $(document).trigger('content.resize'); - return Vue.nextTick().then(() => { - const widthNode = wrapper.find('.slot > .width'); - const heightNode = wrapper.find('.slot > .height'); + await nextTick(); + const widthNode = wrapper.find('.slot > .width'); + const heightNode = wrapper.find('.slot > .height'); - expect(parseInt(widthNode.text(), 10)).toEqual(width); - expect(parseInt(heightNode.text(), 10)).toEqual(height); - }); + expect(parseInt(widthNode.text(), 10)).toEqual(width); + expect(parseInt(heightNode.text(), 10)).toEqual(height); }); it('calls onResize on manual resize', () => { diff --git a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js index e74a867ec97..0da9939e97f 100644 --- a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js +++ b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js @@ -77,8 +77,7 @@ describe('RunnerInstructionsModal component', () => { runnerSetupInstructionsHandler = jest.fn().mockResolvedValue(mockGraphqlInstructions); createComponent(); - - await nextTick(); + await waitForPromises(); }); afterEach(() => { @@ -113,13 +112,15 @@ describe('RunnerInstructionsModal component', () => { }); }); - it('binary instructions are shown', () => { + it('binary instructions are shown', async () => { + await waitForPromises(); const instructions = findBinaryInstructions().text(); expect(instructions).toBe(installInstructions); }); - it('register command is shown with a replaced token', () => { + it('register command is shown with a replaced token', async () => { + await waitForPromises(); const instructions = findRegisterCommand().text(); expect(instructions).toBe( @@ -130,7 +131,7 @@ describe('RunnerInstructionsModal component', () => { describe('when a register token is not shown', () => { beforeEach(async () => { createComponent({ props: { registrationToken: undefined } }); - await nextTick(); + await waitForPromises(); }); it('register command is shown without a defined registration token', () => { @@ -198,16 +199,17 @@ describe('RunnerInstructionsModal component', () => { expect(findSkeletonLoader().exists()).toBe(true); expect(findGlLoadingIcon().exists()).toBe(false); - await nextTick(); // wait for platforms + await nextTick(); + jest.runOnlyPendingTimers(); + await nextTick(); + await nextTick(); expect(findGlLoadingIcon().exists()).toBe(true); }); it('once loaded, should not show a loading state', async () => { createComponent(); - - await nextTick(); // wait for platforms - await nextTick(); // wait for architectures + await waitForPromises(); expect(findSkeletonLoader().exists()).toBe(false); expect(findGlLoadingIcon().exists()).toBe(false); diff --git a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js index 23f8d6afcb5..9a95a838291 100644 --- a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js +++ b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js @@ -22,9 +22,9 @@ describe('RunnerInstructions component', () => { wrapper.destroy(); }); - it('should show the "Show Runner installation instructions" button', () => { + it('should show the "Show runner installation instructions" button', () => { expect(findModalButton().exists()).toBe(true); - expect(findModalButton().text()).toBe('Show Runner installation instructions'); + expect(findModalButton().text()).toBe('Show runner installation instructions'); }); it('should not render the modal once mounted', () => { diff --git a/spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js b/spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js deleted file mode 100644 index 79e41ed0c9e..00000000000 --- a/spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js +++ /dev/null @@ -1,68 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import { GlIcon } from '@gitlab/ui'; -import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; - -import CollapsedCalendarIcon from '~/vue_shared/components/sidebar/collapsed_calendar_icon.vue'; - -describe('CollapsedCalendarIcon', () => { - let wrapper; - - const defaultProps = { - containerClass: 'test-class', - text: 'text', - tooltipText: 'tooltip text', - showIcon: false, - }; - - const createComponent = ({ props = {} } = {}) => { - wrapper = shallowMount(CollapsedCalendarIcon, { - propsData: { ...defaultProps, ...props }, - directives: { - GlTooltip: createMockDirective(), - }, - }); - }; - - beforeEach(() => { - createComponent(); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - const findGlIcon = () => wrapper.findComponent(GlIcon); - const getTooltip = () => getBinding(wrapper.element, 'gl-tooltip'); - - it('adds class to container', () => { - expect(wrapper.classes()).toContain(defaultProps.containerClass); - }); - - it('does not render calendar icon when showIcon is false', () => { - expect(findGlIcon().exists()).toBe(false); - }); - - it('renders calendar icon when showIcon is true', () => { - createComponent({ - props: { showIcon: true }, - }); - - expect(findGlIcon().exists()).toBe(true); - }); - - it('renders text', () => { - expect(wrapper.text()).toBe(defaultProps.text); - }); - - it('renders tooltipText as tooltip', () => { - expect(getTooltip().value).toBe(defaultProps.tooltipText); - }); - - it('emits click event when container is clicked', async () => { - wrapper.trigger('click'); - - await wrapper.vm.$nextTick(); - - expect(wrapper.emitted('click')[0]).toBeDefined(); - }); -}); diff --git a/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js b/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js deleted file mode 100644 index 263d1e9d947..00000000000 --- a/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js +++ /dev/null @@ -1,125 +0,0 @@ -import { GlLoadingIcon } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; -import DatePicker from '~/vue_shared/components/pikaday.vue'; -import SidebarDatePicker from '~/vue_shared/components/sidebar/date_picker.vue'; - -describe('SidebarDatePicker', () => { - let wrapper; - - const createComponent = (propsData = {}, data = {}) => { - wrapper = mount(SidebarDatePicker, { - propsData, - data: () => data, - }); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - const findDatePicker = () => wrapper.findComponent(DatePicker); - const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); - const findEditButton = () => wrapper.find('.title .btn-blank'); - const findRemoveButton = () => wrapper.find('.value-content .btn-blank'); - const findSidebarToggle = () => wrapper.find('.title .gutter-toggle'); - const findValueContent = () => wrapper.find('.value-content'); - - it('should emit toggleCollapse when collapsed toggle sidebar is clicked', () => { - createComponent(); - - wrapper.find('.issuable-sidebar-header .gutter-toggle').trigger('click'); - - expect(wrapper.emitted('toggleCollapse')).toEqual([[]]); - }); - - it('should render collapsed-calendar-icon', () => { - createComponent(); - - expect(wrapper.find('.sidebar-collapsed-icon').exists()).toBe(true); - }); - - it('should render value when not editing', () => { - createComponent(); - - expect(findValueContent().exists()).toBe(true); - }); - - it('should render None if there is no selectedDate', () => { - createComponent(); - - expect(findValueContent().text()).toBe('None'); - }); - - it('should render date-picker when editing', () => { - createComponent({}, { editing: true }); - - expect(findDatePicker().exists()).toBe(true); - }); - - it('should render label', () => { - const label = 'label'; - createComponent({ label }); - expect(wrapper.find('.title').text()).toBe(label); - }); - - it('should render loading-icon when isLoading', () => { - createComponent({ isLoading: true }); - expect(findLoadingIcon().exists()).toBe(true); - }); - - describe('editable', () => { - beforeEach(() => { - createComponent({ editable: true }); - }); - - it('should render edit button', () => { - expect(findEditButton().text()).toBe('Edit'); - }); - - it('should enable editing when edit button is clicked', async () => { - findEditButton().trigger('click'); - - await wrapper.vm.$nextTick(); - - expect(wrapper.vm.editing).toBe(true); - }); - }); - - it('should render date if selectedDate', () => { - createComponent({ selectedDate: new Date('07/07/2017') }); - - expect(wrapper.find('.value-content strong').text()).toBe('Jul 7, 2017'); - }); - - describe('selectedDate and editable', () => { - beforeEach(() => { - createComponent({ selectedDate: new Date('07/07/2017'), editable: true }); - }); - - it('should render remove button if selectedDate and editable', () => { - expect(findRemoveButton().text()).toBe('remove'); - }); - - it('should emit saveDate with null when remove button is clicked', () => { - findRemoveButton().trigger('click'); - - expect(wrapper.emitted('saveDate')).toEqual([[null]]); - }); - }); - - describe('showToggleSidebar', () => { - beforeEach(() => { - createComponent({ showToggleSidebar: true }); - }); - - it('should render toggle-sidebar when showToggleSidebar', () => { - expect(findSidebarToggle().exists()).toBe(true); - }); - - it('should emit toggleCollapse when toggle sidebar is clicked', () => { - findSidebarToggle().trigger('click'); - - expect(wrapper.emitted('toggleCollapse')).toEqual([[]]); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js b/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js index 5336ecc614c..f213e37cbc1 100644 --- a/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js @@ -10,6 +10,7 @@ import { import { shallowMount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; +import { nextTick } from 'vue'; import axios from '~/lib/utils/axios_utils'; import IssuableMoveDropdown from '~/vue_shared/components/sidebar/issuable_move_dropdown.vue'; @@ -74,7 +75,7 @@ describe('IssuableMoveDropdown', () => { searchKey: 'foo', }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.fetchProjects).toHaveBeenCalledWith('foo'); }); @@ -151,7 +152,7 @@ describe('IssuableMoveDropdown', () => { selectedProject, }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.isSelectedProject(project)).toBe(returnValue); }, @@ -164,7 +165,7 @@ describe('IssuableMoveDropdown', () => { selectedProject: null, }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.isSelectedProject(mockProjects[0])).toBe(false); }); @@ -218,7 +219,7 @@ describe('IssuableMoveDropdown', () => { projectsListLoading: true, }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findDropdownEl().find(GlLoadingIcon).exists()).toBe(true); }); @@ -231,7 +232,7 @@ describe('IssuableMoveDropdown', () => { selectedProject: mockProjects[0], }); - await wrapper.vm.$nextTick(); + await nextTick(); const dropdownItems = wrapper.findAll(GlDropdownItem); @@ -251,7 +252,7 @@ describe('IssuableMoveDropdown', () => { }); // Wait for `searchKey` watcher to run. - await wrapper.vm.$nextTick(); + await nextTick(); // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details // eslint-disable-next-line no-restricted-syntax @@ -260,7 +261,7 @@ describe('IssuableMoveDropdown', () => { projectsListLoading: false, }); - await wrapper.vm.$nextTick(); + await nextTick(); const dropdownContentEl = wrapper.find('[data-testid="content"]'); @@ -276,7 +277,7 @@ describe('IssuableMoveDropdown', () => { projectsListLoadFailed: true, }); - await wrapper.vm.$nextTick(); + await nextTick(); const dropdownContentEl = wrapper.find('[data-testid="content"]'); @@ -295,7 +296,7 @@ describe('IssuableMoveDropdown', () => { selectedProject: mockProjects[0], }); - await wrapper.vm.$nextTick(); + await nextTick(); expect( wrapper.find('[data-testid="footer"]').find(GlButton).attributes('disabled'), @@ -352,7 +353,7 @@ describe('IssuableMoveDropdown', () => { projects: mockProjects, }); - await wrapper.vm.$nextTick(); + await nextTick(); wrapper.findAll(GlDropdownItem).at(0).vm.$emit('click', mockEvent); @@ -366,7 +367,7 @@ describe('IssuableMoveDropdown', () => { selectedProject: mockProjects[0], }); - await wrapper.vm.$nextTick(); + await nextTick(); wrapper.find('[data-testid="footer"]').find(GlButton).vm.$emit('click'); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js index c4ed975e746..c05513a6d5f 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js @@ -1,6 +1,6 @@ import { GlIcon, GlButton } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import DropdownButton from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue'; @@ -71,13 +71,12 @@ describe('DropdownButton', () => { expect(dropdownTextEl.text()).toBe('Label'); }); - it('renders provided button text element', () => { + it('renders provided button text element', async () => { store.state.dropdownButtonText = 'Custom label'; const dropdownTextEl = findDropdownText(); - return wrapper.vm.$nextTick().then(() => { - expect(dropdownTextEl.text()).toBe('Custom label'); - }); + await nextTick(); + expect(dropdownTextEl.text()).toBe('Custom label'); }); it('renders chevron icon element', () => { diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js index 0eff6a1dace..0673ffee22b 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js @@ -1,6 +1,6 @@ import { GlButton, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue'; @@ -42,7 +42,7 @@ describe('DropdownContentsCreateView', () => { expect(wrapper.vm.disableCreate).toBe(true); }); - it('returns `true` when `labelCreateInProgress` is true', () => { + it('returns `true` when `labelCreateInProgress` is true', 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({ @@ -51,12 +51,11 @@ describe('DropdownContentsCreateView', () => { }); wrapper.vm.$store.dispatch('requestCreateLabel'); - return wrapper.vm.$nextTick(() => { - expect(wrapper.vm.disableCreate).toBe(true); - }); + await nextTick(); + expect(wrapper.vm.disableCreate).toBe(true); }); - it('returns `false` when label title and color is defined and create request is not already in progress', () => { + it('returns `false` when label title and color is defined and create request is not already in progress', 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({ @@ -64,9 +63,8 @@ describe('DropdownContentsCreateView', () => { selectedColor: '#ff0000', }); - return wrapper.vm.$nextTick(() => { - expect(wrapper.vm.disableCreate).toBe(false); - }); + await nextTick(); + expect(wrapper.vm.disableCreate).toBe(false); }); }); @@ -101,7 +99,7 @@ describe('DropdownContentsCreateView', () => { }); describe('handleCreateClick', () => { - it('calls action `createLabel` with object containing `labelTitle` & `selectedColor`', () => { + it('calls action `createLabel` with object containing `labelTitle` & `selectedColor`', async () => { jest.spyOn(wrapper.vm, 'createLabel').mockImplementation(); // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details // eslint-disable-next-line no-restricted-syntax @@ -112,14 +110,13 @@ describe('DropdownContentsCreateView', () => { wrapper.vm.handleCreateClick(); - return wrapper.vm.$nextTick(() => { - expect(wrapper.vm.createLabel).toHaveBeenCalledWith( - expect.objectContaining({ - title: 'Foo', - color: '#ff0000', - }), - ); - }); + await nextTick(); + expect(wrapper.vm.createLabel).toHaveBeenCalledWith( + expect.objectContaining({ + title: 'Foo', + color: '#ff0000', + }), + ); }); }); }); @@ -169,25 +166,22 @@ describe('DropdownContentsCreateView', () => { }); }); - it('renders color input element', () => { + it('renders color input element', 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({ selectedColor: '#ff0000', }); - return wrapper.vm.$nextTick(() => { - const colorPreviewEl = wrapper.find( - '.color-input-container > .dropdown-label-color-preview', - ); - const colorInputEl = wrapper.find('.color-input-container').find(GlFormInput); + await nextTick(); + const colorPreviewEl = wrapper.find('.color-input-container > .dropdown-label-color-preview'); + const colorInputEl = wrapper.find('.color-input-container').find(GlFormInput); - expect(colorPreviewEl.exists()).toBe(true); - expect(colorPreviewEl.attributes('style')).toContain('background-color'); - expect(colorInputEl.exists()).toBe(true); - expect(colorInputEl.attributes('placeholder')).toBe('Use custom color #FF0000'); - expect(colorInputEl.attributes('value')).toBe('#ff0000'); - }); + expect(colorPreviewEl.exists()).toBe(true); + expect(colorPreviewEl.attributes('style')).toContain('background-color'); + expect(colorInputEl.exists()).toBe(true); + expect(colorInputEl.attributes('placeholder')).toBe('Use custom color #FF0000'); + expect(colorInputEl.attributes('value')).toBe('#ff0000'); }); it('renders create button element', () => { @@ -197,15 +191,14 @@ describe('DropdownContentsCreateView', () => { expect(createBtnEl.text()).toContain('Create'); }); - it('shows gl-loading-icon within create button element when `labelCreateInProgress` is `true`', () => { + it('shows gl-loading-icon within create button element when `labelCreateInProgress` is `true`', async () => { wrapper.vm.$store.dispatch('requestCreateLabel'); - return wrapper.vm.$nextTick(() => { - const loadingIconEl = wrapper.find('.dropdown-actions').find(GlLoadingIcon); + await nextTick(); + const loadingIconEl = wrapper.find('.dropdown-actions').find(GlLoadingIcon); - expect(loadingIconEl.exists()).toBe(true); - expect(loadingIconEl.isVisible()).toBe(true); - }); + expect(loadingIconEl.exists()).toBe(true); + expect(loadingIconEl.isVisible()).toBe(true); }); it('renders cancel button element', () => { diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js index 93a0e2f75bb..42202db4935 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js @@ -6,7 +6,7 @@ import { GlLink, } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes'; import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue'; @@ -114,7 +114,7 @@ describe('DropdownContentsLabelsView', () => { wrapper.vm.$store.dispatch('receiveLabelsSuccess', labels); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.showNoMatchingResultsMessage).toBe(returnValue); }, @@ -249,7 +249,7 @@ describe('DropdownContentsLabelsView', () => { expect(wrapper.vm.toggleDropdownContents).toHaveBeenCalled(); }); - it('calls action `scrollIntoViewIfNeeded` in next tick when any key is pressed', () => { + it('calls action `scrollIntoViewIfNeeded` in next tick when any key is pressed', async () => { jest.spyOn(wrapper.vm, 'scrollIntoViewIfNeeded').mockImplementation(); // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details // eslint-disable-next-line no-restricted-syntax @@ -261,9 +261,8 @@ describe('DropdownContentsLabelsView', () => { keyCode: DOWN_KEY_CODE, }); - return wrapper.vm.$nextTick(() => { - expect(wrapper.vm.scrollIntoViewIfNeeded).toHaveBeenCalled(); - }); + await nextTick(); + expect(wrapper.vm.scrollIntoViewIfNeeded).toHaveBeenCalled(); }); }); @@ -294,15 +293,14 @@ describe('DropdownContentsLabelsView', () => { expect(wrapper.find(GlIntersectionObserver).exists()).toBe(true); }); - it('renders gl-loading-icon component when `labelsFetchInProgress` prop is true', () => { + it('renders gl-loading-icon component when `labelsFetchInProgress` prop is true', async () => { wrapper.vm.$store.dispatch('requestLabels'); - return wrapper.vm.$nextTick(() => { - const loadingIconEl = findLoadingIcon(); + await nextTick(); + const loadingIconEl = findLoadingIcon(); - expect(loadingIconEl.exists()).toBe(true); - expect(loadingIconEl.attributes('class')).toContain('labels-fetch-loading'); - }); + expect(loadingIconEl.exists()).toBe(true); + expect(loadingIconEl.attributes('class')).toContain('labels-fetch-loading'); }); it('renders dropdown title element', () => { @@ -339,47 +337,44 @@ describe('DropdownContentsLabelsView', () => { expect(wrapper.findAll(LabelItem)).toHaveLength(mockLabels.length); }); - it('renders label element with `highlight` set to true when value of `currentHighlightItem` is more than -1', () => { + it('renders label element with `highlight` set to true when value of `currentHighlightItem` is more than -1', 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({ currentHighlightItem: 0, }); - return wrapper.vm.$nextTick(() => { - const labelItemEl = findDropdownContent().find(LabelItem); + await nextTick(); + const labelItemEl = findDropdownContent().find(LabelItem); - expect(labelItemEl.attributes('highlight')).toBe('true'); - }); + expect(labelItemEl.attributes('highlight')).toBe('true'); }); - it('renders element containing "No matching results" when `searchKey` does not match with any label', () => { + it('renders element containing "No matching results" when `searchKey` does not match with any label', 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({ searchKey: 'abc', }); - return wrapper.vm.$nextTick(() => { - const noMatchEl = findDropdownContent().find('li'); + await nextTick(); + const noMatchEl = findDropdownContent().find('li'); - expect(noMatchEl.isVisible()).toBe(true); - expect(noMatchEl.text()).toContain('No matching results'); - }); + expect(noMatchEl.isVisible()).toBe(true); + expect(noMatchEl.text()).toContain('No matching results'); }); - it('renders empty content while loading', () => { + it('renders empty content while loading', async () => { wrapper.vm.$store.state.labelsFetchInProgress = true; - return wrapper.vm.$nextTick(() => { - const dropdownContent = findDropdownContent(); - const loadingIcon = findLoadingIcon(); + await nextTick(); + const dropdownContent = findDropdownContent(); + const loadingIcon = findLoadingIcon(); - expect(dropdownContent.exists()).toBe(true); - expect(dropdownContent.isVisible()).toBe(true); - expect(loadingIcon.exists()).toBe(true); - expect(loadingIcon.isVisible()).toBe(true); - }); + expect(dropdownContent.exists()).toBe(true); + expect(dropdownContent.isVisible()).toBe(true); + expect(loadingIcon.exists()).toBe(true); + expect(loadingIcon.isVisible()).toBe(true); }); it('renders footer list items', () => { @@ -393,14 +388,13 @@ describe('DropdownContentsLabelsView', () => { expect(manageLabelsLink.text()).toBe('Manage labels'); }); - it('does not render "Create label" footer link when `state.allowLabelCreate` is `false`', () => { + it('does not render "Create label" footer link when `state.allowLabelCreate` is `false`', async () => { wrapper.vm.$store.state.allowLabelCreate = false; - return wrapper.vm.$nextTick(() => { - const createLabelLink = findDropdownFooter().findAll(GlLink).at(0); + await nextTick(); + const createLabelLink = findDropdownFooter().findAll(GlLink).at(0); - expect(createLabelLink.text()).not.toBe('Create label'); - }); + expect(createLabelLink.text()).not.toBe('Create label'); }); it('does not render footer list items when `state.variant` is "standalone"', () => { diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js index 110c1d1b7eb..84e9f3f41c3 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js @@ -1,6 +1,6 @@ import { GlButton, GlLoadingIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import DropdownTitle from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue'; @@ -47,14 +47,13 @@ describe('DropdownTitle', () => { expect(editBtnEl.text()).toBe('Edit'); }); - it('renders loading icon element when `labelsSelectInProgress` prop is true', () => { + it('renders loading icon element when `labelsSelectInProgress` prop is true', async () => { wrapper.setProps({ labelsSelectInProgress: true, }); - return wrapper.vm.$nextTick(() => { - expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(true); - }); + await nextTick(); + expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(true); }); }); }); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js index a7f9391cb5f..c6400320dea 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js @@ -1,5 +1,6 @@ import { shallowMount } from '@vue/test-utils'; import { GlIcon } from '@gitlab/ui'; +import { nextTick } from 'vue'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import DropdownValueCollapsedComponent from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue'; @@ -42,7 +43,7 @@ describe('DropdownValueCollapsedComponent', () => { wrapper.trigger('click'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.emitted('onValueClick')[0]).toBeDefined(); }); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js index 4b0ba075eda..31819d0e2f7 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js @@ -1,5 +1,5 @@ import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import { isInViewport } from '~/lib/utils/common_utils'; @@ -139,27 +139,26 @@ describe('LabelsSelectRoot', () => { ${'embedded'} | ${'is-embedded'} `( 'renders component root element with CSS class `$cssClass` when `state.variant` is "$variant"', - ({ variant, cssClass }) => { + async ({ variant, cssClass }) => { createComponent({ ...mockConfig, variant, }); - return wrapper.vm.$nextTick(() => { - expect(wrapper.classes()).toContain(cssClass); - }); + await nextTick(); + expect(wrapper.classes()).toContain(cssClass); }, ); it('renders `dropdown-value-collapsed` component when `allowLabelCreate` prop is `true`', async () => { createComponent(); - await wrapper.vm.$nextTick; + await nextTick; expect(wrapper.find(DropdownValueCollapsed).exists()).toBe(true); }); it('renders `dropdown-title` component', async () => { createComponent(); - await wrapper.vm.$nextTick; + await nextTick; expect(wrapper.find(DropdownTitle).exists()).toBe(true); }); @@ -167,7 +166,7 @@ describe('LabelsSelectRoot', () => { createComponent(mockConfig, { default: 'None', }); - await wrapper.vm.$nextTick; + await nextTick; const valueComp = wrapper.find(DropdownValue); @@ -178,14 +177,14 @@ describe('LabelsSelectRoot', () => { it('renders `dropdown-button` component when `showDropdownButton` prop is `true`', async () => { createComponent(); wrapper.vm.$store.dispatch('toggleDropdownButton'); - await wrapper.vm.$nextTick; + await nextTick; expect(wrapper.find(DropdownButton).exists()).toBe(true); }); it('renders `dropdown-contents` component when `showDropdownButton` & `showDropdownContents` prop is `true`', async () => { createComponent(); wrapper.vm.$store.dispatch('toggleDropdownContents'); - await wrapper.vm.$nextTick; + await nextTick; expect(wrapper.find(DropdownContents).exists()).toBe(true); }); @@ -198,22 +197,20 @@ describe('LabelsSelectRoot', () => { wrapper.vm.$store.dispatch('toggleDropdownContents'); }); - it('set direction when out of viewport', () => { + it('set direction when out of viewport', async () => { isInViewport.mockImplementation(() => false); wrapper.vm.setContentIsOnViewport(wrapper.vm.$store.state); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.find(DropdownContents).props('renderOnTop')).toBe(true); - }); + await nextTick(); + expect(wrapper.find(DropdownContents).props('renderOnTop')).toBe(true); }); - it('does not set direction when inside of viewport', () => { + it('does not set direction when inside of viewport', async () => { isInViewport.mockImplementation(() => true); wrapper.vm.setContentIsOnViewport(wrapper.vm.$store.state); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.find(DropdownContents).props('renderOnTop')).toBe(false); - }); + await nextTick(); + expect(wrapper.find(DropdownContents).props('renderOnTop')).toBe(false); }); }, ); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js index a4199bb3e27..67e1a3ce932 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js @@ -117,9 +117,15 @@ describe('LabelsSelectRoot', () => { it('renders dropdown value component when query labels is resolved', () => { expect(findDropdownValue().exists()).toBe(true); - expect(findDropdownValue().props('selectedLabels')).toEqual( - issuableLabelsQueryResponse.data.workspace.issuable.labels.nodes, - ); + expect(findDropdownValue().props('selectedLabels')).toEqual([ + { + color: '#330066', + description: null, + id: 'gid://gitlab/ProjectLabel/1', + title: 'Label1', + textColor: '#000000', + }, + ]); }); it('emits `onLabelRemove` event on dropdown value label remove event', () => { diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js index 6ef54ce37ce..49224fb915c 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js @@ -96,6 +96,7 @@ export const workspaceLabelsQueryResponse = { labels: { nodes: [ { + __typename: 'Label', color: '#330066', description: null, id: 'gid://gitlab/ProjectLabel/1', @@ -103,6 +104,7 @@ export const workspaceLabelsQueryResponse = { textColor: '#000000', }, { + __typename: 'Label', color: '#2f7b2e', description: null, id: 'gid://gitlab/ProjectLabel/2', @@ -125,6 +127,7 @@ export const issuableLabelsQueryResponse = { labels: { nodes: [ { + __typename: 'Label', color: '#330066', description: null, id: 'gid://gitlab/ProjectLabel/1', diff --git a/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js b/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js index a6c9bda1aa2..267a467059d 100644 --- a/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js @@ -1,6 +1,7 @@ import { GlButton } from '@gitlab/ui'; import { mount, shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import ToggleSidebar from '~/vue_shared/components/sidebar/toggle_sidebar.vue'; describe('ToggleSidebar', () => { @@ -38,7 +39,7 @@ describe('ToggleSidebar', () => { createComponent({ mountFn: mount }); findGlButton().trigger('click'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.emitted('toggle')[0]).toBeDefined(); }); diff --git a/spec/frontend/vue_shared/components/source_viewer_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js index 094d8d42a47..2010bac7060 100644 --- a/spec/frontend/vue_shared/components/source_viewer_spec.js +++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js @@ -1,8 +1,10 @@ import hljs from 'highlight.js/lib/core'; +import { GlLoadingIcon } from '@gitlab/ui'; import Vue, { nextTick } from 'vue'; import VueRouter from 'vue-router'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import SourceViewer from '~/vue_shared/components/source_viewer.vue'; +import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vue'; +import { ROUGE_TO_HLJS_LANGUAGE_MAP } from '~/vue_shared/components/source_viewer/constants'; import LineNumbers from '~/vue_shared/components/line_numbers.vue'; import waitForPromises from 'helpers/wait_for_promises'; @@ -12,42 +14,50 @@ const router = new VueRouter(); describe('Source Viewer component', () => { let wrapper; + const language = 'docker'; + const mappedLanguage = ROUGE_TO_HLJS_LANGUAGE_MAP[language]; const content = `// Some source code`; + const DEFAULT_BLOB_DATA = { language, rawTextBlob: content }; const highlightedContent = `<span data-testid='test-highlighted' id='LC1'>${content}</span><span id='LC2'></span>`; - const language = 'javascript'; - hljs.highlight.mockImplementation(() => ({ value: highlightedContent })); - hljs.highlightAuto.mockImplementation(() => ({ value: highlightedContent })); - - const createComponent = async (props = {}) => { + const createComponent = async (blob = {}) => { wrapper = shallowMountExtended(SourceViewer, { router, - propsData: { content, language, ...props }, + propsData: { blob: { ...DEFAULT_BLOB_DATA, ...blob } }, }); await waitForPromises(); }; + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findLineNumbers = () => wrapper.findComponent(LineNumbers); const findHighlightedContent = () => wrapper.findByTestId('test-highlighted'); const findFirstLine = () => wrapper.find('#LC1'); - beforeEach(() => createComponent()); + beforeEach(() => { + hljs.highlight.mockImplementation(() => ({ value: highlightedContent })); + hljs.highlightAuto.mockImplementation(() => ({ value: highlightedContent })); + + return createComponent(); + }); afterEach(() => wrapper.destroy()); describe('highlight.js', () => { it('registers the language definition', async () => { - const languageDefinition = await import(`highlight.js/lib/languages/${language}`); + const languageDefinition = await import(`highlight.js/lib/languages/${mappedLanguage}`); - expect(hljs.registerLanguage).toHaveBeenCalledWith(language, languageDefinition.default); + expect(hljs.registerLanguage).toHaveBeenCalledWith( + mappedLanguage, + languageDefinition.default, + ); }); it('highlights the content', () => { - expect(hljs.highlight).toHaveBeenCalledWith(content, { language }); + expect(hljs.highlight).toHaveBeenCalledWith(content, { language: mappedLanguage }); }); - describe('auto-detect enabled', () => { - beforeEach(() => createComponent({ autoDetect: true })); + describe('auto-detects if a language cannot be loaded', () => { + beforeEach(() => createComponent({ language: 'some_unknown_language' })); it('highlights the content with auto-detection', () => { expect(hljs.highlightAuto).toHaveBeenCalledWith(content); @@ -56,6 +66,13 @@ describe('Source Viewer component', () => { }); describe('rendering', () => { + it('renders a loading icon if no highlighted content is available yet', async () => { + hljs.highlight.mockImplementation(() => ({ value: null })); + await createComponent(); + + expect(findLoadingIcon().exists()).toBe(true); + }); + it('renders Line Numbers', () => { expect(findLineNumbers().props('lines')).toBe(1); }); diff --git a/spec/frontend/vue_shared/components/source_viewer/utils_spec.js b/spec/frontend/vue_shared/components/source_viewer/utils_spec.js new file mode 100644 index 00000000000..937c3b26c67 --- /dev/null +++ b/spec/frontend/vue_shared/components/source_viewer/utils_spec.js @@ -0,0 +1,13 @@ +import { wrapLines } from '~/vue_shared/components/source_viewer/utils'; + +describe('Wrap lines', () => { + it.each` + input | output + ${'line 1'} | ${'<span id="LC1" class="line">line 1</span>'} + ${'line 1\nline 2'} | ${`<span id="LC1" class="line">line 1</span>\n<span id="LC2" class="line">line 2</span>`} + ${'<span class="hljs-code">line 1\nline 2</span>'} | ${`<span id="LC1" class="hljs-code">line 1\n<span id="LC2" class="line">line 2</span></span>`} + ${'<span class="hljs-code">```bash'} | ${'<span id="LC1" class="hljs-code">```bash'} + `('returns lines wrapped in spans containing line numbers', ({ input, output }) => { + expect(wrapLines(input)).toBe(output); + }); +}); diff --git a/spec/frontend/vue_shared/components/split_button_spec.js b/spec/frontend/vue_shared/components/split_button_spec.js index ad11e6519c4..4965969bc3e 100644 --- a/spec/frontend/vue_shared/components/split_button_spec.js +++ b/spec/frontend/vue_shared/components/split_button_spec.js @@ -1,6 +1,7 @@ import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import SplitButton from '~/vue_shared/components/split_button.vue'; const mockActionItems = [ @@ -27,15 +28,15 @@ describe('SplitButton', () => { const findDropdown = () => wrapper.find(GlDropdown); const findDropdownItem = (index = 0) => findDropdown().findAll(GlDropdownItem).at(index); - const selectItem = (index) => { + const selectItem = async (index) => { findDropdownItem(index).vm.$emit('click'); - return wrapper.vm.$nextTick(); + await nextTick(); }; - const clickToggleButton = () => { + const clickToggleButton = async () => { findDropdown().vm.$emit('click'); - return wrapper.vm.$nextTick(); + await nextTick(); }; it('fails for empty actionItems', () => { diff --git a/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap b/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap index 0f1e118d44c..a613b325462 100644 --- a/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap +++ b/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap @@ -6,6 +6,7 @@ exports[`Upload dropzone component correctly overrides description and drop mess > <button class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3" + type="button" > <div class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column" @@ -86,6 +87,7 @@ exports[`Upload dropzone component when dragging renders correct template when d > <button class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3" + type="button" > <div class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column" @@ -170,6 +172,7 @@ exports[`Upload dropzone component when dragging renders correct template when d > <button class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3" + type="button" > <div class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column" @@ -254,6 +257,7 @@ exports[`Upload dropzone component when dragging renders correct template when d > <button class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3" + type="button" > <div class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column" @@ -339,6 +343,7 @@ exports[`Upload dropzone component when dragging renders correct template when d > <button class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3" + type="button" > <div class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column" @@ -424,6 +429,7 @@ exports[`Upload dropzone component when dragging renders correct template when d > <button class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3" + type="button" > <div class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column" @@ -509,6 +515,7 @@ exports[`Upload dropzone component when no slot provided renders default dropzon > <button class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3" + type="button" > <div class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column" diff --git a/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js b/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js index b3cdbccb271..21e9b401215 100644 --- a/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js +++ b/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js @@ -1,5 +1,6 @@ import { GlIcon, GlSprintf } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue'; jest.mock('~/flash'); @@ -15,6 +16,7 @@ describe('Upload dropzone component', () => { const findDropzoneArea = () => wrapper.find('[data-testid="dropzone-area"]'); const findIcon = () => wrapper.find(GlIcon); const findUploadText = () => wrapper.find('[data-testid="upload-text"]').text(); + const findFileInput = () => wrapper.find('input[type="file"]'); function createComponent({ slots = {}, data = {}, props = {} } = {}) { wrapper = shallowMount(UploadDropzone, { @@ -84,47 +86,40 @@ describe('Upload dropzone component', () => { ${'contains text'} | ${mockDragEvent({ types: ['text'] })} ${'contains files and text'} | ${mockDragEvent({ types: ['Files', 'text'] })} ${'contains files'} | ${mockDragEvent({ types: ['Files'] })} - `('renders correct template when drag event $description', ({ eventPayload }) => { + `('renders correct template when drag event $description', async ({ eventPayload }) => { createComponent(); wrapper.trigger('dragenter', eventPayload); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.element).toMatchSnapshot(); - }); + await nextTick(); + expect(wrapper.element).toMatchSnapshot(); }); - it('renders correct template when dragging stops', () => { + it('renders correct template when dragging stops', async () => { createComponent(); wrapper.trigger('dragenter'); - return wrapper.vm - .$nextTick() - .then(() => { - wrapper.trigger('dragleave'); - return wrapper.vm.$nextTick(); - }) - .then(() => { - expect(wrapper.element).toMatchSnapshot(); - }); + + await nextTick(); + wrapper.trigger('dragleave'); + + await nextTick(); + expect(wrapper.element).toMatchSnapshot(); }); }); describe('when dropping', () => { - it('emits upload event', () => { + it('emits upload event', async () => { createComponent(); const mockFile = { name: 'test', type: 'image/jpg' }; const mockEvent = mockDragEvent({ files: [mockFile] }); wrapper.trigger('dragenter', mockEvent); - return wrapper.vm - .$nextTick() - .then(() => { - wrapper.trigger('drop', mockEvent); - return wrapper.vm.$nextTick(); - }) - .then(() => { - expect(wrapper.emitted().change[0]).toEqual([[mockFile]]); - }); + + await nextTick(); + wrapper.trigger('drop', mockEvent); + + await nextTick(); + expect(wrapper.emitted().change[0]).toEqual([[mockFile]]); }); }); @@ -203,4 +198,60 @@ describe('Upload dropzone component', () => { expect(wrapper.element).toMatchSnapshot(); }); + + describe('file input form name', () => { + it('applies inputFieldName as file input name', () => { + createComponent({ props: { inputFieldName: 'test_field_name' } }); + expect(findFileInput().attributes('name')).toBe('test_field_name'); + }); + + it('uses default file input name if no inputFieldName provided', () => { + createComponent(); + expect(findFileInput().attributes('name')).toBe('upload_file'); + }); + }); + + describe('updates file input files value', () => { + // NOTE: the component assigns dropped files from the drop event to the + // input.files property. There's a restriction that nothing but a FileList + // can be assigned to this property. While FileList can't be created + // manually: it has no constructor. And currently there's no good workaround + // for jsdom. So we have to stub the file input in vm.$refs to ensure that + // the files property is updated. This enforces following tests to know a + // bit too much about the SUT internals See this thread for more details on + // FileList in jsdom: https://github.com/jsdom/jsdom/issues/1272 + function stubFileInputOnWrapper() { + const fakeFileInput = { files: [] }; + wrapper.vm.$refs.fileUpload = fakeFileInput; + } + + it('assigns dragged files to the input files property', async () => { + const mockFile = { name: 'test', type: 'image/jpg' }; + const mockEvent = mockDragEvent({ files: [mockFile] }); + createComponent({ props: { shouldUpdateInputOnFileDrop: true } }); + stubFileInputOnWrapper(); + + wrapper.trigger('dragenter', mockEvent); + await nextTick(); + wrapper.trigger('drop', mockEvent); + await nextTick(); + + expect(wrapper.vm.$refs.fileUpload.files).toEqual([mockFile]); + }); + + it('throws an error when multiple files are dropped on a single file input dropzone', async () => { + const mockFile = { name: 'test', type: 'image/jpg' }; + const mockEvent = mockDragEvent({ files: [mockFile, mockFile] }); + createComponent({ props: { shouldUpdateInputOnFileDrop: true, singleFileSelection: true } }); + stubFileInputOnWrapper(); + + wrapper.trigger('dragenter', mockEvent); + await nextTick(); + wrapper.trigger('drop', mockEvent); + await nextTick(); + + expect(wrapper.vm.$refs.fileUpload.files).toEqual([]); + expect(wrapper.emitted('error')).toHaveLength(1); + }); + }); }); diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js index 1d15da491cd..66bb234aef6 100644 --- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js +++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js @@ -1,5 +1,6 @@ import { GlButton } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import { TEST_HOST } from 'spec/test_constants'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue'; @@ -142,14 +143,13 @@ describe('UserAvatarList', () => { expect(links.length).toEqual(props.items.length); }); - it('with collapse clicked, it renders avatars up to breakpoint', () => { + it('with collapse clicked, it renders avatars up to breakpoint', async () => { clickButton(); - return wrapper.vm.$nextTick(() => { - const links = wrapper.findAll(UserAvatarLink); + await nextTick(); + const links = wrapper.findAll(UserAvatarLink); - expect(links.length).toEqual(TEST_BREAKPOINT); - }); + expect(links.length).toEqual(TEST_BREAKPOINT); }); }); }); diff --git a/spec/frontend/vue_shared/components/user_select_spec.js b/spec/frontend/vue_shared/components/user_select_spec.js index 8994e16e517..411a15e1c74 100644 --- a/spec/frontend/vue_shared/components/user_select_spec.js +++ b/spec/frontend/vue_shared/components/user_select_spec.js @@ -104,14 +104,14 @@ describe('User select dropdown', () => { createComponent({ participantsQueryHandler: mockError }); await waitForPromises(); - expect(wrapper.emitted('error')).toEqual([[], []]); + expect(wrapper.emitted('error')).toEqual([[]]); }); it('emits an `error` event if search query was rejected', async () => { createComponent({ searchQueryHandler: mockError }); await waitForSearch(); - expect(wrapper.emitted('error')).toEqual([[], []]); + expect(wrapper.emitted('error')).toEqual([[]]); }); it('renders current user if they are not in participants or assignees', async () => { diff --git a/spec/frontend/vue_shared/components/web_ide_link_spec.js b/spec/frontend/vue_shared/components/web_ide_link_spec.js index 659d93d6597..5589cbfd08f 100644 --- a/spec/frontend/vue_shared/components/web_ide_link_spec.js +++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js @@ -4,6 +4,7 @@ import { nextTick } from 'vue'; import ActionsButton from '~/vue_shared/components/actions_button.vue'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import WebIdeLink from '~/vue_shared/components/web_ide_link.vue'; +import ConfirmForkModal from '~/vue_shared/components/confirm_fork_modal.vue'; import { stubComponent } from 'helpers/stub_component'; import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper'; @@ -13,6 +14,7 @@ const TEST_WEB_IDE_URL = '/-/ide/project/gitlab-test/test/edit/main/-/'; const TEST_GITPOD_URL = 'https://gitpod.test/'; const TEST_USER_PREFERENCES_GITPOD_PATH = '/-/profile/preferences#user_gitpod_enabled'; const TEST_USER_PROFILE_ENABLE_GITPOD_PATH = '/-/profile?user%5Bgitpod_enabled%5D=true'; +const forkPath = '/some/fork/path'; const ACTION_EDIT = { href: TEST_EDIT_URL, @@ -74,6 +76,7 @@ describe('Web IDE link component', () => { editUrl: TEST_EDIT_URL, webIdeUrl: TEST_WEB_IDE_URL, gitpodUrl: TEST_GITPOD_URL, + forkPath, ...props, }, stubs: { @@ -96,6 +99,7 @@ describe('Web IDE link component', () => { const findActionsButton = () => wrapper.find(ActionsButton); const findLocalStorageSync = () => wrapper.find(LocalStorageSync); const findModal = () => wrapper.findComponent(GlModal); + const findForkConfirmModal = () => wrapper.findComponent(ConfirmForkModal); it.each([ { @@ -213,7 +217,7 @@ describe('Web IDE link component', () => { findLocalStorageSync().vm.$emit('input', ACTION_GITPOD.key); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findActionsButton().props('selectedKey')).toBe(ACTION_GITPOD.key); }); @@ -223,7 +227,7 @@ describe('Web IDE link component', () => { findActionsButton().vm.$emit('select', ACTION_GITPOD.key); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findActionsButton().props('selectedKey')).toBe(ACTION_GITPOD.key); expect(findLocalStorageSync().props('value')).toBe(ACTION_GITPOD.key); @@ -231,16 +235,28 @@ describe('Web IDE link component', () => { }); describe('edit actions', () => { - it.each([ + const testActions = [ { - props: { showWebIdeButton: true, showEditButton: false }, + props: { + showWebIdeButton: true, + showEditButton: false, + forkPath, + forkModalId: 'edit-modal', + }, expectedEventPayload: 'ide', }, { - props: { showWebIdeButton: false, showEditButton: true }, + props: { + showWebIdeButton: false, + showEditButton: true, + forkPath, + forkModalId: 'webide-modal', + }, expectedEventPayload: 'simple', }, - ])( + ]; + + it.each(testActions)( 'emits the correct event when an action handler is called', async ({ props, expectedEventPayload }) => { createComponent({ ...props, needsToFork: true, disableForkModal: true }); @@ -250,6 +266,29 @@ describe('Web IDE link component', () => { expect(wrapper.emitted('edit')).toEqual([[expectedEventPayload]]); }, ); + + it.each(testActions)('renders the fork confirmation modal', async ({ props }) => { + createComponent({ ...props, needsToFork: true }); + + expect(findForkConfirmModal().exists()).toBe(true); + expect(findForkConfirmModal().props()).toEqual({ + visible: false, + forkPath, + modalId: props.forkModalId, + }); + }); + + it.each(testActions)('opens the modal when the button is clicked', async ({ props }) => { + createComponent({ ...props, needsToFork: true }, mountExtended); + + await findActionsButton().trigger('click'); + + expect(findForkConfirmModal().props()).toEqual({ + visible: true, + forkPath, + modalId: props.forkModalId, + }); + }); }); describe('when Gitpod is not enabled', () => { |