From aee0a117a889461ce8ced6fcf73207fe017f1d99 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Mon, 20 Dec 2021 13:37:47 +0000 Subject: Add latest changes from gitlab-org/gitlab@14-6-stable-ee --- .../__snapshots__/source_editor_spec.js.snap | 1 + .../components/chronic_duration_input_spec.js | 390 +++++++++++++++++++++ .../vue_shared/components/clipboard_button_spec.js | 10 + .../confirm_danger/confirm_danger_spec.js | 6 + .../vue_shared/components/confirm_modal_spec.js | 63 +++- .../components/delete_label_modal_spec.js | 64 ---- .../__snapshots__/design_note_pin_spec.js.snap | 55 +++ .../design_management/design_note_pin_spec.js | 42 +++ .../components/diff_viewer/viewers/renamed_spec.js | 7 +- .../components/dismissible_alert_spec.js | 4 + .../components/dom_element_listener_spec.js | 116 ++++++ .../vue_shared/components/file_icon_spec.js | 2 +- .../components/filtered_search_bar/mock_data.js | 104 ------ .../filtered_search_bar/tokens/base_token_spec.js | 57 ++- .../filtered_search_bar/tokens/epic_token_spec.js | 169 --------- .../tokens/iteration_token_spec.js | 116 ------ .../tokens/milestone_token_spec.js | 13 +- .../tokens/release_token_spec.js | 2 +- .../tokens/weight_token_spec.js | 38 -- .../form/__snapshots__/title_spec.js.snap | 1 + .../form/input_copy_toggle_visibility_spec.js | 231 ++++++++++++ .../vue_shared/components/gl_modal_vuex_spec.js | 7 +- .../components/header_ci_component_spec.js | 18 +- .../issuable/issuable_header_warnings_spec.js | 89 ----- .../components/issue/issue_assignees_spec.js | 145 -------- .../components/issue/issue_milestone_spec.js | 160 --------- .../components/issue/related_issuable_item_spec.js | 206 ----------- .../components/issue/related_issuable_mock_data.js | 123 ------- .../vue_shared/components/line_numbers_spec.js | 71 ++++ .../vue_shared/components/markdown/toolbar_spec.js | 39 --- .../components/namespace_select/mock_data.js | 11 + .../namespace_select/namespace_select_spec.js | 86 +++++ .../components/notes/placeholder_note_spec.js | 24 +- .../pagination_bar/pagination_bar_spec.js | 93 +++++ .../project_selector/project_list_item_spec.js | 8 +- .../project_selector/project_selector_spec.js | 5 +- .../components/registry/metadata_item_spec.js | 2 +- .../runner_instructions_modal_spec.js | 10 +- .../sidebar/collapsed_grouped_date_picker_spec.js | 103 ------ .../labels_select_vue/dropdown_button_spec.js | 7 +- .../dropdown_contents_create_view_spec.js | 7 +- .../dropdown_contents_labels_view_spec.js | 7 +- .../labels_select_vue/dropdown_contents_spec.js | 7 +- .../labels_select_vue/dropdown_title_spec.js | 7 +- .../labels_select_vue/dropdown_value_spec.js | 7 +- .../labels_select_vue/labels_select_root_spec.js | 7 +- .../sidebar/labels_select_vue/mock_data.js | 2 +- .../dropdown_contents_create_view_spec.js | 8 +- .../dropdown_contents_labels_view_spec.js | 8 +- .../labels_select_widget/dropdown_contents_spec.js | 41 ++- .../labels_select_widget/dropdown_header_spec.js | 17 + .../labels_select_widget/dropdown_value_spec.js | 5 + .../labels_select_root_spec.js | 77 +++- .../sidebar/labels_select_widget/mock_data.js | 17 + .../vue_shared/components/source_viewer_spec.js | 59 ++++ .../components/storage_counter/usage_graph_spec.js | 137 -------- .../components/tooltip_on_truncate_spec.js | 233 ++++++------ .../vue_shared/components/user_select_spec.js | 8 +- .../components/vuex_module_provider_spec.js | 7 +- 59 files changed, 1626 insertions(+), 1733 deletions(-) create mode 100644 spec/frontend/vue_shared/components/chronic_duration_input_spec.js delete mode 100644 spec/frontend/vue_shared/components/delete_label_modal_spec.js create mode 100644 spec/frontend/vue_shared/components/design_management/__snapshots__/design_note_pin_spec.js.snap create mode 100644 spec/frontend/vue_shared/components/design_management/design_note_pin_spec.js create mode 100644 spec/frontend/vue_shared/components/dom_element_listener_spec.js delete mode 100644 spec/frontend/vue_shared/components/filtered_search_bar/tokens/epic_token_spec.js delete mode 100644 spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js delete mode 100644 spec/frontend/vue_shared/components/filtered_search_bar/tokens/weight_token_spec.js create mode 100644 spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js delete mode 100644 spec/frontend/vue_shared/components/issuable/issuable_header_warnings_spec.js delete mode 100644 spec/frontend/vue_shared/components/issue/issue_assignees_spec.js delete mode 100644 spec/frontend/vue_shared/components/issue/issue_milestone_spec.js delete mode 100644 spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js delete mode 100644 spec/frontend/vue_shared/components/issue/related_issuable_mock_data.js create mode 100644 spec/frontend/vue_shared/components/line_numbers_spec.js create mode 100644 spec/frontend/vue_shared/components/namespace_select/mock_data.js create mode 100644 spec/frontend/vue_shared/components/namespace_select/namespace_select_spec.js create mode 100644 spec/frontend/vue_shared/components/pagination_bar/pagination_bar_spec.js delete mode 100644 spec/frontend/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js create mode 100644 spec/frontend/vue_shared/components/source_viewer_spec.js delete mode 100644 spec/frontend/vue_shared/components/storage_counter/usage_graph_spec.js (limited to 'spec/frontend/vue_shared/components') diff --git a/spec/frontend/vue_shared/components/__snapshots__/source_editor_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/source_editor_spec.js.snap index 7ce155f6a5d..f414359fef2 100644 --- a/spec/frontend/vue_shared/components/__snapshots__/source_editor_spec.js.snap +++ b/spec/frontend/vue_shared/components/__snapshots__/source_editor_spec.js.snap @@ -3,6 +3,7 @@ exports[`Source Editor component rendering matches the snapshot 1`] = `
 {
+  let wrapper;
+  let textElement;
+  let hiddenElement;
+
+  afterEach(() => {
+    wrapper.destroy();
+    wrapper = null;
+    textElement = null;
+    hiddenElement = null;
+  });
+
+  const findComponents = () => {
+    textElement = wrapper.find('input[type=text]').element;
+    hiddenElement = wrapper.find('input[type=hidden]').element;
+  };
+
+  const createComponent = (props = {}) => {
+    if (wrapper) {
+      throw new Error('There should only be one wrapper created per test');
+    }
+
+    wrapper = mount(ChronicDurationInput, { propsData: props });
+    findComponents();
+  };
+
+  describe('value', () => {
+    it('has human-readable output with value', () => {
+      createComponent({ value: MOCK_VALUE });
+
+      expect(textElement.value).toBe('2 hrs 20 mins');
+      expect(hiddenElement.value).toBe(MOCK_VALUE.toString());
+    });
+
+    it('has empty output with no value', () => {
+      createComponent({ value: null });
+
+      expect(textElement.value).toBe('');
+      expect(hiddenElement.value).toBe('');
+    });
+  });
+
+  describe('change', () => {
+    const createAndDispatch = async (initialValue, humanReadableInput) => {
+      createComponent({ value: initialValue });
+      await wrapper.vm.$nextTick();
+      textElement.value = humanReadableInput;
+      textElement.dispatchEvent(new Event('input'));
+    };
+
+    describe('when starting with no value and receiving human-readable input', () => {
+      beforeEach(() => {
+        createAndDispatch(null, '2hr20min');
+      });
+
+      it('updates hidden field', () => {
+        expect(textElement.value).toBe('2hr20min');
+        expect(hiddenElement.value).toBe(MOCK_VALUE.toString());
+      });
+
+      it('emits change event', () => {
+        expect(wrapper.emitted('change')).toEqual([[MOCK_VALUE]]);
+      });
+    });
+
+    describe('when starting with a value and receiving empty input', () => {
+      beforeEach(() => {
+        createAndDispatch(MOCK_VALUE, '');
+      });
+
+      it('updates hidden field', () => {
+        expect(textElement.value).toBe('');
+        expect(hiddenElement.value).toBe('');
+      });
+
+      it('emits change event', () => {
+        expect(wrapper.emitted('change')).toEqual([[null]]);
+      });
+    });
+
+    describe('when starting with a value and receiving invalid input', () => {
+      beforeEach(() => {
+        createAndDispatch(MOCK_VALUE, 'gobbledygook');
+      });
+
+      it('does not update hidden field', () => {
+        expect(textElement.value).toBe('gobbledygook');
+        expect(hiddenElement.value).toBe(MOCK_VALUE.toString());
+      });
+
+      it('does not emit change event', () => {
+        expect(wrapper.emitted('change')).toBeUndefined();
+      });
+    });
+  });
+
+  describe('valid', () => {
+    describe('initial value', () => {
+      beforeEach(() => {
+        createComponent({ value: MOCK_VALUE });
+      });
+
+      it('emits valid with initial value', () => {
+        expect(wrapper.emitted('valid')).toEqual([[{ valid: true, feedback: '' }]]);
+        expect(textElement.validity.valid).toBe(true);
+        expect(textElement.validity.customError).toBe(false);
+        expect(textElement.validationMessage).toBe('');
+        expect(hiddenElement.validity.valid).toBe(true);
+        expect(hiddenElement.validity.customError).toBe(false);
+        expect(hiddenElement.validationMessage).toBe('');
+      });
+
+      it('emits valid with user input', async () => {
+        textElement.value = '1m10s';
+        textElement.dispatchEvent(new Event('input'));
+        await wrapper.vm.$nextTick();
+
+        expect(wrapper.emitted('valid')).toEqual([
+          [{ valid: true, feedback: '' }],
+          [{ valid: true, feedback: '' }],
+        ]);
+        expect(textElement.validity.valid).toBe(true);
+        expect(textElement.validity.customError).toBe(false);
+        expect(textElement.validationMessage).toBe('');
+        expect(hiddenElement.validity.valid).toBe(true);
+        expect(hiddenElement.validity.customError).toBe(false);
+        expect(hiddenElement.validationMessage).toBe('');
+
+        textElement.value = '';
+        textElement.dispatchEvent(new Event('input'));
+        await wrapper.vm.$nextTick();
+
+        expect(wrapper.emitted('valid')).toEqual([
+          [{ valid: true, feedback: '' }],
+          [{ valid: true, feedback: '' }],
+          [{ valid: null, feedback: '' }],
+        ]);
+        expect(textElement.validity.valid).toBe(true);
+        expect(textElement.validity.customError).toBe(false);
+        expect(textElement.validationMessage).toBe('');
+        expect(hiddenElement.validity.valid).toBe(true);
+        expect(hiddenElement.validity.customError).toBe(false);
+        expect(hiddenElement.validationMessage).toBe('');
+      });
+
+      it('emits invalid with user input', async () => {
+        textElement.value = 'gobbledygook';
+        textElement.dispatchEvent(new Event('input'));
+        await wrapper.vm.$nextTick();
+
+        expect(wrapper.emitted('valid')).toEqual([
+          [{ valid: true, feedback: '' }],
+          [{ valid: false, feedback: ChronicDurationInput.i18n.INVALID_INPUT_FEEDBACK }],
+        ]);
+        expect(textElement.validity.valid).toBe(false);
+        expect(textElement.validity.customError).toBe(true);
+        expect(textElement.validationMessage).toBe(
+          ChronicDurationInput.i18n.INVALID_INPUT_FEEDBACK,
+        );
+        expect(hiddenElement.validity.valid).toBe(false);
+        expect(hiddenElement.validity.customError).toBe(true);
+        // Hidden elements do not have validationMessage
+        expect(hiddenElement.validationMessage).toBe('');
+      });
+    });
+
+    describe('no initial value', () => {
+      beforeEach(() => {
+        createComponent({ value: null });
+      });
+
+      it('emits valid with no initial value', () => {
+        expect(wrapper.emitted('valid')).toEqual([[{ valid: null, feedback: '' }]]);
+        expect(textElement.validity.valid).toBe(true);
+        expect(textElement.validity.customError).toBe(false);
+        expect(textElement.validationMessage).toBe('');
+        expect(hiddenElement.validity.valid).toBe(true);
+        expect(hiddenElement.validity.customError).toBe(false);
+        expect(hiddenElement.validationMessage).toBe('');
+      });
+
+      it('emits valid with updated value', async () => {
+        wrapper.setProps({ value: MOCK_VALUE });
+        await wrapper.vm.$nextTick();
+
+        expect(wrapper.emitted('valid')).toEqual([
+          [{ valid: null, feedback: '' }],
+          [{ valid: true, feedback: '' }],
+        ]);
+        expect(textElement.validity.valid).toBe(true);
+        expect(textElement.validity.customError).toBe(false);
+        expect(textElement.validationMessage).toBe('');
+        expect(hiddenElement.validity.valid).toBe(true);
+        expect(hiddenElement.validity.customError).toBe(false);
+        expect(hiddenElement.validationMessage).toBe('');
+      });
+    });
+
+    describe('decimal input', () => {
+      describe('when integerRequired is false', () => {
+        beforeEach(() => {
+          createComponent({ value: null, integerRequired: false });
+        });
+
+        it('emits valid when input is integer', async () => {
+          textElement.value = '2hr20min';
+          textElement.dispatchEvent(new Event('input'));
+          await wrapper.vm.$nextTick();
+
+          expect(wrapper.emitted('change')).toEqual([[MOCK_VALUE]]);
+          expect(wrapper.emitted('valid')).toEqual([
+            [{ valid: null, feedback: '' }],
+            [{ valid: true, feedback: '' }],
+          ]);
+          expect(textElement.validity.valid).toBe(true);
+          expect(textElement.validity.customError).toBe(false);
+          expect(textElement.validationMessage).toBe('');
+          expect(hiddenElement.validity.valid).toBe(true);
+          expect(hiddenElement.validity.customError).toBe(false);
+          expect(hiddenElement.validationMessage).toBe('');
+        });
+
+        it('emits valid when input is decimal', async () => {
+          textElement.value = '1.5s';
+          textElement.dispatchEvent(new Event('input'));
+          await wrapper.vm.$nextTick();
+
+          expect(wrapper.emitted('change')).toEqual([[1.5]]);
+          expect(wrapper.emitted('valid')).toEqual([
+            [{ valid: null, feedback: '' }],
+            [{ valid: true, feedback: '' }],
+          ]);
+          expect(textElement.validity.valid).toBe(true);
+          expect(textElement.validity.customError).toBe(false);
+          expect(textElement.validationMessage).toBe('');
+          expect(hiddenElement.validity.valid).toBe(true);
+          expect(hiddenElement.validity.customError).toBe(false);
+          expect(hiddenElement.validationMessage).toBe('');
+        });
+      });
+
+      describe('when integerRequired is unspecified', () => {
+        beforeEach(() => {
+          createComponent({ value: null });
+        });
+
+        it('emits valid when input is integer', async () => {
+          textElement.value = '2hr20min';
+          textElement.dispatchEvent(new Event('input'));
+          await wrapper.vm.$nextTick();
+
+          expect(wrapper.emitted('change')).toEqual([[MOCK_VALUE]]);
+          expect(wrapper.emitted('valid')).toEqual([
+            [{ valid: null, feedback: '' }],
+            [{ valid: true, feedback: '' }],
+          ]);
+          expect(textElement.validity.valid).toBe(true);
+          expect(textElement.validity.customError).toBe(false);
+          expect(textElement.validationMessage).toBe('');
+          expect(hiddenElement.validity.valid).toBe(true);
+          expect(hiddenElement.validity.customError).toBe(false);
+          expect(hiddenElement.validationMessage).toBe('');
+        });
+
+        it('emits invalid when input is decimal', async () => {
+          textElement.value = '1.5s';
+          textElement.dispatchEvent(new Event('input'));
+          await wrapper.vm.$nextTick();
+
+          expect(wrapper.emitted('change')).toBeUndefined();
+          expect(wrapper.emitted('valid')).toEqual([
+            [{ valid: null, feedback: '' }],
+            [
+              {
+                valid: false,
+                feedback: ChronicDurationInput.i18n.INVALID_DECIMAL_FEEDBACK,
+              },
+            ],
+          ]);
+          expect(textElement.validity.valid).toBe(false);
+          expect(textElement.validity.customError).toBe(true);
+          expect(textElement.validationMessage).toBe(
+            ChronicDurationInput.i18n.INVALID_DECIMAL_FEEDBACK,
+          );
+          expect(hiddenElement.validity.valid).toBe(false);
+          expect(hiddenElement.validity.customError).toBe(true);
+          // Hidden elements do not have validationMessage
+          expect(hiddenElement.validationMessage).toBe('');
+        });
+      });
+    });
+  });
+
+  describe('v-model', () => {
+    beforeEach(() => {
+      wrapper = mount({
+        data() {
+          return { value: 1 * 60 + 10 };
+        },
+        components: { ChronicDurationInput },
+        template: '
', + }); + findComponents(); + }); + + describe('value', () => { + it('passes initial prop via v-model', () => { + expect(textElement.value).toBe('1 min 10 secs'); + expect(hiddenElement.value).toBe((1 * 60 + 10).toString()); + }); + + it('passes updated prop via v-model', async () => { + wrapper.setData({ value: MOCK_VALUE }); + await wrapper.vm.$nextTick(); + + expect(textElement.value).toBe('2 hrs 20 mins'); + expect(hiddenElement.value).toBe(MOCK_VALUE.toString()); + }); + }); + + describe('change', () => { + it('passes user input to parent via v-model', async () => { + textElement.value = '2hr20min'; + textElement.dispatchEvent(new Event('input')); + await wrapper.vm.$nextTick(); + + expect(wrapper.findComponent(ChronicDurationInput).props('value')).toBe(MOCK_VALUE); + expect(textElement.value).toBe('2hr20min'); + expect(hiddenElement.value).toBe(MOCK_VALUE.toString()); + }); + }); + }); + + describe('name', () => { + beforeEach(() => { + createComponent({ name: 'myInput' }); + }); + + it('sets name of hidden field', () => { + expect(hiddenElement.name).toBe('myInput'); + }); + + it('does not set name of text field', () => { + expect(textElement.name).toBe(''); + }); + }); + + describe('form submission', () => { + beforeEach(() => { + wrapper = mount({ + template: `
`, + components: { + ChronicDurationInput, + }, + }); + findComponents(); + }); + + it('creates form data with initial value', () => { + const formData = new FormData(wrapper.find('[data-testid=myForm]').element); + const iter = formData.entries(); + + expect(iter.next()).toEqual({ + value: ['myInput', MOCK_VALUE.toString()], + done: false, + }); + expect(iter.next()).toEqual({ value: undefined, done: true }); + }); + + it('creates form data with user-specified value', async () => { + textElement.value = '1m10s'; + textElement.dispatchEvent(new Event('input')); + await wrapper.vm.$nextTick(); + + const formData = new FormData(wrapper.find('[data-testid=myForm]').element); + const iter = formData.entries(); + + expect(iter.next()).toEqual({ + value: ['myInput', (1 * 60 + 10).toString()], + done: false, + }); + expect(iter.next()).toEqual({ value: undefined, done: true }); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/clipboard_button_spec.js b/spec/frontend/vue_shared/components/clipboard_button_spec.js index ab4008484e5..33445923a49 100644 --- a/spec/frontend/vue_shared/components/clipboard_button_spec.js +++ b/spec/frontend/vue_shared/components/clipboard_button_spec.js @@ -89,6 +89,16 @@ describe('clipboard button', () => { expect(onClick).toHaveBeenCalled(); }); + it('passes the category and variant props to the GlButton', () => { + const category = 'tertiary'; + const variant = 'confirm'; + + createWrapper({ title: '', text: '', category, variant }); + + expect(findButton().props('category')).toBe(category); + expect(findButton().props('variant')).toBe(variant); + }); + describe('integration', () => { it('actually copies to clipboard', () => { initCopyToClipboard(); diff --git a/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js index 220f897c035..af7f85769aa 100644 --- a/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js +++ b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js @@ -9,6 +9,7 @@ describe('Confirm Danger Modal', () => { const phrase = 'En Taro Adun'; const buttonText = 'Click me!'; + const buttonClass = 'gl-w-full'; const modalId = CONFIRM_DANGER_MODAL_ID; const findBtn = () => wrapper.findComponent(GlButton); @@ -19,6 +20,7 @@ describe('Confirm Danger Modal', () => { shallowMountExtended(ConfirmDanger, { propsData: { buttonText, + buttonClass, phrase, ...props, }, @@ -51,6 +53,10 @@ describe('Confirm Danger Modal', () => { expect(findBtn().attributes('disabled')).toBe('true'); }); + it('passes `buttonClass` prop to button', () => { + expect(findBtn().classes()).toContain(buttonClass); + }); + it('will emit `confirm` when the modal confirms', () => { expect(wrapper.emitted('confirm')).toBeUndefined(); diff --git a/spec/frontend/vue_shared/components/confirm_modal_spec.js b/spec/frontend/vue_shared/components/confirm_modal_spec.js index db8d0674121..3ca1c943398 100644 --- a/spec/frontend/vue_shared/components/confirm_modal_spec.js +++ b/spec/frontend/vue_shared/components/confirm_modal_spec.js @@ -1,6 +1,9 @@ import { shallowMount } from '@vue/test-utils'; +import { merge } from 'lodash'; import { TEST_HOST } from 'helpers/test_constants'; +import eventHub, { EVENT_OPEN_CONFIRM_MODAL } from '~/vue_shared/components/confirm_modal_eventhub'; import ConfirmModal from '~/vue_shared/components/confirm_modal.vue'; +import DomElementListener from '~/vue_shared/components/dom_element_listener.vue'; jest.mock('~/lib/utils/csrf', () => ({ token: 'test-csrf-token' })); @@ -54,12 +57,50 @@ describe('vue_shared/components/confirm_modal', () => { findForm() .findAll('input') .wrappers.map((x) => ({ name: x.attributes('name'), value: x.attributes('value') })); + const findDomElementListener = () => wrapper.find(DomElementListener); + const triggerOpenWithEventHub = (modalData) => { + eventHub.$emit(EVENT_OPEN_CONFIRM_MODAL, modalData); + }; + const triggerOpenWithDomListener = (modalData) => { + const element = document.createElement('button'); + + element.dataset.path = modalData.path; + element.dataset.method = modalData.method; + element.dataset.modalAttributes = JSON.stringify(modalData.modalAttributes); + + findDomElementListener().vm.$emit('click', { + preventDefault: jest.fn(), + currentTarget: element, + }); + }; + + describe('default', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders empty GlModal', () => { + expect(findModal().props()).toEqual({}); + }); + + it('renders form missing values', () => { + expect(findForm().attributes('action')).toBe(''); + expect(findFormData()).toEqual([ + { name: '_method', value: undefined }, + { name: 'authenticity_token', value: 'test-csrf-token' }, + ]); + }); + }); describe('template', () => { - describe('when modal data is set', () => { + describe.each` + desc | trigger + ${'when opened from eventhub'} | ${triggerOpenWithEventHub} + ${'when opened from dom listener'} | ${triggerOpenWithDomListener} + `('$desc', ({ trigger }) => { beforeEach(() => { createComponent(); - wrapper.vm.modalAttributes = MOCK_MODAL_DATA.modalAttributes; + trigger(MOCK_MODAL_DATA); }); it('renders GlModal with data', () => { @@ -71,6 +112,14 @@ describe('vue_shared/components/confirm_modal', () => { }), ); }); + + it('renders form', () => { + expect(findForm().attributes('action')).toBe(MOCK_MODAL_DATA.path); + expect(findFormData()).toEqual([ + { name: '_method', value: MOCK_MODAL_DATA.method }, + { name: 'authenticity_token', value: 'test-csrf-token' }, + ]); + }); }); describe.each` @@ -79,11 +128,10 @@ describe('vue_shared/components/confirm_modal', () => { ${'when message has html'} | ${{ messageHtml: '

Header

  • First
' }} | ${'

Header

  • First
'} `('$desc', ({ attrs, expectation }) => { beforeEach(() => { + const modalData = merge({ ...MOCK_MODAL_DATA }, { modalAttributes: attrs }); + createComponent(); - wrapper.vm.modalAttributes = { - ...MOCK_MODAL_DATA.modalAttributes, - ...attrs, - }; + triggerOpenWithEventHub(modalData); }); it('renders message', () => { @@ -96,8 +144,7 @@ describe('vue_shared/components/confirm_modal', () => { describe('submitModal', () => { beforeEach(() => { createComponent(); - wrapper.vm.path = MOCK_MODAL_DATA.path; - wrapper.vm.method = MOCK_MODAL_DATA.method; + triggerOpenWithEventHub(MOCK_MODAL_DATA); }); it('does not submit form', () => { diff --git a/spec/frontend/vue_shared/components/delete_label_modal_spec.js b/spec/frontend/vue_shared/components/delete_label_modal_spec.js deleted file mode 100644 index 3905690dab4..00000000000 --- a/spec/frontend/vue_shared/components/delete_label_modal_spec.js +++ /dev/null @@ -1,64 +0,0 @@ -import { GlModal } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; -import { stubComponent } from 'helpers/stub_component'; -import { TEST_HOST } from 'helpers/test_constants'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -import DeleteLabelModal from '~/vue_shared/components/delete_label_modal.vue'; - -const MOCK_MODAL_DATA = { - labelName: 'label 1', - subjectName: 'GitLab Org', - destroyPath: `${TEST_HOST}/1`, -}; - -describe('vue_shared/components/delete_label_modal', () => { - let wrapper; - - const createComponent = () => { - wrapper = extendedWrapper( - mount(DeleteLabelModal, { - propsData: { - selector: '.js-test-btn', - }, - stubs: { - GlModal: stubComponent(GlModal, { - template: - '
', - }), - }, - }), - ); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - const findModal = () => wrapper.find(GlModal); - const findPrimaryModalButton = () => wrapper.findByTestId('delete-button'); - - describe('template', () => { - describe('when modal data is set', () => { - beforeEach(() => { - createComponent(); - wrapper.vm.labelName = MOCK_MODAL_DATA.labelName; - wrapper.vm.subjectName = MOCK_MODAL_DATA.subjectName; - wrapper.vm.destroyPath = MOCK_MODAL_DATA.destroyPath; - }); - - it('renders GlModal', () => { - expect(findModal().exists()).toBe(true); - }); - - it('displays the label name and subject name', () => { - expect(findModal().text()).toContain( - `${MOCK_MODAL_DATA.labelName} will be permanently deleted from ${MOCK_MODAL_DATA.subjectName}. This cannot be undone`, - ); - }); - - it('passes the destroyPath to the button', () => { - expect(findPrimaryModalButton().attributes('href')).toBe(MOCK_MODAL_DATA.destroyPath); - }); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/design_management/__snapshots__/design_note_pin_spec.js.snap b/spec/frontend/vue_shared/components/design_management/__snapshots__/design_note_pin_spec.js.snap new file mode 100644 index 00000000000..eb0adb0bebd --- /dev/null +++ b/spec/frontend/vue_shared/components/design_management/__snapshots__/design_note_pin_spec.js.snap @@ -0,0 +1,55 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Design note pin component should match the snapshot of note with index 1`] = ` + +`; + +exports[`Design note pin component should match the snapshot of note without index 1`] = ` + +`; + +exports[`Design note pin component should match the snapshot when pin is resolved 1`] = ` + +`; + +exports[`Design note pin component should match the snapshot when position is absent 1`] = ` + +`; 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 new file mode 100644 index 00000000000..984a28c93d6 --- /dev/null +++ b/spec/frontend/vue_shared/components/design_management/design_note_pin_spec.js @@ -0,0 +1,42 @@ +import { shallowMount } from '@vue/test-utils'; +import DesignNotePin from '~/vue_shared/components/design_management/design_note_pin.vue'; + +describe('Design note pin component', () => { + let wrapper; + + function createComponent(propsData = {}) { + wrapper = shallowMount(DesignNotePin, { + propsData: { + position: { + left: '10px', + top: '10px', + }, + ...propsData, + }, + }); + } + + afterEach(() => { + wrapper.destroy(); + }); + + it('should match the snapshot of note without index', () => { + createComponent(); + expect(wrapper.element).toMatchSnapshot(); + }); + + it('should match the snapshot of note with index', () => { + createComponent({ label: 1 }); + expect(wrapper.element).toMatchSnapshot(); + }); + + it('should match the snapshot when pin is resolved', () => { + createComponent({ isResolved: true }); + expect(wrapper.element).toMatchSnapshot(); + }); + + it('should match the snapshot when position is absent', () => { + createComponent({ position: null }); + expect(wrapper.element).toMatchSnapshot(); + }); +}); 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 9f433816b34..b8d3cbebe16 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,4 +1,5 @@ -import { createLocalVue, shallowMount, mount } from '@vue/test-utils'; +import { shallowMount, mount } from '@vue/test-utils'; +import Vue from 'vue'; import Vuex from 'vuex'; import { TRANSITION_LOAD_START, @@ -11,15 +12,13 @@ import { } from '~/diffs/constants'; import Renamed from '~/vue_shared/components/diff_viewer/viewers/renamed.vue'; -const localVue = createLocalVue(); -localVue.use(Vuex); +Vue.use(Vuex); function createRenamedComponent({ props = {}, store = new Vuex.Store({}), deep = false }) { const mnt = deep ? mount : shallowMount; return mnt(Renamed, { propsData: { ...props }, - localVue, store, }); } diff --git a/spec/frontend/vue_shared/components/dismissible_alert_spec.js b/spec/frontend/vue_shared/components/dismissible_alert_spec.js index fcd004d35a7..879d4aba441 100644 --- a/spec/frontend/vue_shared/components/dismissible_alert_spec.js +++ b/spec/frontend/vue_shared/components/dismissible_alert_spec.js @@ -43,6 +43,10 @@ describe('vue_shared/components/dismissible_alert', () => { it('hides the alert', () => { expect(findAlert().exists()).toBe(false); }); + + it('emmits alertDismissed', () => { + expect(wrapper.emitted('alertDismissed')).toBeTruthy(); + }); }); }); diff --git a/spec/frontend/vue_shared/components/dom_element_listener_spec.js b/spec/frontend/vue_shared/components/dom_element_listener_spec.js new file mode 100644 index 00000000000..a848c34b7ce --- /dev/null +++ b/spec/frontend/vue_shared/components/dom_element_listener_spec.js @@ -0,0 +1,116 @@ +import { mount } from '@vue/test-utils'; +import { setHTMLFixture } from 'helpers/fixtures'; +import DomElementListener from '~/vue_shared/components/dom_element_listener.vue'; + +const DEFAULT_SLOT_CONTENT = 'Default slot content'; +const SELECTOR = '.js-test-include'; +const HTML = ` +
+ + +
+`; + +describe('~/vue_shared/components/dom_element_listener.vue', () => { + let wrapper; + let spies; + + const createComponent = () => { + wrapper = mount(DomElementListener, { + propsData: { + selector: SELECTOR, + }, + listeners: spies, + slots: { + default: DEFAULT_SLOT_CONTENT, + }, + }); + }; + + const findElement = (testId) => document.querySelector(`[data-testid="${testId}"]`); + const spiesCallCount = () => + Object.values(spies) + .map((x) => x.mock.calls.length) + .reduce((a, b) => a + b); + + beforeEach(() => { + setHTMLFixture(HTML); + spies = { + click: jest.fn(), + focus: jest.fn(), + }; + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('default', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders default slot', () => { + expect(wrapper.text()).toBe(DEFAULT_SLOT_CONTENT); + }); + + it('does not initially trigger listeners', () => { + expect(spiesCallCount()).toBe(0); + }); + + describe.each` + event | testId + ${'click'} | ${'lorem'} + ${'focus'} | ${'ipsum'} + `( + 'when matching element triggers event (testId=$testId, event=$event)', + ({ event, testId }) => { + beforeEach(() => { + findElement(testId).dispatchEvent(new Event(event)); + }); + + it('triggers listener', () => { + expect(spiesCallCount()).toBe(1); + expect(spies[event]).toHaveBeenCalledWith(expect.any(Event)); + expect(spies[event]).toHaveBeenCalledWith( + expect.objectContaining({ + target: findElement(testId), + }), + ); + }); + }, + ); + + describe.each` + desc | event | testId + ${'when non-matching element triggers event'} | ${'click'} | ${'hello'} + ${'when matching element triggers unlistened event'} | ${'hover'} | ${'lorem'} + `('$desc', ({ event, testId }) => { + beforeEach(() => { + findElement(testId).dispatchEvent(new Event(event)); + }); + + it('does not trigger listeners', () => { + expect(spiesCallCount()).toBe(0); + }); + }); + }); + + describe('after destroyed', () => { + beforeEach(() => { + createComponent(); + wrapper.destroy(); + }); + + describe('when matching element triggers event', () => { + beforeEach(() => { + findElement('lorem').dispatchEvent(new Event('click')); + }); + + it('does not trigger any listeners', () => { + expect(spiesCallCount()).toBe(0); + }); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/file_icon_spec.js b/spec/frontend/vue_shared/components/file_icon_spec.js index c10663f6c14..b0e623520a8 100644 --- a/spec/frontend/vue_shared/components/file_icon_spec.js +++ b/spec/frontend/vue_shared/components/file_icon_spec.js @@ -34,7 +34,7 @@ describe('File Icon component', () => { it.each` fileName | iconName - ${'test.js'} | ${'javascript'} + ${'index.js'} | ${'javascript'} ${'test.png'} | ${'image'} ${'test.PNG'} | ${'image'} ${'.npmrc'} | ${'npm'} diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js index 238c5d16db5..e3e2ef5610d 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js @@ -5,12 +5,9 @@ import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/co import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue'; import BranchToken from '~/vue_shared/components/filtered_search_bar/tokens/branch_token.vue'; import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue'; -import EpicToken from '~/vue_shared/components/filtered_search_bar/tokens/epic_token.vue'; -import IterationToken from '~/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue'; import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue'; import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue'; import ReleaseToken from '~/vue_shared/components/filtered_search_bar/tokens/release_token.vue'; -import WeightToken from '~/vue_shared/components/filtered_search_bar/tokens/weight_token.vue'; export const mockAuthor1 = { id: 1, @@ -65,11 +62,6 @@ export const mockMilestones = [ mockEscapedMilestone, ]; -export const mockEpics = [ - { iid: 1, id: 1, title: 'Foo', group_full_path: 'gitlab-org' }, - { iid: 2, id: 2, title: 'Bar', group_full_path: 'gitlab-org/design' }, -]; - export const mockEmoji1 = { name: 'thumbsup', }; @@ -102,27 +94,6 @@ export const mockAuthorToken = { fetchAuthors: Api.projectUsers.bind(Api), }; -export const mockIterationToken = { - type: 'iteration', - icon: 'iteration', - title: 'Iteration', - unique: true, - token: IterationToken, - fetchIterations: () => Promise.resolve(), -}; - -export const mockIterations = [ - { - id: 1, - title: 'Iteration 1', - startDate: '2021-11-05', - dueDate: '2021-11-10', - iterationCadence: { - title: 'Cadence 1', - }, - }, -]; - export const mockLabelToken = { type: 'label_name', icon: 'labels', @@ -153,73 +124,6 @@ export const mockReleaseToken = { fetchReleases: () => Promise.resolve(), }; -export const mockEpicToken = { - type: 'epic_iid', - icon: 'clock', - title: 'Epic', - unique: true, - symbol: '&', - token: EpicToken, - operators: OPERATOR_IS_ONLY, - idProperty: 'iid', - fullPath: 'gitlab-org', -}; - -export const mockEpicNode1 = { - __typename: 'Epic', - parent: null, - id: 'gid://gitlab/Epic/40', - iid: '2', - title: 'Marketing epic', - description: 'Mock epic description', - state: 'opened', - startDate: '2017-12-25', - dueDate: '2018-02-15', - webUrl: 'http://gdk.test:3000/groups/gitlab-org/marketing/-/epics/1', - hasChildren: false, - hasParent: false, - confidential: false, -}; - -export const mockEpicNode2 = { - __typename: 'Epic', - parent: null, - id: 'gid://gitlab/Epic/41', - iid: '3', - title: 'Another marketing', - startDate: '2017-12-26', - dueDate: '2018-03-10', - state: 'opened', - webUrl: 'http://gdk.test:3000/groups/gitlab-org/marketing/-/epics/2', -}; - -export const mockGroupEpicsQueryResponse = { - data: { - group: { - id: 'gid://gitlab/Group/1', - name: 'Gitlab Org', - epics: { - edges: [ - { - node: { - ...mockEpicNode1, - }, - __typename: 'EpicEdge', - }, - { - node: { - ...mockEpicNode2, - }, - __typename: 'EpicEdge', - }, - ], - __typename: 'EpicConnection', - }, - __typename: 'Group', - }, - }, -}; - export const mockReactionEmojiToken = { type: 'my_reaction_emoji', icon: 'thumb-up', @@ -243,14 +147,6 @@ export const mockMembershipToken = { ], }; -export const mockWeightToken = { - type: 'weight', - icon: 'weight', - title: 'Weight', - unique: true, - token: WeightToken, -}; - export const mockMembershipTokenOptionsWithoutTitles = { ...mockMembershipToken, options: [{ value: 'exclude' }, { value: 'only' }], 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 f9ce0338d2f..84f0151d9db 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 @@ -14,7 +14,13 @@ import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_t import { mockLabelToken } from '../mock_data'; -jest.mock('~/vue_shared/components/filtered_search_bar/filtered_search_utils'); +jest.mock('~/vue_shared/components/filtered_search_bar/filtered_search_utils', () => ({ + getRecentlyUsedSuggestions: jest.fn(), + setTokenValueToRecentlyUsed: jest.fn(), + stripQuotes: jest.requireActual( + '~/vue_shared/components/filtered_search_bar/filtered_search_utils', + ).stripQuotes, +})); const mockStorageKey = 'recent-tokens-label_name'; @@ -46,13 +52,13 @@ const defaultSlots = { }; const mockProps = { - config: mockLabelToken, + config: { ...mockLabelToken, recentSuggestionsStorageKey: mockStorageKey }, value: { data: '' }, active: false, suggestions: [], suggestionsLoading: false, defaultSuggestions: DEFAULT_NONE_ANY, - recentSuggestionsStorageKey: mockStorageKey, + getActiveTokenValue: (labels, data) => labels.find((label) => label.title === data), }; function createComponent({ @@ -152,30 +158,22 @@ describe('BaseToken', () => { describe('methods', () => { describe('handleTokenValueSelected', () => { - it('calls `setTokenValueToRecentlyUsed` when `recentSuggestionsStorageKey` is defined', () => { - const mockTokenValue = { - id: 1, - title: 'Foo', - }; + const mockTokenValue = mockLabels[0]; - wrapper.vm.handleTokenValueSelected(mockTokenValue); + it('calls `setTokenValueToRecentlyUsed` when `recentSuggestionsStorageKey` is defined', () => { + wrapper.vm.handleTokenValueSelected(mockTokenValue.title); expect(setTokenValueToRecentlyUsed).toHaveBeenCalledWith(mockStorageKey, mockTokenValue); }); it('does not add token from preloadedSuggestions', async () => { - const mockTokenValue = { - id: 1, - title: 'Foo', - }; - wrapper.setProps({ preloadedSuggestions: [mockTokenValue], }); await wrapper.vm.$nextTick(); - wrapper.vm.handleTokenValueSelected(mockTokenValue); + wrapper.vm.handleTokenValueSelected(mockTokenValue.title); expect(setTokenValueToRecentlyUsed).not.toHaveBeenCalled(); }); @@ -190,7 +188,7 @@ describe('BaseToken', () => { const glFilteredSearchToken = wrapperWithNoStubs.find(GlFilteredSearchToken); expect(glFilteredSearchToken.exists()).toBe(true); - expect(glFilteredSearchToken.props('config')).toBe(mockLabelToken); + expect(glFilteredSearchToken.props('config')).toEqual(mockProps.config); wrapperWithNoStubs.destroy(); }); @@ -239,6 +237,7 @@ describe('BaseToken', () => { stubs: { Portal: true }, }); }); + it('emits `fetch-suggestions` event on component after a delay when component emits `input` event', async () => { jest.useFakeTimers(); @@ -250,6 +249,32 @@ describe('BaseToken', () => { expect(wrapperWithNoStubs.emitted('fetch-suggestions')).toBeTruthy(); expect(wrapperWithNoStubs.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(); + + jest.runAllTimers(); + + expect(wrapperWithNoStubs.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(); + + expect(wrapperWithNoStubs.emitted('fetch-suggestions')[2]).toEqual(['foo']); + }); + }); }); }); }); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/epic_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/epic_token_spec.js deleted file mode 100644 index 6ee5d50d396..00000000000 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/epic_token_spec.js +++ /dev/null @@ -1,169 +0,0 @@ -import { GlFilteredSearchTokenSegment } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; -import MockAdapter from 'axios-mock-adapter'; -import Vue from 'vue'; -import VueApollo from 'vue-apollo'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import waitForPromises from 'helpers/wait_for_promises'; -import createFlash from '~/flash'; -import axios from '~/lib/utils/axios_utils'; - -import searchEpicsQuery from '~/vue_shared/components/filtered_search_bar/queries/search_epics.query.graphql'; -import EpicToken from '~/vue_shared/components/filtered_search_bar/tokens/epic_token.vue'; -import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; - -import { mockEpicToken, mockEpics, mockGroupEpicsQueryResponse } from '../mock_data'; - -jest.mock('~/flash'); -Vue.use(VueApollo); - -const defaultStubs = { - Portal: true, - GlFilteredSearchSuggestionList: { - template: '
', - methods: { - getValue: () => '=', - }, - }, -}; - -describe('EpicToken', () => { - let mock; - let wrapper; - let fakeApollo; - - const findBaseToken = () => wrapper.findComponent(BaseToken); - - function createComponent( - options = {}, - epicsQueryHandler = jest.fn().mockResolvedValue(mockGroupEpicsQueryResponse), - ) { - fakeApollo = createMockApollo([[searchEpicsQuery, epicsQueryHandler]]); - const { - config = mockEpicToken, - value = { data: '' }, - active = false, - stubs = defaultStubs, - } = options; - return mount(EpicToken, { - apolloProvider: fakeApollo, - propsData: { - config, - value, - active, - }, - provide: { - portalName: 'fake target', - alignSuggestions: function fakeAlignSuggestions() {}, - suggestionsListClass: 'custom-class', - }, - stubs, - }); - } - - beforeEach(() => { - mock = new MockAdapter(axios); - wrapper = createComponent(); - }); - - afterEach(() => { - mock.restore(); - wrapper.destroy(); - }); - - describe('computed', () => { - beforeEach(async () => { - wrapper = createComponent({ - data: { - epics: mockEpics, - }, - }); - - await wrapper.vm.$nextTick(); - }); - }); - - describe('methods', () => { - describe('fetchEpicsBySearchTerm', () => { - it('calls fetchEpics with provided searchTerm param', () => { - jest.spyOn(wrapper.vm, 'fetchEpics'); - - findBaseToken().vm.$emit('fetch-suggestions', 'foo'); - - expect(wrapper.vm.fetchEpics).toHaveBeenCalledWith('foo'); - }); - - it('sets response to `epics` when request is successful', async () => { - jest.spyOn(wrapper.vm, 'fetchEpics').mockResolvedValue({ - data: mockEpics, - }); - - findBaseToken().vm.$emit('fetch-suggestions'); - - await waitForPromises(); - - expect(wrapper.vm.epics).toEqual(mockEpics); - }); - - it('calls `createFlash` with flash error message when request fails', async () => { - jest.spyOn(wrapper.vm, 'fetchEpics').mockRejectedValue({}); - - findBaseToken().vm.$emit('fetch-suggestions', 'foo'); - - await waitForPromises(); - - expect(createFlash).toHaveBeenCalledWith({ - message: 'There was a problem fetching epics.', - }); - }); - - it('sets `loading` to false when request completes', async () => { - jest.spyOn(wrapper.vm, 'fetchEpics').mockRejectedValue({}); - - findBaseToken().vm.$emit('fetch-suggestions', 'foo'); - - await waitForPromises(); - - expect(wrapper.vm.loading).toBe(false); - }); - }); - }); - - describe('template', () => { - const getTokenValueEl = () => wrapper.findAllComponents(GlFilteredSearchTokenSegment).at(2); - - beforeEach(async () => { - wrapper = createComponent({ - value: { data: `${mockEpics[0].title}::&${mockEpics[0].iid}` }, - data: { epics: mockEpics }, - }); - - await wrapper.vm.$nextTick(); - }); - - it('renders BaseToken component', () => { - expect(findBaseToken().exists()).toBe(true); - }); - - it('renders token item when value is selected', () => { - const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); - - expect(tokenSegments).toHaveLength(3); - expect(tokenSegments.at(2).text()).toBe(`${mockEpics[0].title}::&${mockEpics[0].iid}`); - }); - - it.each` - value | valueType | tokenValueString - ${`${mockEpics[0].title}::&${mockEpics[0].iid}`} | ${'string'} | ${`${mockEpics[0].title}::&${mockEpics[0].iid}`} - ${`${mockEpics[1].title}::&${mockEpics[1].iid}`} | ${'number'} | ${`${mockEpics[1].title}::&${mockEpics[1].iid}`} - `('renders token item when selection is a $valueType', async ({ value, tokenValueString }) => { - wrapper.setProps({ - value: { data: value }, - }); - - await wrapper.vm.$nextTick(); - - expect(getTokenValueEl().text()).toBe(tokenValueString); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js deleted file mode 100644 index 44bc16adb97..00000000000 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js +++ /dev/null @@ -1,116 +0,0 @@ -import { - GlFilteredSearchToken, - GlFilteredSearchTokenSegment, - GlFilteredSearchSuggestion, -} from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; -import waitForPromises from 'helpers/wait_for_promises'; -import createFlash from '~/flash'; -import IterationToken from '~/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue'; -import { mockIterationToken, mockIterations } from '../mock_data'; - -jest.mock('~/flash'); - -describe('IterationToken', () => { - const id = 123; - let wrapper; - - const createComponent = ({ - config = mockIterationToken, - value = { data: '' }, - active = false, - stubs = {}, - provide = {}, - } = {}) => - mount(IterationToken, { - propsData: { - active, - config, - value, - }, - provide: { - portalName: 'fake target', - alignSuggestions: function fakeAlignSuggestions() {}, - suggestionsListClass: () => 'custom-class', - ...provide, - }, - stubs, - }); - - afterEach(() => { - wrapper.destroy(); - }); - - describe('when iteration cadence feature is available', () => { - beforeEach(async () => { - wrapper = createComponent({ - active: true, - config: { ...mockIterationToken, initialIterations: mockIterations }, - value: { data: 'i' }, - stubs: { Portal: true }, - provide: { - glFeatures: { - iterationCadences: true, - }, - }, - }); - - await wrapper.setData({ loading: false }); - }); - - it('renders iteration start date and due date', () => { - const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); - - expect(suggestions.at(3).text()).toContain('Nov 5, 2021 - Nov 10, 2021'); - }); - }); - - it('renders iteration value', async () => { - wrapper = createComponent({ value: { data: id } }); - - await wrapper.vm.$nextTick(); - - const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment); - - expect(tokenSegments).toHaveLength(3); // `Iteration` `=` `gitlab-org: #1` - expect(tokenSegments.at(2).text()).toBe(id.toString()); - }); - - it('fetches initial values', () => { - const fetchIterationsSpy = jest.fn().mockResolvedValue(); - - wrapper = createComponent({ - config: { ...mockIterationToken, fetchIterations: fetchIterationsSpy }, - value: { data: id }, - }); - - expect(fetchIterationsSpy).toHaveBeenCalledWith(id); - }); - - it('fetches iterations on user input', () => { - const search = 'hello'; - const fetchIterationsSpy = jest.fn().mockResolvedValue(); - - wrapper = createComponent({ - config: { ...mockIterationToken, fetchIterations: fetchIterationsSpy }, - }); - - wrapper.findComponent(GlFilteredSearchToken).vm.$emit('input', { data: search }); - - expect(fetchIterationsSpy).toHaveBeenCalledWith(search); - }); - - it('renders error message when request fails', async () => { - const fetchIterationsSpy = jest.fn().mockRejectedValue(); - - wrapper = createComponent({ - config: { ...mockIterationToken, fetchIterations: fetchIterationsSpy }, - }); - - await waitForPromises(); - - expect(createFlash).toHaveBeenCalledWith({ - message: 'There was a problem fetching iterations.', - }); - }); -}); 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 936841651d1..4a098db33c5 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 @@ -9,18 +9,15 @@ import MockAdapter from 'axios-mock-adapter'; import waitForPromises from 'helpers/wait_for_promises'; import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; -import { sortMilestonesByDueDate } from '~/milestones/milestone_utils'; +import { sortMilestonesByDueDate } from '~/milestones/utils'; -import { - DEFAULT_MILESTONES, - DEFAULT_MILESTONES_GRAPHQL, -} from '~/vue_shared/components/filtered_search_bar/constants'; +import { DEFAULT_MILESTONES } from '~/vue_shared/components/filtered_search_bar/constants'; import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue'; import { mockMilestoneToken, mockMilestones, mockRegularMilestone } from '../mock_data'; jest.mock('~/flash'); -jest.mock('~/milestones/milestone_utils'); +jest.mock('~/milestones/utils'); const defaultStubs = { Portal: true, @@ -199,12 +196,12 @@ describe('MilestoneToken', () => { beforeEach(() => { wrapper = createComponent({ active: true, - config: { ...mockMilestoneToken, defaultMilestones: DEFAULT_MILESTONES_GRAPHQL }, + config: { ...mockMilestoneToken, defaultMilestones: DEFAULT_MILESTONES }, }); }); it('finds the correct value from the activeToken', () => { - DEFAULT_MILESTONES_GRAPHQL.forEach(({ value, title }) => { + DEFAULT_MILESTONES.forEach(({ value, title }) => { const activeToken = wrapper.vm.getActiveMilestone([], value); expect(activeToken.title).toEqual(title); 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 b804ff97b82..b2f246a5985 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 @@ -8,7 +8,7 @@ import { mockReleaseToken } from '../mock_data'; jest.mock('~/flash'); describe('ReleaseToken', () => { - const id = 123; + const id = '123'; let wrapper; const createComponent = ({ config = mockReleaseToken, value = { data: '' } } = {}) => diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/weight_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/weight_token_spec.js deleted file mode 100644 index 4277899f8db..00000000000 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/weight_token_spec.js +++ /dev/null @@ -1,38 +0,0 @@ -import { GlFilteredSearchTokenSegment } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; -import WeightToken from '~/vue_shared/components/filtered_search_bar/tokens/weight_token.vue'; -import { mockWeightToken } from '../mock_data'; - -jest.mock('~/flash'); - -describe('WeightToken', () => { - const weight = '3'; - let wrapper; - - const createComponent = ({ config = mockWeightToken, value = { data: '' } } = {}) => - mount(WeightToken, { - propsData: { - active: false, - config, - value, - }, - provide: { - portalName: 'fake target', - alignSuggestions: function fakeAlignSuggestions() {}, - suggestionsListClass: () => 'custom-class', - }, - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('renders weight value', () => { - wrapper = createComponent({ value: { data: weight } }); - - const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment); - - expect(tokenSegments).toHaveLength(3); // `Weight` `=` `3` - expect(tokenSegments.at(2).text()).toBe(weight); - }); -}); diff --git a/spec/frontend/vue_shared/components/form/__snapshots__/title_spec.js.snap b/spec/frontend/vue_shared/components/form/__snapshots__/title_spec.js.snap index ff1dad2de68..58ad1f681bc 100644 --- a/spec/frontend/vue_shared/components/form/__snapshots__/title_spec.js.snap +++ b/spec/frontend/vue_shared/components/form/__snapshots__/title_spec.js.snap @@ -5,6 +5,7 @@ exports[`Title edit field matches the snapshot 1`] = ` label="Title" label-for="title-field-edit" labeldescription="" + optionaltext="(optional)" > diff --git a/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js b/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js new file mode 100644 index 00000000000..b67385cc43e --- /dev/null +++ b/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js @@ -0,0 +1,231 @@ +import { merge } from 'lodash'; +import { GlFormInputGroup } from '@gitlab/ui'; + +import InputCopyToggleVisibility from '~/vue_shared/components/form/input_copy_toggle_visibility.vue'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; + +import { mountExtended } from 'helpers/vue_test_utils_helper'; + +describe('InputCopyToggleVisibility', () => { + let wrapper; + + afterEach(() => { + wrapper.destroy(); + }); + + const valueProp = 'hR8x1fuJbzwu5uFKLf9e'; + + const createComponent = (options = {}) => { + wrapper = mountExtended( + InputCopyToggleVisibility, + merge({}, options, { + directives: { + GlTooltip: createMockDirective(), + }, + }), + ); + }; + + const findFormInputGroup = () => wrapper.findComponent(GlFormInputGroup); + const findFormInput = () => findFormInputGroup().find('input'); + const findRevealButton = () => + wrapper.findByRole('button', { + name: InputCopyToggleVisibility.i18n.toggleVisibilityLabelReveal, + }); + const findHideButton = () => + wrapper.findByRole('button', { + name: InputCopyToggleVisibility.i18n.toggleVisibilityLabelHide, + }); + const findCopyButton = () => wrapper.findComponent(ClipboardButton); + const createCopyEvent = () => { + const event = new Event('copy', { cancelable: true }); + Object.assign(event, { preventDefault: jest.fn(), clipboardData: { setData: jest.fn() } }); + + return event; + }; + + const itDoesNotModifyCopyEvent = () => { + it('does not modify copy event', () => { + const event = createCopyEvent(); + + findFormInput().element.dispatchEvent(event); + + expect(event.clipboardData.setData).not.toHaveBeenCalled(); + expect(event.preventDefault).not.toHaveBeenCalled(); + }); + }; + + describe('when `value` prop is passed', () => { + beforeEach(() => { + createComponent({ + propsData: { + value: valueProp, + }, + }); + }); + + it('displays value as hidden', () => { + expect(findFormInputGroup().props('value')).toBe('********************'); + }); + + it('saves actual value to clipboard when manually copied', () => { + const event = createCopyEvent(); + findFormInput().element.dispatchEvent(event); + + expect(event.clipboardData.setData).toHaveBeenCalledWith('text/plain', valueProp); + expect(event.preventDefault).toHaveBeenCalled(); + }); + + describe('visibility toggle button', () => { + it('renders a reveal button', () => { + const revealButton = findRevealButton(); + + expect(revealButton.exists()).toBe(true); + + const tooltip = getBinding(revealButton.element, 'gl-tooltip'); + + expect(tooltip.value).toBe(InputCopyToggleVisibility.i18n.toggleVisibilityLabelReveal); + }); + + describe('when clicked', () => { + beforeEach(async () => { + await findRevealButton().trigger('click'); + }); + + it('displays value', () => { + expect(findFormInputGroup().props('value')).toBe(valueProp); + }); + + it('renders a hide button', () => { + const hideButton = findHideButton(); + + expect(hideButton.exists()).toBe(true); + + const tooltip = getBinding(hideButton.element, 'gl-tooltip'); + + expect(tooltip.value).toBe(InputCopyToggleVisibility.i18n.toggleVisibilityLabelHide); + }); + + it('emits `visibility-change` event', () => { + expect(wrapper.emitted('visibility-change')[0]).toEqual([true]); + }); + }); + }); + + describe('copy button', () => { + it('renders button with correct props passed', () => { + expect(findCopyButton().props()).toMatchObject({ + text: valueProp, + title: 'Copy', + }); + }); + + describe('when clicked', () => { + beforeEach(async () => { + await findCopyButton().trigger('click'); + }); + + it('emits `copy` event', () => { + expect(wrapper.emitted('copy')[0]).toEqual([]); + }); + }); + }); + }); + + describe('when `value` prop is not passed', () => { + beforeEach(() => { + createComponent(); + }); + + it('displays value as hidden with 20 asterisks', () => { + expect(findFormInputGroup().props('value')).toBe('********************'); + }); + }); + + describe('when `initialVisibility` prop is `true`', () => { + beforeEach(() => { + createComponent({ + propsData: { + value: valueProp, + initialVisibility: true, + }, + }); + }); + + it('displays value', () => { + expect(findFormInputGroup().props('value')).toBe(valueProp); + }); + + itDoesNotModifyCopyEvent(); + }); + + describe('when `showToggleVisibilityButton` is `false`', () => { + beforeEach(() => { + createComponent({ + propsData: { + value: valueProp, + showToggleVisibilityButton: false, + }, + }); + }); + + it('does not render visibility toggle button', () => { + expect(findRevealButton().exists()).toBe(false); + expect(findHideButton().exists()).toBe(false); + }); + + it('displays value', () => { + expect(findFormInputGroup().props('value')).toBe(valueProp); + }); + + itDoesNotModifyCopyEvent(); + }); + + describe('when `showCopyButton` is `false`', () => { + beforeEach(() => { + createComponent({ + propsData: { + showCopyButton: false, + }, + }); + }); + + it('does not render copy button', () => { + expect(findCopyButton().exists()).toBe(false); + }); + }); + + it('passes `formInputGroupProps` prop to `GlFormInputGroup`', () => { + createComponent({ + propsData: { + formInputGroupProps: { + label: 'Foo bar', + }, + }, + }); + + expect(findFormInputGroup().props('label')).toBe('Foo bar'); + }); + + it('passes `copyButtonTitle` prop to `ClipboardButton`', () => { + createComponent({ + propsData: { + copyButtonTitle: 'Copy token', + }, + }); + + expect(findCopyButton().props('title')).toBe('Copy token'); + }); + + it('renders slots in `gl-form-group`', () => { + const description = 'Mock input description'; + createComponent({ + slots: { + description, + }, + }); + + expect(wrapper.findByText(description).exists()).toBe(true); + }); +}); 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 390a70792f3..b837a998cd6 100644 --- a/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js +++ b/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js @@ -1,12 +1,12 @@ import { GlModal } from '@gitlab/ui'; -import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { shallowMount } from '@vue/test-utils'; +import Vue 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'; import createState from '~/vuex_shared/modules/modal/state'; -const localVue = createLocalVue(); -localVue.use(Vuex); +Vue.use(Vuex); const TEST_SLOT = 'Lorem ipsum modal dolar sit.'; const TEST_MODAL_ID = 'my-modal-id'; @@ -36,7 +36,6 @@ describe('GlModalVuex', () => { wrapper = shallowMount(GlModalVuex, { ...options, - localVue, store, propsData, stubs: { diff --git a/spec/frontend/vue_shared/components/header_ci_component_spec.js b/spec/frontend/vue_shared/components/header_ci_component_spec.js index b76f475a6fb..aea76f164f0 100644 --- a/spec/frontend/vue_shared/components/header_ci_component_spec.js +++ b/spec/frontend/vue_shared/components/header_ci_component_spec.js @@ -1,4 +1,4 @@ -import { GlButton, GlAvatarLink } from '@gitlab/ui'; +import { GlButton, GlAvatarLink, GlTooltip } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import CiIconBadge from '~/vue_shared/components/ci_badge_link.vue'; @@ -32,6 +32,7 @@ describe('Header CI Component', () => { const findTimeAgo = () => wrapper.findComponent(TimeagoTooltip); const findUserLink = () => wrapper.findComponent(GlAvatarLink); const findSidebarToggleBtn = () => wrapper.findComponent(GlButton); + const findStatusTooltip = () => wrapper.findComponent(GlTooltip); const findActionButtons = () => wrapper.findByTestId('ci-header-action-buttons'); const findHeaderItemText = () => wrapper.findByTestId('ci-header-item-text'); @@ -91,6 +92,21 @@ describe('Header CI Component', () => { }); }); + describe('when the user has a status', () => { + const STATUS_MESSAGE = 'Working on exciting features...'; + + beforeEach(() => { + createComponent({ + itemName: 'Pipeline', + user: { ...defaultProps.user, status: { message: STATUS_MESSAGE } }, + }); + }); + + it('renders a tooltip', () => { + expect(findStatusTooltip().text()).toBe(STATUS_MESSAGE); + }); + }); + describe('with data from GraphQL', () => { const userId = 1; diff --git a/spec/frontend/vue_shared/components/issuable/issuable_header_warnings_spec.js b/spec/frontend/vue_shared/components/issuable/issuable_header_warnings_spec.js deleted file mode 100644 index ad8331afcff..00000000000 --- a/spec/frontend/vue_shared/components/issuable/issuable_header_warnings_spec.js +++ /dev/null @@ -1,89 +0,0 @@ -import { createLocalVue } from '@vue/test-utils'; -import Vuex from 'vuex'; -import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { createStore as createMrStore } from '~/mr_notes/stores'; -import createIssueStore from '~/notes/stores'; -import IssuableHeaderWarnings from '~/vue_shared/components/issuable/issuable_header_warnings.vue'; - -const ISSUABLE_TYPE_ISSUE = 'issue'; -const ISSUABLE_TYPE_MR = 'merge request'; - -const localVue = createLocalVue(); -localVue.use(Vuex); - -describe('IssuableHeaderWarnings', () => { - let wrapper; - - const findConfidentialIcon = () => wrapper.findByTestId('confidential'); - const findLockedIcon = () => wrapper.findByTestId('locked'); - const findHiddenIcon = () => wrapper.findByTestId('hidden'); - - const renderTestMessage = (renders) => (renders ? 'renders' : 'does not render'); - - const createComponent = ({ store, provide }) => { - wrapper = shallowMountExtended(IssuableHeaderWarnings, { - store, - localVue, - provide, - directives: { - GlTooltip: createMockDirective(), - }, - }); - }; - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - describe.each` - issuableType - ${ISSUABLE_TYPE_ISSUE} | ${ISSUABLE_TYPE_MR} - `(`when issuableType=$issuableType`, ({ issuableType }) => { - describe.each` - lockStatus | confidentialStatus | hiddenStatus - ${true} | ${true} | ${false} - ${true} | ${false} | ${false} - ${false} | ${true} | ${false} - ${false} | ${false} | ${false} - ${true} | ${true} | ${true} - ${true} | ${false} | ${true} - ${false} | ${true} | ${true} - ${false} | ${false} | ${true} - `( - `when locked=$lockStatus, confidential=$confidentialStatus, and hidden=$hiddenStatus`, - ({ lockStatus, confidentialStatus, hiddenStatus }) => { - const store = issuableType === ISSUABLE_TYPE_ISSUE ? createIssueStore() : createMrStore(); - - beforeEach(() => { - store.getters.getNoteableData.confidential = confidentialStatus; - store.getters.getNoteableData.discussion_locked = lockStatus; - - createComponent({ store, provide: { hidden: hiddenStatus } }); - }); - - it(`${renderTestMessage(lockStatus)} the locked icon`, () => { - expect(findLockedIcon().exists()).toBe(lockStatus); - }); - - it(`${renderTestMessage(confidentialStatus)} the confidential icon`, () => { - expect(findConfidentialIcon().exists()).toBe(confidentialStatus); - }); - - it(`${renderTestMessage(confidentialStatus)} the hidden icon`, () => { - const hiddenIcon = findHiddenIcon(); - - expect(hiddenIcon.exists()).toBe(hiddenStatus); - - if (hiddenStatus) { - expect(hiddenIcon.attributes('title')).toBe( - 'This issue is hidden because its author has been banned', - ); - expect(getBinding(hiddenIcon.element, 'gl-tooltip')).not.toBeUndefined(); - } - }); - }, - ); - }); -}); diff --git a/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js b/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js deleted file mode 100644 index f74b9b37197..00000000000 --- a/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js +++ /dev/null @@ -1,145 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import { mockAssigneesList } from 'jest/boards/mock_data'; -import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue'; -import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; - -const TEST_CSS_CLASSES = 'test-classes'; -const TEST_MAX_VISIBLE = 4; -const TEST_ICON_SIZE = 16; - -describe('IssueAssigneesComponent', () => { - let wrapper; - let vm; - - const factory = (props) => { - wrapper = shallowMount(IssueAssignees, { - propsData: { - assignees: mockAssigneesList, - ...props, - }, - }); - vm = wrapper.vm; - }; - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - const findTooltipText = () => wrapper.find('.js-assignee-tooltip').text(); - const findAvatars = () => wrapper.findAll(UserAvatarLink); - const findOverflowCounter = () => wrapper.find('.avatar-counter'); - - it('returns default data props', () => { - factory({ assignees: mockAssigneesList }); - expect(vm.iconSize).toBe(24); - expect(vm.maxVisible).toBe(3); - expect(vm.maxAssignees).toBe(99); - }); - - describe.each` - numAssignees | maxVisible | expectedShown | expectedHidden - ${0} | ${3} | ${0} | ${''} - ${1} | ${3} | ${1} | ${''} - ${2} | ${3} | ${2} | ${''} - ${3} | ${3} | ${3} | ${''} - ${4} | ${3} | ${2} | ${'+2'} - ${5} | ${2} | ${1} | ${'+4'} - ${1000} | ${5} | ${4} | ${'99+'} - `( - 'with assignees ($numAssignees) and maxVisible ($maxVisible)', - ({ numAssignees, maxVisible, expectedShown, expectedHidden }) => { - beforeEach(() => { - factory({ assignees: Array(numAssignees).fill({}), maxVisible }); - }); - - if (expectedShown) { - it('shows assignee avatars', () => { - expect(findAvatars().length).toEqual(expectedShown); - }); - } else { - it('does not show assignee avatars', () => { - expect(findAvatars().length).toEqual(0); - }); - } - - if (expectedHidden) { - it('shows overflow counter', () => { - const hiddenCount = numAssignees - expectedShown; - - expect(findOverflowCounter().exists()).toBe(true); - expect(findOverflowCounter().text()).toEqual(expectedHidden.toString()); - expect(findOverflowCounter().attributes('title')).toEqual( - `${hiddenCount} more assignees`, - ); - }); - } else { - it('does not show overflow counter', () => { - expect(findOverflowCounter().exists()).toBe(false); - }); - } - }, - ); - - describe('when mounted', () => { - beforeEach(() => { - factory({ - imgCssClasses: TEST_CSS_CLASSES, - maxVisible: TEST_MAX_VISIBLE, - iconSize: TEST_ICON_SIZE, - }); - }); - - it('computes alt text for assignee avatar', () => { - expect(vm.avatarUrlTitle(mockAssigneesList[0])).toBe('Assigned to Terrell Graham'); - }); - - it('renders assignee', () => { - const data = findAvatars().wrappers.map((x) => ({ - ...x.props(), - })); - - const expected = mockAssigneesList.slice(0, TEST_MAX_VISIBLE - 1).map((x) => - expect.objectContaining({ - linkHref: x.web_url, - imgAlt: `Assigned to ${x.name}`, - imgCssClasses: TEST_CSS_CLASSES, - imgSrc: x.avatar_url, - imgSize: TEST_ICON_SIZE, - }), - ); - - expect(data).toEqual(expected); - }); - - describe('assignee tooltips', () => { - it('renders "Assignee" header', () => { - expect(findTooltipText()).toContain('Assignee'); - }); - - it('renders assignee name', () => { - expect(findTooltipText()).toContain('Terrell Graham'); - }); - - it('renders assignee @username', () => { - expect(findTooltipText()).toContain('@monserrate.gleichner'); - }); - - it('does not render `@` when username not available', () => { - const userName = 'User without username'; - factory({ - assignees: [ - { - name: userName, - }, - ], - }); - - const tooltipText = findTooltipText(); - - expect(tooltipText).toContain(userName); - expect(tooltipText).not.toContain('@'); - }); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js b/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js deleted file mode 100644 index 9a121050225..00000000000 --- a/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js +++ /dev/null @@ -1,160 +0,0 @@ -import { GlIcon } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; - -import { mockMilestone } from 'jest/boards/mock_data'; -import IssueMilestone from '~/vue_shared/components/issue/issue_milestone.vue'; - -const createComponent = (milestone = mockMilestone) => { - const Component = Vue.extend(IssueMilestone); - - return shallowMount(Component, { - propsData: { - milestone, - }, - }); -}; - -describe('IssueMilestoneComponent', () => { - let wrapper; - let vm; - - beforeEach((done) => { - wrapper = createComponent(); - - ({ vm } = wrapper); - - Vue.nextTick(done); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - describe('computed', () => { - describe('isMilestoneStarted', () => { - it('should return `false` when milestoneStart prop is not defined', async () => { - wrapper.setProps({ - milestone: { ...mockMilestone, start_date: '' }, - }); - await wrapper.vm.$nextTick(); - - expect(wrapper.vm.isMilestoneStarted).toBe(false); - }); - - it('should return `true` when milestone start date is past current date', async () => { - await wrapper.setProps({ - milestone: { ...mockMilestone, start_date: '1990-07-22' }, - }); - await wrapper.vm.$nextTick(); - - expect(wrapper.vm.isMilestoneStarted).toBe(true); - }); - }); - - describe('isMilestonePastDue', () => { - it('should return `false` when milestoneDue prop is not defined', async () => { - wrapper.setProps({ - milestone: { ...mockMilestone, due_date: '' }, - }); - await wrapper.vm.$nextTick(); - - expect(wrapper.vm.isMilestonePastDue).toBe(false); - }); - - it('should return `true` when milestone due is past current date', () => { - wrapper.setProps({ - milestone: { ...mockMilestone, due_date: '1990-07-22' }, - }); - - expect(wrapper.vm.isMilestonePastDue).toBe(true); - }); - }); - - describe('milestoneDatesAbsolute', () => { - it('returns string containing absolute milestone due date', () => { - expect(vm.milestoneDatesAbsolute).toBe('(December 31, 2019)'); - }); - - it('returns string containing absolute milestone start date when due date is not present', async () => { - wrapper.setProps({ - milestone: { ...mockMilestone, due_date: '' }, - }); - await wrapper.vm.$nextTick(); - - expect(wrapper.vm.milestoneDatesAbsolute).toBe('(January 1, 2018)'); - }); - - it('returns empty string when both milestone start and due dates are not present', async () => { - wrapper.setProps({ - milestone: { ...mockMilestone, start_date: '', due_date: '' }, - }); - await wrapper.vm.$nextTick(); - - expect(wrapper.vm.milestoneDatesAbsolute).toBe(''); - }); - }); - - describe('milestoneDatesHuman', () => { - it('returns string containing milestone due date when date is yet to be due', async () => { - wrapper.setProps({ - milestone: { ...mockMilestone, due_date: `${new Date().getFullYear() + 10}-01-01` }, - }); - await wrapper.vm.$nextTick(); - - expect(wrapper.vm.milestoneDatesHuman).toContain('years remaining'); - }); - - it('returns string containing milestone start date when date has already started and due date is not present', async () => { - wrapper.setProps({ - milestone: { ...mockMilestone, start_date: '1990-07-22', due_date: '' }, - }); - await wrapper.vm.$nextTick(); - - expect(wrapper.vm.milestoneDatesHuman).toContain('Started'); - }); - - it('returns string containing milestone start date when date is yet to start and due date is not present', async () => { - wrapper.setProps({ - milestone: { - ...mockMilestone, - start_date: `${new Date().getFullYear() + 10}-01-01`, - due_date: '', - }, - }); - await wrapper.vm.$nextTick(); - - expect(wrapper.vm.milestoneDatesHuman).toContain('Starts'); - }); - - it('returns empty string when milestone start and due dates are not present', async () => { - wrapper.setProps({ - milestone: { ...mockMilestone, start_date: '', due_date: '' }, - }); - await wrapper.vm.$nextTick(); - - expect(wrapper.vm.milestoneDatesHuman).toBe(''); - }); - }); - }); - - describe('template', () => { - it('renders component root element with class `issue-milestone-details`', () => { - expect(vm.$el.classList.contains('issue-milestone-details')).toBe(true); - }); - - it('renders milestone icon', () => { - expect(wrapper.find(GlIcon).props('name')).toBe('clock'); - }); - - it('renders milestone title', () => { - expect(vm.$el.querySelector('.milestone-title').innerText.trim()).toBe(mockMilestone.title); - }); - - it('renders milestone tooltip', () => { - expect(vm.$el.querySelector('.js-item-milestone').innerText.trim()).toContain( - mockMilestone.title, - ); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js b/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js deleted file mode 100644 index 6ab828efebe..00000000000 --- a/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js +++ /dev/null @@ -1,206 +0,0 @@ -import { mount } from '@vue/test-utils'; -import { TEST_HOST } from 'helpers/test_constants'; -import IssueDueDate from '~/boards/components/issue_due_date.vue'; -import { formatDate } from '~/lib/utils/datetime_utility'; -import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue'; -import { defaultAssignees, defaultMilestone } from './related_issuable_mock_data'; - -describe('RelatedIssuableItem', () => { - let wrapper; - - function mountComponent({ mountMethod = mount, stubs = {}, props = {}, slots = {} } = {}) { - wrapper = mountMethod(RelatedIssuableItem, { - propsData: props, - slots, - stubs, - }); - } - - const props = { - idKey: 1, - displayReference: 'gitlab-org/gitlab-test#1', - pathIdSeparator: '#', - path: `${TEST_HOST}/path`, - title: 'title', - confidential: true, - dueDate: '1990-12-31', - weight: 10, - createdAt: '2018-12-01T00:00:00.00Z', - milestone: defaultMilestone, - assignees: defaultAssignees, - eventNamespace: 'relatedIssue', - }; - const slots = { - dueDate: '
', - weight: '
', - }; - - const findRemoveButton = () => wrapper.find({ ref: 'removeButton' }); - const findLockIcon = () => wrapper.find({ ref: 'lockIcon' }); - - beforeEach(() => { - mountComponent({ props, slots }); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('contains issuable-info-container class when canReorder is false', () => { - expect(wrapper.props('canReorder')).toBe(false); - expect(wrapper.find('.issuable-info-container').exists()).toBe(true); - }); - - it('does not render token state', () => { - expect(wrapper.find('.text-secondary svg').exists()).toBe(false); - }); - - it('does not render remove button', () => { - expect(wrapper.find({ ref: 'removeButton' }).exists()).toBe(false); - }); - - describe('token title', () => { - it('links to computedPath', () => { - expect(wrapper.find('.item-title a').attributes('href')).toEqual(wrapper.props('path')); - }); - - it('renders confidential icon', () => { - expect(wrapper.find('.confidential-icon').exists()).toBe(true); - }); - - it('renders title', () => { - expect(wrapper.find('.item-title a').text()).toEqual(props.title); - }); - }); - - describe('token state', () => { - const tokenState = () => wrapper.find({ ref: 'iconElementXL' }); - - beforeEach(() => { - wrapper.setProps({ state: 'opened' }); - }); - - it('renders if hasState', () => { - expect(tokenState().exists()).toBe(true); - }); - - it('renders state title', () => { - const stateTitle = tokenState().attributes('title'); - const formattedCreateDate = formatDate(props.createdAt); - - expect(stateTitle).toContain('Created'); - expect(stateTitle).toContain(`${formattedCreateDate}`); - }); - - it('renders aria label', () => { - expect(tokenState().attributes('aria-label')).toEqual('opened'); - }); - - it('renders open icon when open state', () => { - expect(tokenState().classes('issue-token-state-icon-open')).toBe(true); - }); - - it('renders close icon when close state', async () => { - wrapper.setProps({ - state: 'closed', - closedAt: '2018-12-01T00:00:00.00Z', - }); - await wrapper.vm.$nextTick(); - - expect(tokenState().classes('issue-token-state-icon-closed')).toBe(true); - }); - }); - - describe('token metadata', () => { - const tokenMetadata = () => wrapper.find('.item-meta'); - - it('renders item path and ID', () => { - const pathAndID = tokenMetadata().find('.item-path-id').text(); - - expect(pathAndID).toContain('gitlab-org/gitlab-test'); - expect(pathAndID).toContain('#1'); - }); - - it('renders milestone icon and name', () => { - const milestoneIcon = tokenMetadata().find('.item-milestone svg'); - const milestoneTitle = tokenMetadata().find('.item-milestone .milestone-title'); - - expect(milestoneIcon.attributes('data-testid')).toBe('clock-icon'); - expect(milestoneTitle.text()).toContain('Milestone title'); - }); - - it('renders due date component with correct due date', () => { - expect(wrapper.find(IssueDueDate).props('date')).toBe(props.dueDate); - }); - - it('does not render red icon for overdue issue that is closed', async () => { - mountComponent({ - props: { - ...props, - closedAt: '2018-12-01T00:00:00.00Z', - }, - }); - await wrapper.vm.$nextTick(); - - expect(wrapper.find(IssueDueDate).props('closed')).toBe(true); - }); - }); - - describe('token assignees', () => { - it('renders assignees avatars', () => { - // Expect 2 times 2 because assignees are rendered twice, due to layout issues - expect(wrapper.findAll('.item-assignees .user-avatar-link').length).toBeDefined(); - - expect(wrapper.find('.item-assignees .avatar-counter').text()).toContain('+2'); - }); - }); - - describe('remove button', () => { - beforeEach(() => { - wrapper.setProps({ canRemove: true }); - }); - - it('renders if canRemove', () => { - expect(findRemoveButton().exists()).toBe(true); - }); - - it('does not render the lock icon', () => { - expect(findLockIcon().exists()).toBe(false); - }); - - it('renders disabled button when removeDisabled', async () => { - wrapper.setData({ removeDisabled: true }); - await wrapper.vm.$nextTick(); - - expect(findRemoveButton().attributes('disabled')).toEqual('disabled'); - }); - - it('triggers onRemoveRequest when clicked', async () => { - findRemoveButton().trigger('click'); - await wrapper.vm.$nextTick(); - const { relatedIssueRemoveRequest } = wrapper.emitted(); - - expect(relatedIssueRemoveRequest.length).toBe(1); - expect(relatedIssueRemoveRequest[0]).toEqual([props.idKey]); - }); - }); - - describe('when issue is locked', () => { - const lockedMessage = 'Issues created from a vulnerability cannot be removed'; - - beforeEach(() => { - wrapper.setProps({ - isLocked: true, - lockedMessage, - }); - }); - - it('does not render the remove button', () => { - expect(findRemoveButton().exists()).toBe(false); - }); - - it('renders the lock icon with the correct title', () => { - expect(findLockIcon().attributes('title')).toBe(lockedMessage); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/issue/related_issuable_mock_data.js b/spec/frontend/vue_shared/components/issue/related_issuable_mock_data.js deleted file mode 100644 index 6cdb945ec20..00000000000 --- a/spec/frontend/vue_shared/components/issue/related_issuable_mock_data.js +++ /dev/null @@ -1,123 +0,0 @@ -import { TEST_HOST } from 'helpers/test_constants'; - -export const defaultProps = { - endpoint: '/foo/bar/issues/1/related_issues', - currentNamespacePath: 'foo', - currentProjectPath: 'bar', -}; - -export const issuable1 = { - id: 200, - epicIssueId: 1, - confidential: false, - reference: 'foo/bar#123', - displayReference: '#123', - title: 'some title', - path: '/foo/bar/issues/123', - relationPath: '/foo/bar/issues/123/relation', - state: 'opened', - linkType: 'relates_to', - dueDate: '2010-11-22', - weight: 5, -}; - -export const issuable2 = { - id: 201, - epicIssueId: 2, - confidential: false, - reference: 'foo/bar#124', - displayReference: '#124', - title: 'some other thing', - path: '/foo/bar/issues/124', - relationPath: '/foo/bar/issues/124/relation', - state: 'opened', - linkType: 'blocks', -}; - -export const issuable3 = { - id: 202, - epicIssueId: 3, - confidential: false, - reference: 'foo/bar#125', - displayReference: '#125', - title: 'some other other thing', - path: '/foo/bar/issues/125', - relationPath: '/foo/bar/issues/125/relation', - state: 'opened', - linkType: 'is_blocked_by', -}; - -export const issuable4 = { - id: 203, - epicIssueId: 4, - confidential: false, - reference: 'foo/bar#126', - displayReference: '#126', - title: 'some other other other thing', - path: '/foo/bar/issues/126', - relationPath: '/foo/bar/issues/126/relation', - state: 'opened', -}; - -export const issuable5 = { - id: 204, - epicIssueId: 5, - confidential: false, - reference: 'foo/bar#127', - displayReference: '#127', - title: 'some other other other thing', - path: '/foo/bar/issues/127', - relationPath: '/foo/bar/issues/127/relation', - state: 'opened', -}; - -export const defaultMilestone = { - id: 1, - state: 'active', - title: 'Milestone title', - start_date: '2018-01-01', - due_date: '2019-12-31', -}; - -export const defaultAssignees = [ - { - id: 1, - name: 'Administrator', - username: 'root', - state: 'active', - avatar_url: `${TEST_HOST}`, - web_url: `${TEST_HOST}/root`, - status_tooltip_html: null, - path: '/root', - }, - { - id: 13, - name: 'Brooks Beatty', - username: 'brynn_champlin', - state: 'active', - avatar_url: `${TEST_HOST}`, - web_url: `${TEST_HOST}/brynn_champlin`, - status_tooltip_html: null, - path: '/brynn_champlin', - }, - { - id: 6, - name: 'Bryce Turcotte', - username: 'melynda', - state: 'active', - avatar_url: `${TEST_HOST}`, - web_url: `${TEST_HOST}/melynda`, - status_tooltip_html: null, - path: '/melynda', - }, - { - id: 20, - name: 'Conchita Eichmann', - username: 'juliana_gulgowski', - state: 'active', - avatar_url: `${TEST_HOST}`, - web_url: `${TEST_HOST}/juliana_gulgowski`, - status_tooltip_html: null, - path: '/juliana_gulgowski', - }, -]; diff --git a/spec/frontend/vue_shared/components/line_numbers_spec.js b/spec/frontend/vue_shared/components/line_numbers_spec.js new file mode 100644 index 00000000000..5bedd0ccd02 --- /dev/null +++ b/spec/frontend/vue_shared/components/line_numbers_spec.js @@ -0,0 +1,71 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlIcon, GlLink } from '@gitlab/ui'; +import LineNumbers from '~/vue_shared/components/line_numbers.vue'; + +describe('Line Numbers component', () => { + let wrapper; + const lines = 10; + + const createComponent = () => { + wrapper = shallowMount(LineNumbers, { propsData: { lines } }); + }; + + const findGlIcon = () => wrapper.findComponent(GlIcon); + const findLineNumbers = () => wrapper.findAllComponents(GlLink); + const findFirstLineNumber = () => findLineNumbers().at(0); + const findSecondLineNumber = () => findLineNumbers().at(1); + + beforeEach(() => createComponent()); + + afterEach(() => wrapper.destroy()); + + describe('rendering', () => { + it('renders Line Numbers', () => { + expect(findLineNumbers().length).toBe(lines); + expect(findFirstLineNumber().attributes()).toMatchObject({ + id: 'L1', + href: '#L1', + }); + }); + + it('renders a link icon', () => { + expect(findGlIcon().props()).toMatchObject({ + size: 12, + name: 'link', + }); + }); + }); + + describe('clicking a line number', () => { + let firstLineNumber; + let firstLineNumberElement; + + beforeEach(() => { + firstLineNumber = findFirstLineNumber(); + firstLineNumberElement = firstLineNumber.element; + + jest.spyOn(firstLineNumberElement, 'scrollIntoView'); + jest.spyOn(firstLineNumberElement.classList, 'add'); + jest.spyOn(firstLineNumberElement.classList, 'remove'); + + firstLineNumber.vm.$emit('click'); + }); + + it('adds the highlight (hll) class', () => { + expect(firstLineNumberElement.classList.add).toHaveBeenCalledWith('hll'); + }); + + it('removes the highlight (hll) class from a previously highlighted line', () => { + findSecondLineNumber().vm.$emit('click'); + + expect(firstLineNumberElement.classList.remove).toHaveBeenCalledWith('hll'); + }); + + it('scrolls the line into view', () => { + expect(firstLineNumberElement.scrollIntoView).toHaveBeenCalledWith({ + behavior: 'smooth', + block: 'center', + }); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/markdown/toolbar_spec.js b/spec/frontend/vue_shared/components/markdown/toolbar_spec.js index eddc4033a65..8bff85b0bda 100644 --- a/spec/frontend/vue_shared/components/markdown/toolbar_spec.js +++ b/spec/frontend/vue_shared/components/markdown/toolbar_spec.js @@ -1,24 +1,17 @@ import { mount } from '@vue/test-utils'; -import { isExperimentVariant } from '~/experimentation/utils'; -import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue'; -import { INVITE_MEMBERS_IN_COMMENT } from '~/invite_members/constants'; import Toolbar from '~/vue_shared/components/markdown/toolbar.vue'; -jest.mock('~/experimentation/utils', () => ({ isExperimentVariant: jest.fn() })); - describe('toolbar', () => { let wrapper; const createMountedWrapper = (props = {}) => { wrapper = mount(Toolbar, { propsData: { markdownDocsPath: '', ...props }, - stubs: { 'invite-members-trigger': true }, }); }; afterEach(() => { wrapper.destroy(); - isExperimentVariant.mockReset(); }); describe('user can attach file', () => { @@ -40,36 +33,4 @@ describe('toolbar', () => { expect(wrapper.vm.$el.querySelector('.uploading-container')).toBeNull(); }); }); - - describe('user can invite member', () => { - const findInviteLink = () => wrapper.find(InviteMembersTrigger); - - beforeEach(() => { - isExperimentVariant.mockReturnValue(true); - createMountedWrapper(); - }); - - it('should render the invite members trigger', () => { - expect(findInviteLink().exists()).toBe(true); - }); - - it('should have correct props', () => { - expect(findInviteLink().props().displayText).toBe('Invite Member'); - expect(findInviteLink().props().trackExperiment).toBe(INVITE_MEMBERS_IN_COMMENT); - expect(findInviteLink().props().triggerSource).toBe(INVITE_MEMBERS_IN_COMMENT); - }); - }); - - describe('user can not invite member', () => { - const findInviteLink = () => wrapper.find(InviteMembersTrigger); - - beforeEach(() => { - isExperimentVariant.mockReturnValue(false); - createMountedWrapper(); - }); - - it('should render the invite members trigger', () => { - expect(findInviteLink().exists()).toBe(false); - }); - }); }); diff --git a/spec/frontend/vue_shared/components/namespace_select/mock_data.js b/spec/frontend/vue_shared/components/namespace_select/mock_data.js new file mode 100644 index 00000000000..c9d96672e85 --- /dev/null +++ b/spec/frontend/vue_shared/components/namespace_select/mock_data.js @@ -0,0 +1,11 @@ +export const group = [ + { 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, +}; 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 new file mode 100644 index 00000000000..8f07f63993d --- /dev/null +++ b/spec/frontend/vue_shared/components/namespace_select/namespace_select_spec.js @@ -0,0 +1,86 @@ +import { GlDropdown, GlDropdownItem, GlDropdownSectionHeader } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import NamespaceSelect, { + i18n, +} from '~/vue_shared/components/namespace_select/namespace_select.vue'; +import { user, group, namespaces } from './mock_data'; + +describe('Namespace Select', () => { + let wrapper; + + const createComponent = (props = {}) => + shallowMountExtended(NamespaceSelect, { + propsData: { + data: namespaces, + ...props, + }, + }); + + 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 findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); + const findSectionHeaders = () => wrapper.findAllComponents(GlDropdownSectionHeader); + + beforeEach(() => { + wrapper = createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders the dropdown', () => { + expect(findDropdown().exists()).toBe(true); + }); + + it('renders each dropdown item', () => { + const items = findDropdownItems().wrappers; + expect(items).toHaveLength(flatNamespaces().length); + }); + + it('renders the human name for each item', () => { + const dropdownItems = wrappersText(findDropdownItems()); + const flatNames = flatNamespaces().map(({ humanName }) => humanName); + expect(dropdownItems).toEqual(flatNames); + }); + + it('sets the initial dropdown text', () => { + expect(selectedDropdownItemText()).toBe(i18n.DEFAULT_TEXT); + }); + + it('splits group and user namespaces', () => { + const headers = findSectionHeaders(); + expect(headers).toHaveLength(2); + expect(wrappersText(headers)).toEqual([i18n.GROUPS, i18n.USERS]); + }); + + it('sets the dropdown to full width', () => { + expect(findDropdownAttributes('block')).toBeUndefined(); + + wrapper = createComponent({ fullWidth: true }); + + expect(findDropdownAttributes('block')).not.toBeUndefined(); + expect(findDropdownAttributes('block')).toBe('true'); + }); + + describe('with a selected namespace', () => { + const selectedGroupIndex = 1; + const selectedItem = group[selectedGroupIndex]; + + beforeEach(() => { + findDropdownItems().at(selectedGroupIndex).vm.$emit('click'); + }); + + it('sets the dropdown text', () => { + expect(selectedDropdownItemText()).toBe(selectedItem.humanName); + }); + + it('emits the `select` event when a namespace is selected', () => { + const args = [selectedItem]; + expect(wrapper.emitted('select')).toEqual([args]); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js b/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js index 0f30b50da0b..c8dab0204d3 100644 --- a/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js +++ b/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js @@ -1,10 +1,11 @@ -import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; import Vuex from 'vuex'; import IssuePlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue'; +import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import { userDataMock } from '../../../notes/mock_data'; -const localVue = createLocalVue(); -localVue.use(Vuex); +Vue.use(Vuex); const getters = { getUserData: () => userDataMock, @@ -15,9 +16,8 @@ describe('Issue placeholder note component', () => { const findNote = () => wrapper.find({ ref: 'note' }); - const createComponent = (isIndividual = false) => { + const createComponent = (isIndividual = false, propsData = {}) => { wrapper = shallowMount(IssuePlaceholderNote, { - localVue, store: new Vuex.Store({ getters, }), @@ -26,6 +26,7 @@ describe('Issue placeholder note component', () => { body: 'Foo', individual_note: isIndividual, }, + ...propsData, }, }); }; @@ -52,4 +53,17 @@ describe('Issue placeholder note component', () => { expect(findNote().classes()).toContain('discussion'); }); + + describe('avatar size', () => { + it.each` + size | line | isOverviewTab + ${40} | ${null} | ${false} + ${24} | ${{ line_code: '123' }} | ${false} + ${40} | ${{ line_code: '123' }} | ${true} + `('renders avatar $size for $line and $isOverviewTab', ({ size, line, isOverviewTab }) => { + createComponent(false, { line, isOverviewTab }); + + expect(wrapper.findComponent(UserAvatarLink).props('imgSize')).toBe(size); + }); + }); }); diff --git a/spec/frontend/vue_shared/components/pagination_bar/pagination_bar_spec.js b/spec/frontend/vue_shared/components/pagination_bar/pagination_bar_spec.js new file mode 100644 index 00000000000..08119dee8af --- /dev/null +++ b/spec/frontend/vue_shared/components/pagination_bar/pagination_bar_spec.js @@ -0,0 +1,93 @@ +import { GlPagination, GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue'; +import PaginationLinks from '~/vue_shared/components/pagination_links.vue'; + +describe('Pagination bar', () => { + const DEFAULT_PROPS = { + pageInfo: { + total: 50, + totalPages: 3, + page: 3, + perPage: 20, + }, + }; + let wrapper; + + const createComponent = (propsData) => { + wrapper = mount(PaginationBar, { + propsData: { + ...DEFAULT_PROPS, + ...propsData, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('events', () => { + beforeEach(() => { + createComponent(); + }); + + it('emits set-page event when page is selected', () => { + const NEXT_PAGE = 3; + // PaginationLinks uses prop instead of event for handling page change + // So we go one level deep to test this + wrapper + .findComponent(PaginationLinks) + .findComponent(GlPagination) + .vm.$emit('input', NEXT_PAGE); + expect(wrapper.emitted('set-page')).toEqual([[NEXT_PAGE]]); + }); + + it('emits set-page-size event when page size is selected', () => { + const firstItemInPageSizeDropdown = wrapper.findComponent(GlDropdownItem); + firstItemInPageSizeDropdown.vm.$emit('click'); + + const [emittedPageSizeChange] = wrapper.emitted('set-page-size')[0]; + expect(firstItemInPageSizeDropdown.text()).toMatchInterpolatedText( + `${emittedPageSizeChange} items per page`, + ); + }); + }); + + it('renders current page size', () => { + const CURRENT_PAGE_SIZE = 40; + + createComponent({ + pageInfo: { + ...DEFAULT_PROPS.pageInfo, + perPage: CURRENT_PAGE_SIZE, + }, + }); + + expect(wrapper.find(GlDropdown).find('button').text()).toMatchInterpolatedText( + `${CURRENT_PAGE_SIZE} items per page`, + ); + }); + + it('renders current page information', () => { + createComponent(); + + expect(wrapper.find('[data-testid="information"]').text()).toMatchInterpolatedText( + 'Showing 41 - 50 of 50', + ); + }); + + it('renders current page information when total count is over 1000', () => { + createComponent({ + pageInfo: { + ...DEFAULT_PROPS.pageInfo, + total: 1200, + page: 2, + }, + }); + + expect(wrapper.find('[data-testid="information"]').text()).toMatchInterpolatedText( + 'Showing 21 - 40 of 1000+', + ); + }); +}); diff --git a/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js b/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js index 7fdacbe83a2..5afa017aa76 100644 --- a/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js +++ b/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js @@ -1,13 +1,12 @@ -import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; import mockProjects from 'test_fixtures_static/projects.json'; import { trimText } from 'helpers/text_helper'; import ProjectAvatar from '~/vue_shared/components/deprecated_project_avatar/default.vue'; import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue'; -const localVue = createLocalVue(); - describe('ProjectListItem component', () => { - const Component = localVue.extend(ProjectListItem); + const Component = Vue.extend(ProjectListItem); let wrapper; let vm; let options; @@ -20,7 +19,6 @@ describe('ProjectListItem component', () => { project, selected: false, }, - localVue, }; }); 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 de5cee846a1..34cee10392d 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,5 +1,5 @@ import { GlSearchBoxByType, GlInfiniteScroll } from '@gitlab/ui'; -import { mount, createLocalVue } from '@vue/test-utils'; +import { mount } from '@vue/test-utils'; import { head } from 'lodash'; import Vue from 'vue'; import mockProjects from 'test_fixtures_static/projects.json'; @@ -7,8 +7,6 @@ import { trimText } from 'helpers/text_helper'; import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue'; import ProjectSelector from '~/vue_shared/components/project_selector/project_selector.vue'; -const localVue = createLocalVue(); - describe('ProjectSelector component', () => { let wrapper; let vm; @@ -28,7 +26,6 @@ describe('ProjectSelector component', () => { beforeEach(() => { wrapper = mount(Vue.extend(ProjectSelector), { - localVue, propsData: { projectSearchResults: searchResults, selectedProjects: selected, diff --git a/spec/frontend/vue_shared/components/registry/metadata_item_spec.js b/spec/frontend/vue_shared/components/registry/metadata_item_spec.js index 1ccf3ddc5a5..e4abdc15fd5 100644 --- a/spec/frontend/vue_shared/components/registry/metadata_item_spec.js +++ b/spec/frontend/vue_shared/components/registry/metadata_item_spec.js @@ -2,7 +2,7 @@ import { GlIcon, GlLink } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import component from '~/vue_shared/components/registry/metadata_item.vue'; -import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; +import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue'; describe('Metadata Item', () => { let wrapper; 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 8536ffed573..e74a867ec97 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 @@ -1,7 +1,7 @@ import { GlAlert, GlModal, GlButton, GlLoadingIcon, GlSkeletonLoader } from '@gitlab/ui'; import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; -import { shallowMount, createLocalVue } from '@vue/test-utils'; -import { nextTick } from 'vue'; +import { shallowMount } from '@vue/test-utils'; +import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; @@ -16,8 +16,7 @@ import { mockGraphqlInstructionsWindows, } from './mock_data'; -const localVue = createLocalVue(); -localVue.use(VueApollo); +Vue.use(VueApollo); let resizeCallback; const MockResizeObserver = { @@ -33,7 +32,7 @@ const MockResizeObserver = { }, }; -localVue.directive('gl-resize-observer', MockResizeObserver); +Vue.directive('gl-resize-observer', MockResizeObserver); jest.mock('@gitlab/ui/dist/utils'); @@ -67,7 +66,6 @@ describe('RunnerInstructionsModal component', () => { registrationToken: 'MY_TOKEN', ...props, }, - localVue, apolloProvider: fakeApollo, ...options, }), diff --git a/spec/frontend/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js b/spec/frontend/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js deleted file mode 100644 index e72b3bf45c4..00000000000 --- a/spec/frontend/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js +++ /dev/null @@ -1,103 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; - -import CollapsedGroupedDatePicker from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue'; -import CollapsedCalendarIcon from '~/vue_shared/components/sidebar/collapsed_calendar_icon.vue'; - -describe('CollapsedGroupedDatePicker', () => { - let wrapper; - - const defaultProps = { - showToggleSidebar: true, - }; - - const minDate = new Date('07/17/2016'); - const maxDate = new Date('07/17/2017'); - - const createComponent = ({ props = {} } = {}) => { - wrapper = shallowMount(CollapsedGroupedDatePicker, { - propsData: { ...defaultProps, ...props }, - }); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - const findCollapsedCalendarIcon = () => wrapper.findComponent(CollapsedCalendarIcon); - const findAllCollapsedCalendarIcons = () => wrapper.findAllComponents(CollapsedCalendarIcon); - - describe('toggleCollapse events', () => { - it('should emit when collapsed-calendar-icon is clicked', () => { - createComponent(); - - findCollapsedCalendarIcon().trigger('click'); - - expect(wrapper.emitted('toggleCollapse')[0]).toBeDefined(); - }); - }); - - describe('minDate and maxDate', () => { - it('should render both collapsed-calendar-icon', () => { - createComponent({ - props: { - minDate, - maxDate, - }, - }); - - const icons = findAllCollapsedCalendarIcons(); - - expect(icons.length).toBe(2); - expect(icons.at(0).text()).toBe('Jul 17 2016'); - expect(icons.at(1).text()).toBe('Jul 17 2017'); - }); - }); - - describe('minDate', () => { - it('should render minDate in collapsed-calendar-icon', () => { - createComponent({ - props: { - minDate, - }, - }); - - const icons = findAllCollapsedCalendarIcons(); - - expect(icons.length).toBe(1); - expect(icons.at(0).text()).toBe('From Jul 17 2016'); - }); - }); - - describe('maxDate', () => { - it('should render maxDate in collapsed-calendar-icon', () => { - createComponent({ - props: { - maxDate, - }, - }); - const icons = findAllCollapsedCalendarIcons(); - - expect(icons.length).toBe(1); - expect(icons.at(0).text()).toBe('Until Jul 17 2017'); - }); - }); - - describe('no dates', () => { - beforeEach(() => { - createComponent(); - }); - - it('should render None', () => { - const icons = findAllCollapsedCalendarIcons(); - - expect(icons.length).toBe(1); - expect(icons.at(0).text()).toBe('None'); - }); - - it('should have tooltip as `Start and due date`', () => { - const icons = findAllCollapsedCalendarIcons(); - - expect(icons.at(0).props('tooltipText')).toBe('Start and due date'); - }); - }); -}); 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 59b170bfba9..c4ed975e746 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,5 +1,6 @@ import { GlIcon, GlButton } from '@gitlab/ui'; -import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; import Vuex from 'vuex'; import DropdownButton from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue'; @@ -9,8 +10,7 @@ import labelSelectModule from '~/vue_shared/components/sidebar/labels_select_vue import { mockConfig } from './mock_data'; let store; -const localVue = createLocalVue(); -localVue.use(Vuex); +Vue.use(Vuex); const createComponent = (initialState = mockConfig) => { store = new Vuex.Store(labelSelectModule()); @@ -18,7 +18,6 @@ const createComponent = (initialState = mockConfig) => { store.dispatch('setInitialState', initialState); return shallowMount(DropdownButton, { - localVue, store, }); }; 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 c4a645082e6..1fe85637a62 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,5 +1,6 @@ import { GlButton, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui'; -import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; import Vuex from 'vuex'; import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue'; @@ -8,8 +9,7 @@ import labelSelectModule from '~/vue_shared/components/sidebar/labels_select_vue import { mockConfig, mockSuggestedColors } from './mock_data'; -const localVue = createLocalVue(); -localVue.use(Vuex); +Vue.use(Vuex); const createComponent = (initialState = mockConfig) => { const store = new Vuex.Store(labelSelectModule()); @@ -17,7 +17,6 @@ const createComponent = (initialState = mockConfig) => { store.dispatch('setInitialState', initialState); return shallowMount(DropdownContentsCreateView, { - localVue, store, }); }; 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 e39e8794fdd..80b8edd28ba 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 @@ -5,7 +5,8 @@ import { GlSearchBoxByType, GlLink, } from '@gitlab/ui'; -import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { shallowMount } from '@vue/test-utils'; +import Vue 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'; @@ -18,8 +19,7 @@ import defaultState from '~/vue_shared/components/sidebar/labels_select_vue/stor import { mockConfig, mockLabels, mockRegularLabel } from './mock_data'; -const localVue = createLocalVue(); -localVue.use(Vuex); +Vue.use(Vuex); describe('DropdownContentsLabelsView', () => { let wrapper; @@ -43,7 +43,6 @@ describe('DropdownContentsLabelsView', () => { store.dispatch('receiveLabelsSuccess', mockLabels); wrapper = shallowMount(DropdownContentsLabelsView, { - localVue, store, }); }; diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_spec.js index 88557917cb5..9781d9c4de0 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_spec.js @@ -1,4 +1,5 @@ -import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; import Vuex from 'vuex'; import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants'; @@ -7,8 +8,7 @@ import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vu import { mockConfig } from './mock_data'; -const localVue = createLocalVue(); -localVue.use(Vuex); +Vue.use(Vuex); const createComponent = (initialState = mockConfig, propsData = {}) => { const store = new Vuex.Store(labelsSelectModule()); @@ -17,7 +17,6 @@ const createComponent = (initialState = mockConfig, propsData = {}) => { return shallowMount(DropdownContents, { propsData, - localVue, store, }); }; 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 726a113dbd9..110c1d1b7eb 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,5 +1,6 @@ import { GlButton, GlLoadingIcon } from '@gitlab/ui'; -import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; import Vuex from 'vuex'; import DropdownTitle from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue'; @@ -8,8 +9,7 @@ import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vu import { mockConfig } from './mock_data'; -const localVue = createLocalVue(); -localVue.use(Vuex); +Vue.use(Vuex); const createComponent = (initialState = mockConfig) => { const store = new Vuex.Store(labelsSelectModule()); @@ -17,7 +17,6 @@ const createComponent = (initialState = mockConfig) => { store.dispatch('setInitialState', initialState); return shallowMount(DropdownTitle, { - localVue, store, propsData: { labelsSelectInProgress: false, diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_spec.js index 960ea77cb6e..f3c4839002b 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_spec.js @@ -1,5 +1,6 @@ import { GlLabel } from '@gitlab/ui'; -import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; import Vuex from 'vuex'; import DropdownValue from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue'; @@ -8,8 +9,7 @@ import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vu import { mockConfig, mockLabels, mockRegularLabel, mockScopedLabel } from './mock_data'; -const localVue = createLocalVue(); -localVue.use(Vuex); +Vue.use(Vuex); describe('DropdownValue', () => { let wrapper; @@ -23,7 +23,6 @@ describe('DropdownValue', () => { store.dispatch('setInitialState', { ...mockConfig, ...initialState }); wrapper = shallowMount(DropdownValue, { - localVue, store, slots, }); 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 bc1ec8b812b..4b0ba075eda 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,4 +1,5 @@ -import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; import Vuex from 'vuex'; import { isInViewport } from '~/lib/utils/common_utils'; @@ -18,8 +19,7 @@ jest.mock('~/lib/utils/common_utils', () => ({ isInViewport: jest.fn().mockReturnValue(true), })); -const localVue = createLocalVue(); -localVue.use(Vuex); +Vue.use(Vuex); describe('LabelsSelectRoot', () => { let wrapper; @@ -27,7 +27,6 @@ describe('LabelsSelectRoot', () => { const createComponent = (config = mockConfig, slots = {}) => { wrapper = shallowMount(LabelsSelectRoot, { - localVue, slots, store, propsData: config, diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js index 1faa3b0af1d..884bc4684ba 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js @@ -75,7 +75,7 @@ export const mockSuggestedColors = { '#013220': 'Dark green', '#6699cc': 'Blue-gray', '#0000ff': 'Blue', - '#e6e6fa': 'Lavendar', + '#e6e6fa': 'Lavender', '#9400d3': 'Dark violet', '#330066': 'Deep violet', '#808080': 'Gray', diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js index bf873f9162b..d8491334b5d 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js @@ -1,6 +1,6 @@ import { GlLoadingIcon, GlLink } from '@gitlab/ui'; -import { shallowMount, createLocalVue } from '@vue/test-utils'; -import { nextTick } from 'vue'; +import { shallowMount } from '@vue/test-utils'; +import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; @@ -18,8 +18,7 @@ jest.mock('~/flash'); const colors = Object.keys(mockSuggestedColors); -const localVue = createLocalVue(); -localVue.use(VueApollo); +Vue.use(VueApollo); const userRecoverableError = { ...createLabelSuccessfulResponse, @@ -63,7 +62,6 @@ describe('DropdownContentsCreateView', () => { }); wrapper = shallowMount(DropdownContentsCreateView, { - localVue, apolloProvider: mockApollo, propsData: { fullPath: '', diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js index 2980409fdce..6f5a4b7e613 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js @@ -4,8 +4,8 @@ import { GlDropdownItem, GlIntersectionObserver, } from '@gitlab/ui'; -import { shallowMount, createLocalVue } from '@vue/test-utils'; -import { nextTick } from 'vue'; +import { shallowMount } from '@vue/test-utils'; +import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; @@ -19,8 +19,7 @@ import { mockConfig, workspaceLabelsQueryResponse } from './mock_data'; jest.mock('~/flash'); -const localVue = createLocalVue(); -localVue.use(VueApollo); +Vue.use(VueApollo); const localSelectedLabels = [ { @@ -47,7 +46,6 @@ describe('DropdownContentsLabelsView', () => { const mockApollo = createMockApollo([[projectLabelsQuery, queryHandler]]); wrapper = shallowMount(DropdownContentsLabelsView, { - localVue, apolloProvider: mockApollo, provide: { variant: DropdownVariant.Sidebar, diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js index 8bcef347c96..00da9b74957 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js @@ -4,12 +4,12 @@ import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_w import DropdownContents from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue'; import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue'; import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue'; -import DropdownHeader from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue'; import DropdownFooter from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue'; import { mockLabels } from './mock_data'; const showDropdown = jest.fn(); +const focusInput = jest.fn(); const GlDropdownStub = { template: ` @@ -25,6 +25,15 @@ const GlDropdownStub = { }, }; +const DropdownHeaderStub = { + template: ` +
Hello, I am a header
+ `, + methods: { + focusInput, + }, +}; + describe('DropdownContent', () => { let wrapper; @@ -52,6 +61,7 @@ describe('DropdownContent', () => { }, stubs: { GlDropdown: GlDropdownStub, + DropdownHeader: DropdownHeaderStub, }, }); }; @@ -62,7 +72,7 @@ describe('DropdownContent', () => { const findCreateView = () => wrapper.findComponent(DropdownContentsCreateView); const findLabelsView = () => wrapper.findComponent(DropdownContentsLabelsView); - const findDropdownHeader = () => wrapper.findComponent(DropdownHeader); + const findDropdownHeader = () => wrapper.findComponent(DropdownHeaderStub); const findDropdownFooter = () => wrapper.findComponent(DropdownFooter); const findDropdown = () => wrapper.findComponent(GlDropdownStub); @@ -114,19 +124,7 @@ describe('DropdownContent', () => { expect(wrapper.emitted('setLabels')).toEqual([[[updatedLabel]]]); }); - it('does not render header on standalone variant', () => { - createComponent({ props: { variant: DropdownVariant.Standalone } }); - - expect(findDropdownHeader().exists()).toBe(false); - }); - - it('renders header on embedded variant', () => { - createComponent({ props: { variant: DropdownVariant.Embedded } }); - - expect(findDropdownHeader().exists()).toBe(true); - }); - - it('renders header on sidebar variant', () => { + it('renders header', () => { createComponent(); expect(findDropdownHeader().exists()).toBe(true); @@ -135,11 +133,20 @@ describe('DropdownContent', () => { it('sets searchKey for labels view on input event from header', async () => { createComponent(); - expect(wrapper.vm.searchKey).toEqual(''); + expect(findLabelsView().props('searchKey')).toBe(''); findDropdownHeader().vm.$emit('input', '123'); await nextTick(); - expect(findLabelsView().props('searchKey')).toEqual('123'); + expect(findLabelsView().props('searchKey')).toBe('123'); + }); + + it('clears and focuses search input on selecting a label', () => { + createComponent(); + findDropdownHeader().vm.$emit('input', '123'); + findLabelsView().vm.$emit('input', []); + + expect(findLabelsView().props('searchKey')).toBe(''); + expect(focusInput).toHaveBeenCalled(); }); describe('Create view', () => { diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_header_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_header_spec.js index 592559ef305..c4faef8ccdd 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_header_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_header_spec.js @@ -9,6 +9,7 @@ describe('DropdownHeader', () => { const createComponent = ({ showDropdownContentsCreateView = false, labelsFetchInProgress = false, + isStandalone = false, } = {}) => { wrapper = extendedWrapper( shallowMount(DropdownHeader, { @@ -18,6 +19,7 @@ describe('DropdownHeader', () => { labelsCreateTitle: 'Create label', labelsListTitle: 'Select label', searchKey: '', + isStandalone, }, stubs: { GlSearchBoxByType, @@ -32,6 +34,7 @@ describe('DropdownHeader', () => { const findSearchInput = () => wrapper.findComponent(GlSearchBoxByType); const findGoBackButton = () => wrapper.findByTestId('go-back-button'); + const findDropdownTitle = () => wrapper.findByTestId('dropdown-header-title'); beforeEach(() => { createComponent(); @@ -72,4 +75,18 @@ describe('DropdownHeader', () => { }, ); }); + + describe('Standalone variant', () => { + beforeEach(() => { + createComponent({ isStandalone: true }); + }); + + it('renders search input', () => { + expect(findSearchInput().exists()).toBe(true); + }); + + it('does not render title', async () => { + expect(findDropdownTitle().exists()).toBe(false); + }); + }); }); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_value_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_value_spec.js index e7e78cd7a33..0c4f4b7d504 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_value_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_value_spec.js @@ -95,5 +95,10 @@ describe('DropdownValue', () => { findRegularLabel().vm.$emit('close'); expect(wrapper.emitted('onLabelRemove')).toEqual([[mockRegularLabel.id]]); }); + + it('emits `onCollapsedValueClick` when clicking on collapsed value', () => { + wrapper.find('.sidebar-collapsed-icon').trigger('click'); + expect(wrapper.emitted('onCollapsedValueClick')).toEqual([[]]); + }); }); }); 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 d4203528874..a4199bb3e27 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 @@ -1,25 +1,34 @@ -import { shallowMount, createLocalVue } from '@vue/test-utils'; -import { nextTick } from 'vue'; +import { shallowMount } from '@vue/test-utils'; +import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import createFlash from '~/flash'; -import { IssuableType } from '~/issue_show/constants'; +import { IssuableType } from '~/issues/constants'; import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; import DropdownContents from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue'; import DropdownValue from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue'; import issueLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql'; +import updateIssueLabelsMutation from '~/boards/graphql/issue_set_labels.mutation.graphql'; +import updateMergeRequestLabelsMutation from '~/sidebar/queries/update_merge_request_labels.mutation.graphql'; +import updateEpicLabelsMutation from '~/vue_shared/components/sidebar/labels_select_widget/graphql/epic_update_labels.mutation.graphql'; import LabelsSelectRoot from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue'; -import { mockConfig, issuableLabelsQueryResponse } from './mock_data'; +import { mockConfig, issuableLabelsQueryResponse, updateLabelsMutationResponse } from './mock_data'; jest.mock('~/flash'); -const localVue = createLocalVue(); -localVue.use(VueApollo); +Vue.use(VueApollo); const successfulQueryHandler = jest.fn().mockResolvedValue(issuableLabelsQueryResponse); +const successfulMutationHandler = jest.fn().mockResolvedValue(updateLabelsMutationResponse); const errorQueryHandler = jest.fn().mockRejectedValue('Houston, we have a problem'); +const updateLabelsMutation = { + [IssuableType.Issue]: updateIssueLabelsMutation, + [IssuableType.MergeRequest]: updateMergeRequestLabelsMutation, + [IssuableType.Epic]: updateEpicLabelsMutation, +}; + describe('LabelsSelectRoot', () => { let wrapper; @@ -30,17 +39,21 @@ describe('LabelsSelectRoot', () => { const createComponent = ({ config = mockConfig, slots = {}, + issuableType = IssuableType.Issue, queryHandler = successfulQueryHandler, + mutationHandler = successfulMutationHandler, } = {}) => { - const mockApollo = createMockApollo([[issueLabelsQuery, queryHandler]]); + const mockApollo = createMockApollo([ + [issueLabelsQuery, queryHandler], + [updateLabelsMutation[issuableType], mutationHandler], + ]); wrapper = shallowMount(LabelsSelectRoot, { slots, apolloProvider: mockApollo, - localVue, propsData: { ...config, - issuableType: IssuableType.Issue, + issuableType, labelCreateType: 'project', workspaceType: 'project', }, @@ -60,9 +73,9 @@ describe('LabelsSelectRoot', () => { wrapper.destroy(); }); - it('renders component with classes `labels-select-wrapper position-relative`', () => { + it('renders component with classes `labels-select-wrapper gl-relative`', () => { createComponent(); - expect(wrapper.classes()).toEqual(['labels-select-wrapper', 'position-relative']); + expect(wrapper.classes()).toEqual(['labels-select-wrapper', 'gl-relative']); }); it.each` @@ -130,4 +143,46 @@ describe('LabelsSelectRoot', () => { findDropdownContents().vm.$emit('setLabels', [label]); expect(wrapper.emitted('updateSelectedLabels')).toEqual([[{ labels: [label] }]]); }); + + describe.each` + issuableType + ${IssuableType.Issue} + ${IssuableType.MergeRequest} + ${IssuableType.Epic} + `('when updating labels for $issuableType', ({ issuableType }) => { + const label = { id: 'gid://gitlab/ProjectLabel/2' }; + + it('sets the loading state', async () => { + createComponent({ issuableType }); + await nextTick(); + findDropdownContents().vm.$emit('setLabels', [label]); + await nextTick(); + + expect(findSidebarEditableItem().props('loading')).toBe(true); + }); + + it('updates labels correctly after successful mutation', async () => { + createComponent({ issuableType }); + await nextTick(); + findDropdownContents().vm.$emit('setLabels', [label]); + await waitForPromises(); + + expect(findDropdownValue().props('selectedLabels')).toEqual( + updateLabelsMutationResponse.data.updateIssuableLabels.issuable.labels.nodes, + ); + }); + + it('displays an error if mutation was rejected', async () => { + createComponent({ issuableType, mutationHandler: errorQueryHandler }); + await nextTick(); + findDropdownContents().vm.$emit('setLabels', [label]); + await waitForPromises(); + + expect(createFlash).toHaveBeenCalledWith({ + captureError: true, + error: expect.anything(), + message: 'An error occurred while updating labels.', + }); + }); + }); }); 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 5c5bf5f2187..6ef54ce37ce 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 @@ -118,7 +118,9 @@ export const workspaceLabelsQueryResponse = { export const issuableLabelsQueryResponse = { data: { workspace: { + id: 'workspace-1', issuable: { + __typename: 'Issue', id: '1', labels: { nodes: [ @@ -135,3 +137,18 @@ export const issuableLabelsQueryResponse = { }, }, }; + +export const updateLabelsMutationResponse = { + data: { + updateIssuableLabels: { + errors: [], + issuable: { + __typename: 'Issue', + id: '1', + labels: { + nodes: [], + }, + }, + }, + }, +}; diff --git a/spec/frontend/vue_shared/components/source_viewer_spec.js b/spec/frontend/vue_shared/components/source_viewer_spec.js new file mode 100644 index 00000000000..758068379de --- /dev/null +++ b/spec/frontend/vue_shared/components/source_viewer_spec.js @@ -0,0 +1,59 @@ +import hljs from 'highlight.js/lib/core'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import SourceViewer from '~/vue_shared/components/source_viewer.vue'; +import LineNumbers from '~/vue_shared/components/line_numbers.vue'; +import waitForPromises from 'helpers/wait_for_promises'; + +jest.mock('highlight.js/lib/core'); + +describe('Source Viewer component', () => { + let wrapper; + const content = `// Some source code`; + const highlightedContent = `${content}`; + const language = 'javascript'; + + hljs.highlight.mockImplementation(() => ({ value: highlightedContent })); + hljs.highlightAuto.mockImplementation(() => ({ value: highlightedContent })); + + const createComponent = async (props = {}) => { + wrapper = shallowMountExtended(SourceViewer, { propsData: { content, language, ...props } }); + await waitForPromises(); + }; + + const findLineNumbers = () => wrapper.findComponent(LineNumbers); + const findHighlightedContent = () => wrapper.findByTestId('test-highlighted'); + + beforeEach(() => createComponent()); + + afterEach(() => wrapper.destroy()); + + describe('highlight.js', () => { + it('registers the language definition', async () => { + const languageDefinition = await import(`highlight.js/lib/languages/${language}`); + + expect(hljs.registerLanguage).toHaveBeenCalledWith(language, languageDefinition.default); + }); + + it('highlights the content', () => { + expect(hljs.highlight).toHaveBeenCalledWith(content, { language }); + }); + + describe('auto-detect enabled', () => { + beforeEach(() => createComponent({ autoDetect: true })); + + it('highlights the content with auto-detection', () => { + expect(hljs.highlightAuto).toHaveBeenCalledWith(content); + }); + }); + }); + + describe('rendering', () => { + it('renders Line Numbers', () => { + expect(findLineNumbers().props('lines')).toBe(1); + }); + + it('renders the highlighted content', () => { + expect(findHighlightedContent().exists()).toBe(true); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/storage_counter/usage_graph_spec.js b/spec/frontend/vue_shared/components/storage_counter/usage_graph_spec.js deleted file mode 100644 index 103eee4b9a8..00000000000 --- a/spec/frontend/vue_shared/components/storage_counter/usage_graph_spec.js +++ /dev/null @@ -1,137 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import { numberToHumanSize } from '~/lib/utils/number_utils'; -import UsageGraph from '~/vue_shared/components/storage_counter/usage_graph.vue'; - -let data; -let wrapper; - -function mountComponent({ rootStorageStatistics, limit }) { - wrapper = shallowMount(UsageGraph, { - propsData: { - rootStorageStatistics, - limit, - }, - }); -} -function findStorageTypeUsagesSerialized() { - return wrapper - .findAll('[data-testid="storage-type-usage"]') - .wrappers.map((wp) => wp.element.style.flex); -} - -describe('Storage Counter usage graph component', () => { - beforeEach(() => { - data = { - rootStorageStatistics: { - wikiSize: 5000, - repositorySize: 4000, - packagesSize: 3000, - lfsObjectsSize: 2000, - buildArtifactsSize: 500, - pipelineArtifactsSize: 500, - snippetsSize: 2000, - storageSize: 17000, - uploadsSize: 1000, - }, - limit: 2000, - }; - mountComponent(data); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('renders the legend in order', () => { - const types = wrapper.findAll('[data-testid="storage-type-legend"]'); - - const { - buildArtifactsSize, - pipelineArtifactsSize, - lfsObjectsSize, - packagesSize, - repositorySize, - wikiSize, - snippetsSize, - uploadsSize, - } = data.rootStorageStatistics; - - expect(types.at(0).text()).toMatchInterpolatedText(`Wikis ${numberToHumanSize(wikiSize)}`); - expect(types.at(1).text()).toMatchInterpolatedText( - `Repositories ${numberToHumanSize(repositorySize)}`, - ); - expect(types.at(2).text()).toMatchInterpolatedText( - `Packages ${numberToHumanSize(packagesSize)}`, - ); - expect(types.at(3).text()).toMatchInterpolatedText( - `LFS Objects ${numberToHumanSize(lfsObjectsSize)}`, - ); - expect(types.at(4).text()).toMatchInterpolatedText( - `Snippets ${numberToHumanSize(snippetsSize)}`, - ); - expect(types.at(5).text()).toMatchInterpolatedText( - `Artifacts ${numberToHumanSize(buildArtifactsSize + pipelineArtifactsSize)}`, - ); - expect(types.at(6).text()).toMatchInterpolatedText(`Uploads ${numberToHumanSize(uploadsSize)}`); - }); - - describe('when storage type is not used', () => { - beforeEach(() => { - data.rootStorageStatistics.wikiSize = 0; - mountComponent(data); - }); - - it('filters the storage type', () => { - expect(wrapper.text()).not.toContain('Wikis'); - }); - }); - - describe('when there is no storage usage', () => { - beforeEach(() => { - data.rootStorageStatistics.storageSize = 0; - mountComponent(data); - }); - - it('it does not render', () => { - expect(wrapper.html()).toEqual(''); - }); - }); - - describe('when limit is 0', () => { - beforeEach(() => { - data.limit = 0; - mountComponent(data); - }); - - it('sets correct flex values', () => { - expect(findStorageTypeUsagesSerialized()).toStrictEqual([ - '0.29411764705882354', - '0.23529411764705882', - '0.17647058823529413', - '0.11764705882352941', - '0.11764705882352941', - '0.058823529411764705', - '0.058823529411764705', - ]); - }); - }); - - describe('when storage exceeds limit', () => { - beforeEach(() => { - data.limit = data.rootStorageStatistics.storageSize - 1; - mountComponent(data); - }); - - it('it does render correclty', () => { - expect(findStorageTypeUsagesSerialized()).toStrictEqual([ - '0.29411764705882354', - '0.23529411764705882', - '0.17647058823529413', - '0.11764705882352941', - '0.11764705882352941', - '0.058823529411764705', - '0.058823529411764705', - ]); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js b/spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js index 380b7231acd..9e7e5c1263f 100644 --- a/spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js +++ b/spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js @@ -1,25 +1,20 @@ import { mount, shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import { hasHorizontalOverflow } from '~/lib/utils/dom_utils'; -import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; +import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; -const DUMMY_TEXT = 'lorem-ipsum-dolar-sit-amit-consectur-adipiscing-elit-sed-do'; +const MOCK_TITLE = 'lorem-ipsum-dolar-sit-amit-consectur-adipiscing-elit-sed-do'; +const SHORT_TITLE = 'my-text'; -const createChildElement = () => `${DUMMY_TEXT}`; +const createChildElement = () => `${MOCK_TITLE}`; jest.mock('~/lib/utils/dom_utils', () => ({ - hasHorizontalOverflow: jest.fn(() => { + ...jest.requireActual('~/lib/utils/dom_utils'), + hasHorizontalOverflow: jest.fn().mockImplementation(() => { throw new Error('this needs to be mocked'); }), })); -jest.mock('@gitlab/ui', () => ({ - GlTooltipDirective: { - bind(el, binding) { - el.classList.add('gl-tooltip'); - el.setAttribute('data-original-title', el.title); - el.dataset.placement = binding.value.placement; - }, - }, -})); describe('TooltipOnTruncate component', () => { let wrapper; @@ -27,15 +22,31 @@ describe('TooltipOnTruncate component', () => { const createComponent = ({ propsData, ...options } = {}) => { wrapper = shallowMount(TooltipOnTruncate, { - attachTo: document.body, propsData: { + title: MOCK_TITLE, ...propsData, }, + slots: { + default: [MOCK_TITLE], + }, + directives: { + GlTooltip: createMockDirective(), + GlResizeObserver: createMockDirective(), + }, ...options, }); }; const createWrappedComponent = ({ propsData, ...options }) => { + const WrappedTooltipOnTruncate = { + ...TooltipOnTruncate, + directives: { + ...TooltipOnTruncate.directives, + GlTooltip: createMockDirective(), + GlResizeObserver: createMockDirective(), + }, + }; + // set a parent around the tested component parent = mount( { @@ -43,74 +54,85 @@ describe('TooltipOnTruncate component', () => { title: { default: '' }, }, template: ` - -
{{title}}
-
+ +
{{title}}
+
`, components: { - TooltipOnTruncate, + TooltipOnTruncate: WrappedTooltipOnTruncate, }, }, { propsData: { ...propsData }, - attachTo: document.body, ...options, }, ); - wrapper = parent.find(TooltipOnTruncate); + wrapper = parent.find(WrappedTooltipOnTruncate); }; - const hasTooltip = () => wrapper.classes('gl-tooltip'); + const getTooltipValue = () => getBinding(wrapper.element, 'gl-tooltip')?.value; + const resize = async ({ truncate }) => { + hasHorizontalOverflow.mockReturnValueOnce(truncate); + getBinding(wrapper.element, 'gl-resize-observer').value(); + await nextTick(); + }; afterEach(() => { wrapper.destroy(); }); - describe('with default target', () => { - it('renders tooltip if truncated', () => { + describe('when truncated', () => { + beforeEach(async () => { hasHorizontalOverflow.mockReturnValueOnce(true); - createComponent({ - propsData: { - title: DUMMY_TEXT, - }, - slots: { - default: [DUMMY_TEXT], - }, - }); + createComponent(); + }); - return wrapper.vm.$nextTick().then(() => { - expect(hasHorizontalOverflow).toHaveBeenCalledWith(wrapper.element); - expect(hasTooltip()).toBe(true); - expect(wrapper.attributes('data-original-title')).toEqual(DUMMY_TEXT); - expect(wrapper.attributes('data-placement')).toEqual('top'); + it('renders tooltip', async () => { + expect(hasHorizontalOverflow).toHaveBeenLastCalledWith(wrapper.element); + expect(getTooltipValue()).toMatchObject({ + title: MOCK_TITLE, + placement: 'top', + disabled: false, }); + expect(wrapper.classes('js-show-tooltip')).toBe(true); }); + }); - it('does not render tooltip if normal', () => { + describe('with default target', () => { + beforeEach(async () => { hasHorizontalOverflow.mockReturnValueOnce(false); - createComponent({ - propsData: { - title: DUMMY_TEXT, - }, - slots: { - default: [DUMMY_TEXT], - }, + createComponent(); + }); + + it('does not render tooltip if not truncated', () => { + expect(hasHorizontalOverflow).toHaveBeenLastCalledWith(wrapper.element); + expect(getTooltipValue()).toMatchObject({ + disabled: true, }); + expect(wrapper.classes('js-show-tooltip')).toBe(false); + }); - return wrapper.vm.$nextTick().then(() => { - expect(hasHorizontalOverflow).toHaveBeenCalledWith(wrapper.element); - expect(hasTooltip()).toBe(false); + it('renders tooltip on resize', async () => { + await resize({ truncate: true }); + + expect(getTooltipValue()).toMatchObject({ + disabled: false, + }); + + await resize({ truncate: false }); + + expect(getTooltipValue()).toMatchObject({ + disabled: true, }); }); }); describe('with child target', () => { - it('renders tooltip if truncated', () => { + it('renders tooltip if truncated', async () => { hasHorizontalOverflow.mockReturnValueOnce(true); createComponent({ propsData: { - title: DUMMY_TEXT, truncateTarget: 'child', }, slots: { @@ -118,13 +140,18 @@ describe('TooltipOnTruncate component', () => { }, }); - return wrapper.vm.$nextTick().then(() => { - expect(hasHorizontalOverflow).toHaveBeenCalledWith(wrapper.element.childNodes[0]); - expect(hasTooltip()).toBe(true); + expect(hasHorizontalOverflow).toHaveBeenLastCalledWith(wrapper.element.childNodes[0]); + + await nextTick(); + + expect(getTooltipValue()).toMatchObject({ + title: MOCK_TITLE, + placement: 'top', + disabled: false, }); }); - it('does not render tooltip if normal', () => { + it('does not render tooltip if normal', async () => { hasHorizontalOverflow.mockReturnValueOnce(false); createComponent({ propsData: { @@ -135,19 +162,21 @@ describe('TooltipOnTruncate component', () => { }, }); - return wrapper.vm.$nextTick().then(() => { - expect(hasHorizontalOverflow).toHaveBeenCalledWith(wrapper.element.childNodes[0]); - expect(hasTooltip()).toBe(false); + expect(hasHorizontalOverflow).toHaveBeenLastCalledWith(wrapper.element.childNodes[0]); + + await nextTick(); + + expect(getTooltipValue()).toMatchObject({ + disabled: true, }); }); }); describe('with fn target', () => { - it('renders tooltip if truncated', () => { + it('renders tooltip if truncated', async () => { hasHorizontalOverflow.mockReturnValueOnce(true); createComponent({ propsData: { - title: DUMMY_TEXT, truncateTarget: (el) => el.childNodes[1], }, slots: { @@ -155,93 +184,97 @@ describe('TooltipOnTruncate component', () => { }, }); - return wrapper.vm.$nextTick().then(() => { - expect(hasHorizontalOverflow).toHaveBeenCalledWith(wrapper.element.childNodes[1]); - expect(hasTooltip()).toBe(true); + expect(hasHorizontalOverflow).toHaveBeenLastCalledWith(wrapper.element.childNodes[1]); + + await nextTick(); + + expect(getTooltipValue()).toMatchObject({ + disabled: false, }); }); }); describe('placement', () => { - it('sets data-placement when tooltip is rendered', () => { - const placement = 'bottom'; + it('sets placement when tooltip is rendered', () => { + const mockPlacement = 'bottom'; hasHorizontalOverflow.mockReturnValueOnce(true); createComponent({ propsData: { - placement, - }, - slots: { - default: DUMMY_TEXT, + placement: mockPlacement, }, }); - return wrapper.vm.$nextTick().then(() => { - expect(hasTooltip()).toBe(true); - expect(wrapper.attributes('data-placement')).toEqual(placement); + expect(hasHorizontalOverflow).toHaveBeenLastCalledWith(wrapper.element); + expect(getTooltipValue()).toMatchObject({ + placement: mockPlacement, }); }); }); describe('updates when title and slot content changes', () => { describe('is initialized with a long text', () => { - beforeEach(() => { + beforeEach(async () => { hasHorizontalOverflow.mockReturnValueOnce(true); createWrappedComponent({ - propsData: { title: DUMMY_TEXT }, + propsData: { title: MOCK_TITLE }, }); - return parent.vm.$nextTick(); + await nextTick(); }); it('renders tooltip', () => { - expect(hasTooltip()).toBe(true); - expect(wrapper.attributes('data-original-title')).toEqual(DUMMY_TEXT); - expect(wrapper.attributes('data-placement')).toEqual('top'); + expect(getTooltipValue()).toMatchObject({ + title: MOCK_TITLE, + placement: 'top', + disabled: false, + }); }); - it('does not render tooltip after updated to a short text', () => { + it('does not render tooltip after updated to a short text', async () => { hasHorizontalOverflow.mockReturnValueOnce(false); parent.setProps({ - title: 'new-text', + title: SHORT_TITLE, }); - return wrapper.vm - .$nextTick() - .then(() => wrapper.vm.$nextTick()) // wait 2 times to get an updated slot - .then(() => { - expect(hasTooltip()).toBe(false); - }); + await nextTick(); + await nextTick(); // wait 2 times to get an updated slot + + expect(getTooltipValue()).toMatchObject({ + title: SHORT_TITLE, + disabled: true, + }); }); }); - describe('is initialized with a short text', () => { - beforeEach(() => { + describe('is initialized with a short text that does not overflow', () => { + beforeEach(async () => { hasHorizontalOverflow.mockReturnValueOnce(false); createWrappedComponent({ - propsData: { title: DUMMY_TEXT }, + propsData: { title: MOCK_TITLE }, }); - return wrapper.vm.$nextTick(); + await nextTick(); }); it('does not render tooltip', () => { - expect(hasTooltip()).toBe(false); + expect(getTooltipValue()).toMatchObject({ + title: MOCK_TITLE, + disabled: true, + }); }); - it('renders tooltip after text is updated', () => { + it('renders tooltip after text is updated', async () => { hasHorizontalOverflow.mockReturnValueOnce(true); - const newText = 'new-text'; parent.setProps({ - title: newText, + title: SHORT_TITLE, }); - return wrapper.vm - .$nextTick() - .then(() => wrapper.vm.$nextTick()) // wait 2 times to get an updated slot - .then(() => { - expect(hasTooltip()).toBe(true); - expect(wrapper.attributes('data-original-title')).toEqual(newText); - expect(wrapper.attributes('data-placement')).toEqual('top'); - }); + await nextTick(); + await nextTick(); // wait 2 times to get an updated slot + + expect(getTooltipValue()).toMatchObject({ + title: SHORT_TITLE, + disabled: false, + }); }); }); }); diff --git a/spec/frontend/vue_shared/components/user_select_spec.js b/spec/frontend/vue_shared/components/user_select_spec.js index b777ac0a0a4..8994e16e517 100644 --- a/spec/frontend/vue_shared/components/user_select_spec.js +++ b/spec/frontend/vue_shared/components/user_select_spec.js @@ -1,7 +1,7 @@ import { GlSearchBoxByType, GlDropdown } from '@gitlab/ui'; -import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { shallowMount } from '@vue/test-utils'; import { cloneDeep } from 'lodash'; -import { nextTick } from 'vue'; +import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; @@ -33,8 +33,7 @@ const waitForSearch = async () => { await waitForPromises(); }; -const localVue = createLocalVue(); -localVue.use(VueApollo); +Vue.use(VueApollo); describe('User select dropdown', () => { let wrapper; @@ -62,7 +61,6 @@ describe('User select dropdown', () => { [getIssueParticipantsQuery, participantsQueryHandler], ]); wrapper = shallowMount(UserSelect, { - localVue, apolloProvider: fakeApollo, propsData: { headerText: 'test', diff --git a/spec/frontend/vue_shared/components/vuex_module_provider_spec.js b/spec/frontend/vue_shared/components/vuex_module_provider_spec.js index ebd396bd87c..c136c2054ac 100644 --- a/spec/frontend/vue_shared/components/vuex_module_provider_spec.js +++ b/spec/frontend/vue_shared/components/vuex_module_provider_spec.js @@ -1,4 +1,4 @@ -import { mount, createLocalVue } from '@vue/test-utils'; +import { mount } from '@vue/test-utils'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; import VuexModuleProvider from '~/vue_shared/components/vuex_module_provider.vue'; @@ -38,10 +38,9 @@ describe('~/vue_shared/components/vuex_module_provider', () => { it('does not blow up when used with vue-apollo', () => { // See https://github.com/vuejs/vue-apollo/pull/1153 for details - const localVue = createLocalVue(); - localVue.use(VueApollo); + Vue.use(VueApollo); - createComponent({ localVue }); + createComponent(); expect(findProvidedVuexModule()).toBe(TEST_VUEX_MODULE); }); }); -- cgit v1.2.1