summaryrefslogtreecommitdiff
path: root/spec/frontend/vue_shared/components
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-12-20 13:37:47 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-12-20 13:37:47 +0000
commitaee0a117a889461ce8ced6fcf73207fe017f1d99 (patch)
tree891d9ef189227a8445d83f35c1b0fc99573f4380 /spec/frontend/vue_shared/components
parent8d46af3258650d305f53b819eabf7ab18d22f59e (diff)
downloadgitlab-ce-aee0a117a889461ce8ced6fcf73207fe017f1d99.tar.gz
Add latest changes from gitlab-org/gitlab@14-6-stable-eev14.6.0-rc42
Diffstat (limited to 'spec/frontend/vue_shared/components')
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/source_editor_spec.js.snap1
-rw-r--r--spec/frontend/vue_shared/components/chronic_duration_input_spec.js390
-rw-r--r--spec/frontend/vue_shared/components/clipboard_button_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/confirm_modal_spec.js63
-rw-r--r--spec/frontend/vue_shared/components/delete_label_modal_spec.js64
-rw-r--r--spec/frontend/vue_shared/components/design_management/__snapshots__/design_note_pin_spec.js.snap55
-rw-r--r--spec/frontend/vue_shared/components/design_management/design_note_pin_spec.js42
-rw-r--r--spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/dismissible_alert_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/dom_element_listener_spec.js116
-rw-r--r--spec/frontend/vue_shared/components/file_icon_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js104
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js57
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/epic_token_spec.js169
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js116
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js13
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/weight_token_spec.js38
-rw-r--r--spec/frontend/vue_shared/components/form/__snapshots__/title_spec.js.snap1
-rw-r--r--spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js231
-rw-r--r--spec/frontend/vue_shared/components/gl_modal_vuex_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/header_ci_component_spec.js18
-rw-r--r--spec/frontend/vue_shared/components/issuable/issuable_header_warnings_spec.js89
-rw-r--r--spec/frontend/vue_shared/components/issue/issue_assignees_spec.js145
-rw-r--r--spec/frontend/vue_shared/components/issue/issue_milestone_spec.js160
-rw-r--r--spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js206
-rw-r--r--spec/frontend/vue_shared/components/issue/related_issuable_mock_data.js123
-rw-r--r--spec/frontend/vue_shared/components/line_numbers_spec.js71
-rw-r--r--spec/frontend/vue_shared/components/markdown/toolbar_spec.js39
-rw-r--r--spec/frontend/vue_shared/components/namespace_select/mock_data.js11
-rw-r--r--spec/frontend/vue_shared/components/namespace_select/namespace_select_spec.js86
-rw-r--r--spec/frontend/vue_shared/components/notes/placeholder_note_spec.js24
-rw-r--r--spec/frontend/vue_shared/components/pagination_bar/pagination_bar_spec.js93
-rw-r--r--spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/project_selector/project_selector_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/registry/metadata_item_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js103
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js41
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_header_spec.js17
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_value_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js77
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js17
-rw-r--r--spec/frontend/vue_shared/components/source_viewer_spec.js59
-rw-r--r--spec/frontend/vue_shared/components/storage_counter/usage_graph_spec.js137
-rw-r--r--spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js233
-rw-r--r--spec/frontend/vue_shared/components/user_select_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/vuex_module_provider_spec.js7
59 files changed, 1626 insertions, 1733 deletions
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`] = `
<div
data-editor-loading=""
+ data-qa-selector="source_editor_container"
id="source-editor-snippet_777"
>
<pre
diff --git a/spec/frontend/vue_shared/components/chronic_duration_input_spec.js b/spec/frontend/vue_shared/components/chronic_duration_input_spec.js
new file mode 100644
index 00000000000..530d01402c6
--- /dev/null
+++ b/spec/frontend/vue_shared/components/chronic_duration_input_spec.js
@@ -0,0 +1,390 @@
+import { mount } from '@vue/test-utils';
+import ChronicDurationInput from '~/vue_shared/components/chronic_duration_input.vue';
+
+const MOCK_VALUE = 2 * 3600 + 20 * 60;
+
+describe('vue_shared/components/chronic_duration_input', () => {
+ let wrapper;
+ let textElement;
+ let hiddenElement;
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ textElement = null;
+ hiddenElement = null;
+ });
+
+ const findComponents = () => {
+ textElement = wrapper.find('input[type=text]').element;
+ hiddenElement = wrapper.find('input[type=hidden]').element;
+ };
+
+ const createComponent = (props = {}) => {
+ if (wrapper) {
+ throw new Error('There should only be one wrapper created per test');
+ }
+
+ wrapper = mount(ChronicDurationInput, { propsData: props });
+ findComponents();
+ };
+
+ describe('value', () => {
+ it('has human-readable output with value', () => {
+ createComponent({ value: MOCK_VALUE });
+
+ expect(textElement.value).toBe('2 hrs 20 mins');
+ expect(hiddenElement.value).toBe(MOCK_VALUE.toString());
+ });
+
+ it('has empty output with no value', () => {
+ createComponent({ value: null });
+
+ expect(textElement.value).toBe('');
+ expect(hiddenElement.value).toBe('');
+ });
+ });
+
+ describe('change', () => {
+ const createAndDispatch = async (initialValue, humanReadableInput) => {
+ createComponent({ value: initialValue });
+ await 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: '<div><chronic-duration-input v-model="value"/></div>',
+ });
+ 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: `<form data-testid="myForm"><chronic-duration-input name="myInput" :value="${MOCK_VALUE}"/></form>`,
+ 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: '<p>Header</p><ul onhover="alert(1)"><li>First</li></ul>' }} | ${'<p>Header</p><ul><li>First</li></ul>'}
`('$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:
- '<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>',
- }),
- },
- }),
- );
- };
-
- 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`] = `
+<button
+ aria-label="Comment '1' position"
+ class="gl-display-flex gl-align-items-center gl-justify-content-center gl-font-sm js-image-badge design-note-pin gl-absolute"
+ style="left: 10px; top: 10px;"
+ type="button"
+>
+
+ 1
+
+</button>
+`;
+
+exports[`Design note pin component should match the snapshot of note without index 1`] = `
+<button
+ aria-label="Comment form position"
+ class="gl-display-flex gl-align-items-center gl-justify-content-center gl-font-sm btn-transparent comment-indicator gl-absolute"
+ style="left: 10px; top: 10px;"
+ type="button"
+>
+ <gl-icon-stub
+ name="image-comment-dark"
+ size="24"
+ />
+</button>
+`;
+
+exports[`Design note pin component should match the snapshot when pin is resolved 1`] = `
+<button
+ aria-label="Comment form position"
+ class="gl-display-flex gl-align-items-center gl-justify-content-center gl-font-sm btn-transparent comment-indicator resolved gl-absolute"
+ style="left: 10px; top: 10px;"
+ type="button"
+>
+ <gl-icon-stub
+ name="image-comment-dark"
+ size="24"
+ />
+</button>
+`;
+
+exports[`Design note pin component should match the snapshot when position is absent 1`] = `
+<button
+ aria-label="Comment form position"
+ class="gl-display-flex gl-align-items-center gl-justify-content-center gl-font-sm btn-transparent comment-indicator"
+ type="button"
+>
+ <gl-icon-stub
+ name="image-comment-dark"
+ size="24"
+ />
+</button>
+`;
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 = `
+<div>
+ <button class="js-test-include" data-testid="lorem">Lorem</button>
+ <button class="js-test-include" data-testid="ipsum">Ipsum</button>
+ <button data-testid="hello">Hello</a>
+</div>
+`;
+
+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: '<div></div>',
- 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)"
>
<gl-form-input-stub />
</gl-form-group-stub>
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: '<div class="js-due-date-slot"></div>',
- weight: '<div class="js-weight-slot"></div>',
- };
-
- 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('<span class="bold">Created</span>');
- expect(stateTitle).toContain(`<span class="text-tertiary">${formattedCreateDate}</span>`);
- });
-
- 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: `
+ <div>Hello, I am a header</div>
+ `,
+ 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 = `<span data-testid='test-highlighted'>${content}</span>`;
+ 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 = () => `<a href="#">${DUMMY_TEXT}</a>`;
+const createChildElement = () => `<a href="#">${MOCK_TITLE}</a>`;
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: `
- <TooltipOnTruncate :title="title" truncate-target="child">
- <div>{{title}}</div>
- </TooltipOnTruncate>
+ <TooltipOnTruncate :title="title" truncate-target="child">
+ <div>{{title}}</div>
+ </TooltipOnTruncate>
`,
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);
});
});