summaryrefslogtreecommitdiff
path: root/spec/frontend/vue_shared
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/vue_shared')
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap8
-rw-r--r--spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js11
-rw-r--r--spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/changed_file_icon_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/confirm_modal_spec.js120
-rw-r--r--spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js194
-rw-r--r--spec/frontend/vue_shared/components/gl_mentions_spec.js34
-rw-r--r--spec/frontend/vue_shared/components/issue/related_issuable_mock_data.js121
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js31
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js61
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js55
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js223
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js265
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_spec.js54
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js61
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_spec.js84
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js127
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js66
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js276
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/getters_spec.js31
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js172
-rw-r--r--spec/frontend/vue_shared/components/user_popover/user_popover_spec.js107
22 files changed, 2029 insertions, 83 deletions
diff --git a/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap
index 2abcc53bf14..1f54405928b 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap
@@ -8,6 +8,8 @@ exports[`Expand button on click when short text is provided renders button after
style="display: none;"
type="button"
>
+ <!---->
+
<svg
aria-hidden="true"
class="s12 ic-ellipsis_h"
@@ -32,6 +34,8 @@ exports[`Expand button on click when short text is provided renders button after
style=""
type="button"
>
+ <!---->
+
<svg
aria-hidden="true"
class="s12 ic-ellipsis_h"
@@ -51,6 +55,8 @@ exports[`Expand button when short text is provided renders button before text 1`
class="btn js-text-expander-prepend text-expander btn-blank btn-secondary btn-md"
type="button"
>
+ <!---->
+
<svg
aria-hidden="true"
class="s12 ic-ellipsis_h"
@@ -75,6 +81,8 @@ exports[`Expand button when short text is provided renders button before text 1`
style="display: none;"
type="button"
>
+ <!---->
+
<svg
aria-hidden="true"
class="s12 ic-ellipsis_h"
diff --git a/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js b/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
index 17ea78b5826..ce3f289eb6e 100644
--- a/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
@@ -1,14 +1,19 @@
import { shallowMount } from '@vue/test-utils';
import RichViewer from '~/vue_shared/components/blob_viewers/rich_viewer.vue';
+import { handleBlobRichViewer } from '~/blob/viewer';
+
+jest.mock('~/blob/viewer');
describe('Blob Rich Viewer component', () => {
let wrapper;
const content = '<h1 id="markdown">Foo Bar</h1>';
+ const defaultType = 'markdown';
- function createComponent() {
+ function createComponent(type = defaultType) {
wrapper = shallowMount(RichViewer, {
propsData: {
content,
+ type,
},
});
}
@@ -24,4 +29,8 @@ describe('Blob Rich Viewer component', () => {
it('renders the passed content without transformations', () => {
expect(wrapper.html()).toContain(content);
});
+
+ it('queries for advanced viewer', () => {
+ expect(handleBlobRichViewer).toHaveBeenCalledWith(expect.anything(), defaultType);
+ });
});
diff --git a/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js b/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js
index d12bfc5c686..79195aa1350 100644
--- a/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js
@@ -10,6 +10,7 @@ describe('Blob Simple Viewer component', () => {
wrapper = shallowMount(SimpleViewer, {
propsData: {
content,
+ type: 'text',
},
});
}
diff --git a/spec/frontend/vue_shared/components/changed_file_icon_spec.js b/spec/frontend/vue_shared/components/changed_file_icon_spec.js
index 8258eb8204c..03519a6f803 100644
--- a/spec/frontend/vue_shared/components/changed_file_icon_spec.js
+++ b/spec/frontend/vue_shared/components/changed_file_icon_spec.js
@@ -5,6 +5,7 @@ import Icon from '~/vue_shared/components/icon.vue';
const changedFile = () => ({ changed: true });
const stagedFile = () => ({ changed: true, staged: true });
const newFile = () => ({ changed: true, tempFile: true });
+const deletedFile = () => ({ changed: false, tempFile: false, staged: false, deleted: true });
const unchangedFile = () => ({ changed: false, tempFile: false, staged: false, deleted: false });
describe('Changed file icon', () => {
@@ -54,10 +55,11 @@ describe('Changed file icon', () => {
});
describe.each`
- file | iconName | tooltipText | desc
- ${changedFile()} | ${'file-modified'} | ${'Unstaged modification'} | ${'with file changed'}
- ${stagedFile()} | ${'file-modified-solid'} | ${'Staged modification'} | ${'with file staged'}
- ${newFile()} | ${'file-addition'} | ${'Unstaged addition'} | ${'with file new'}
+ file | iconName | tooltipText | desc
+ ${changedFile()} | ${'file-modified'} | ${'Modified'} | ${'with file changed'}
+ ${stagedFile()} | ${'file-modified-solid'} | ${'Modified'} | ${'with file staged'}
+ ${newFile()} | ${'file-addition'} | ${'Added'} | ${'with file new'}
+ ${deletedFile()} | ${'file-deletion'} | ${'Deleted'} | ${'with file deleted'}
`('$desc', ({ file, iconName, tooltipText }) => {
beforeEach(() => {
factory({ file });
diff --git a/spec/frontend/vue_shared/components/confirm_modal_spec.js b/spec/frontend/vue_shared/components/confirm_modal_spec.js
new file mode 100644
index 00000000000..7bccd6f1a64
--- /dev/null
+++ b/spec/frontend/vue_shared/components/confirm_modal_spec.js
@@ -0,0 +1,120 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlModal } from '@gitlab/ui';
+import { TEST_HOST } from 'helpers/test_constants';
+import ConfirmModal from '~/vue_shared/components/confirm_modal.vue';
+
+jest.mock('~/lib/utils/csrf', () => ({ token: 'test-csrf-token' }));
+
+describe('vue_shared/components/confirm_modal', () => {
+ const MOCK_MODAL_DATA = {
+ path: `${TEST_HOST}/1`,
+ method: 'delete',
+ modalAttributes: {
+ title: 'Are you sure?',
+ message: 'This will remove item 1',
+ okVariant: 'danger',
+ okTitle: 'Remove item',
+ },
+ };
+
+ const defaultProps = {
+ selector: '.test-button',
+ };
+
+ const actionSpies = {
+ openModal: jest.fn(),
+ closeModal: jest.fn(),
+ };
+
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(ConfirmModal, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ methods: {
+ ...actionSpies,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findModal = () => wrapper.find(GlModal);
+ const findForm = () => wrapper.find('form');
+ const findFormData = () =>
+ findForm()
+ .findAll('input')
+ .wrappers.map(x => ({ name: x.attributes('name'), value: x.attributes('value') }));
+
+ describe('template', () => {
+ describe('when modal data is set', () => {
+ beforeEach(() => {
+ createComponent();
+ wrapper.vm.modalAttributes = MOCK_MODAL_DATA.modalAttributes;
+ });
+
+ it('renders GlModal wtih data', () => {
+ expect(findModal().exists()).toBeTruthy();
+ expect(findModal().attributes()).toEqual(
+ expect.objectContaining({
+ oktitle: MOCK_MODAL_DATA.modalAttributes.okTitle,
+ okvariant: MOCK_MODAL_DATA.modalAttributes.okVariant,
+ }),
+ );
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('submitModal', () => {
+ beforeEach(() => {
+ createComponent();
+ wrapper.vm.path = MOCK_MODAL_DATA.path;
+ wrapper.vm.method = MOCK_MODAL_DATA.method;
+ });
+
+ it('does not submit form', () => {
+ expect(findForm().element.submit).not.toHaveBeenCalled();
+ });
+
+ describe('when modal submitted', () => {
+ beforeEach(() => {
+ findModal().vm.$emit('primary');
+ });
+
+ it('submits form', () => {
+ expect(findFormData()).toEqual([
+ { name: '_method', value: MOCK_MODAL_DATA.method },
+ { name: 'authenticity_token', value: 'test-csrf-token' },
+ ]);
+ expect(findForm().element.submit).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('closeModal', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('does not close modal', () => {
+ expect(actionSpies.closeModal).not.toHaveBeenCalled();
+ });
+
+ describe('when modal closed', () => {
+ beforeEach(() => {
+ findModal().vm.$emit('cancel');
+ });
+
+ it('closes modal', () => {
+ expect(actionSpies.closeModal).toHaveBeenCalled();
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js b/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js
new file mode 100644
index 00000000000..f364f374887
--- /dev/null
+++ b/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js
@@ -0,0 +1,194 @@
+import Vue from 'vue';
+import { compileToFunctions } from 'vue-template-compiler';
+import { mount } from '@vue/test-utils';
+
+import { GREEN_BOX_IMAGE_URL, RED_BOX_IMAGE_URL } from 'spec/test_constants';
+import imageDiffViewer from '~/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue';
+
+describe('ImageDiffViewer', () => {
+ const requiredProps = {
+ diffMode: 'replaced',
+ newPath: GREEN_BOX_IMAGE_URL,
+ oldPath: RED_BOX_IMAGE_URL,
+ };
+ const allProps = {
+ ...requiredProps,
+ oldSize: 2048,
+ newSize: 1024,
+ };
+ let wrapper;
+ let vm;
+
+ function createComponent(props) {
+ const ImageDiffViewer = Vue.extend(imageDiffViewer);
+ wrapper = mount(ImageDiffViewer, { propsData: props });
+ vm = wrapper.vm;
+ }
+
+ const triggerEvent = (eventName, el = vm.$el, clientX = 0) => {
+ const event = new MouseEvent(eventName, {
+ bubbles: true,
+ cancelable: true,
+ view: window,
+ detail: 1,
+ screenX: clientX,
+ clientX,
+ });
+
+ // JSDOM does not implement experimental APIs
+ event.pageX = clientX;
+
+ el.dispatchEvent(event);
+ };
+
+ const dragSlider = (sliderElement, doc, dragPixel) => {
+ triggerEvent('mousedown', sliderElement);
+ triggerEvent('mousemove', doc.body, dragPixel);
+ triggerEvent('mouseup', doc.body);
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders image diff for replaced', done => {
+ createComponent({ ...allProps });
+
+ vm.$nextTick(() => {
+ const metaInfoElements = vm.$el.querySelectorAll('.image-info');
+
+ expect(vm.$el.querySelector('.added img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL);
+
+ expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe(RED_BOX_IMAGE_URL);
+
+ expect(vm.$el.querySelector('.view-modes-menu li.active').textContent.trim()).toBe('2-up');
+ expect(vm.$el.querySelector('.view-modes-menu li:nth-child(2)').textContent.trim()).toBe(
+ 'Swipe',
+ );
+
+ expect(vm.$el.querySelector('.view-modes-menu li:nth-child(3)').textContent.trim()).toBe(
+ 'Onion skin',
+ );
+
+ expect(metaInfoElements.length).toBe(2);
+ expect(metaInfoElements[0]).toHaveText('2.00 KiB');
+ expect(metaInfoElements[1]).toHaveText('1.00 KiB');
+
+ done();
+ });
+ });
+
+ it('renders image diff for new', done => {
+ createComponent({ ...allProps, diffMode: 'new', oldPath: '' });
+
+ setImmediate(() => {
+ const metaInfoElement = vm.$el.querySelector('.image-info');
+
+ expect(vm.$el.querySelector('.added img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL);
+ expect(metaInfoElement).toHaveText('1.00 KiB');
+
+ done();
+ });
+ });
+
+ it('renders image diff for deleted', done => {
+ createComponent({ ...allProps, diffMode: 'deleted', newPath: '' });
+
+ setImmediate(() => {
+ const metaInfoElement = vm.$el.querySelector('.image-info');
+
+ expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe(RED_BOX_IMAGE_URL);
+ expect(metaInfoElement).toHaveText('2.00 KiB');
+
+ done();
+ });
+ });
+
+ it('renders image diff for renamed', done => {
+ vm = new Vue({
+ components: {
+ imageDiffViewer,
+ },
+ data: {
+ ...allProps,
+ diffMode: 'renamed',
+ },
+ ...compileToFunctions(`
+ <image-diff-viewer
+ :diff-mode="diffMode"
+ :new-path="newPath"
+ :old-path="oldPath"
+ :new-size="newSize"
+ :old-size="oldSize"
+ >
+ <span slot="image-overlay" class="overlay">test</span>
+ </image-diff-viewer>
+ `),
+ }).$mount();
+
+ setImmediate(() => {
+ const metaInfoElement = vm.$el.querySelector('.image-info');
+
+ expect(vm.$el.querySelector('img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL);
+ expect(vm.$el.querySelector('.overlay')).not.toBe(null);
+
+ expect(metaInfoElement).toHaveText('2.00 KiB');
+
+ done();
+ });
+ });
+
+ describe('swipeMode', () => {
+ beforeEach(done => {
+ createComponent({ ...requiredProps });
+
+ setImmediate(() => {
+ done();
+ });
+ });
+
+ it('switches to Swipe Mode', done => {
+ vm.$el.querySelector('.view-modes-menu li:nth-child(2)').click();
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.view-modes-menu li.active').textContent.trim()).toBe('Swipe');
+ done();
+ });
+ });
+ });
+
+ describe('onionSkin', () => {
+ beforeEach(done => {
+ createComponent({ ...requiredProps });
+
+ setImmediate(() => {
+ done();
+ });
+ });
+
+ it('switches to Onion Skin Mode', done => {
+ vm.$el.querySelector('.view-modes-menu li:nth-child(3)').click();
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.view-modes-menu li.active').textContent.trim()).toBe(
+ 'Onion skin',
+ );
+ done();
+ });
+ });
+
+ it('has working drag handler', done => {
+ vm.$el.querySelector('.view-modes-menu li:nth-child(3)').click();
+
+ vm.$nextTick(() => {
+ dragSlider(vm.$el.querySelector('.dragger'), document, 20);
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.dragger').style.left).toBe('20px');
+ expect(vm.$el.querySelector('.added.frame').style.opacity).toBe('0.2');
+ done();
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/gl_mentions_spec.js b/spec/frontend/vue_shared/components/gl_mentions_spec.js
new file mode 100644
index 00000000000..32fc055a77d
--- /dev/null
+++ b/spec/frontend/vue_shared/components/gl_mentions_spec.js
@@ -0,0 +1,34 @@
+import { shallowMount } from '@vue/test-utils';
+import Tribute from 'tributejs';
+import GlMentions from '~/vue_shared/components/gl_mentions.vue';
+
+describe('GlMentions', () => {
+ let wrapper;
+
+ describe('Tribute', () => {
+ const mentions = '/gitlab-org/gitlab-test/-/autocomplete_sources/members?type=Issue&type_id=1';
+
+ beforeEach(() => {
+ wrapper = shallowMount(GlMentions, {
+ propsData: {
+ dataSources: {
+ mentions,
+ },
+ },
+ slots: {
+ default: ['<input/>'],
+ },
+ });
+ });
+
+ it('is set to tribute instance variable', () => {
+ expect(wrapper.vm.tribute instanceof Tribute).toBe(true);
+ });
+
+ it('contains the slot input element', () => {
+ wrapper.find('input').setValue('@');
+
+ expect(wrapper.vm.tribute.current.element).toBe(wrapper.find('input').element);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/issue/related_issuable_mock_data.js b/spec/frontend/vue_shared/components/issue/related_issuable_mock_data.js
new file mode 100644
index 00000000000..5f69d761fdf
--- /dev/null
+++ b/spec/frontend/vue_shared/components/issue/related_issuable_mock_data.js
@@ -0,0 +1,121 @@
+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: `${gl.TEST_HOST}`,
+ web_url: `${gl.TEST_HOST}/root`,
+ status_tooltip_html: null,
+ path: '/root',
+ },
+ {
+ id: 13,
+ name: 'Brooks Beatty',
+ username: 'brynn_champlin',
+ state: 'active',
+ avatar_url: `${gl.TEST_HOST}`,
+ web_url: `${gl.TEST_HOST}/brynn_champlin`,
+ status_tooltip_html: null,
+ path: '/brynn_champlin',
+ },
+ {
+ id: 6,
+ name: 'Bryce Turcotte',
+ username: 'melynda',
+ state: 'active',
+ avatar_url: `${gl.TEST_HOST}`,
+ web_url: `${gl.TEST_HOST}/melynda`,
+ status_tooltip_html: null,
+ path: '/melynda',
+ },
+ {
+ id: 20,
+ name: 'Conchita Eichmann',
+ username: 'juliana_gulgowski',
+ state: 'active',
+ avatar_url: `${gl.TEST_HOST}`,
+ web_url: `${gl.TEST_HOST}/juliana_gulgowski`,
+ status_tooltip_html: null,
+ path: '/juliana_gulgowski',
+ },
+];
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js
index 2fffb31acf5..5cbbb99eaef 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js
@@ -1,39 +1,38 @@
-import Vue from 'vue';
-
-import mountComponent from 'helpers/vue_mount_component_helper';
+import { shallowMount } from '@vue/test-utils';
+import { GlLoadingIcon } from '@gitlab/ui';
import dropdownTitleComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_title.vue';
-const createComponent = (canEdit = true) => {
- const Component = Vue.extend(dropdownTitleComponent);
-
- return mountComponent(Component, {
- canEdit,
+const createComponent = (canEdit = true) =>
+ shallowMount(dropdownTitleComponent, {
+ propsData: {
+ canEdit,
+ },
});
-};
describe('DropdownTitleComponent', () => {
- let vm;
+ let wrapper;
beforeEach(() => {
- vm = createComponent();
+ wrapper = createComponent();
});
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
+ wrapper = null;
});
describe('template', () => {
it('renders title text', () => {
- expect(vm.$el.classList.contains('title', 'hide-collapsed')).toBe(true);
- expect(vm.$el.innerText.trim()).toContain('Labels');
+ expect(wrapper.vm.$el.classList.contains('title', 'hide-collapsed')).toBe(true);
+ expect(wrapper.vm.$el.innerText.trim()).toContain('Labels');
});
it('renders spinner icon element', () => {
- expect(vm.$el.querySelector('.fa-spinner.fa-spin.block-loading')).not.toBeNull();
+ expect(wrapper.find(GlLoadingIcon)).not.toBeNull();
});
it('renders `Edit` button element', () => {
- const editBtnEl = vm.$el.querySelector('button.edit-link.js-sidebar-dropdown-toggle');
+ const editBtnEl = wrapper.vm.$el.querySelector('button.edit-link.js-sidebar-dropdown-toggle');
expect(editBtnEl).not.toBeNull();
expect(editBtnEl.innerText.trim()).toBe('Edit');
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
index 54ad96073c8..06355c0dd65 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
@@ -1,31 +1,26 @@
import { mount } from '@vue/test-utils';
-import { hexToRgb } from '~/lib/utils/color_utils';
import DropdownValueComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value.vue';
-import DropdownValueScopedLabel from '~/vue_shared/components/sidebar/labels_select/dropdown_value_scoped_label.vue';
+import { GlLabel } from '@gitlab/ui';
import {
mockConfig,
mockLabels,
} from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data';
-const labelStyles = {
- textColor: '#FFFFFF',
- color: '#BADA55',
-};
const createComponent = (
labels = mockLabels,
labelFilterBasePath = mockConfig.labelFilterBasePath,
-) => {
- labels.forEach(label => Object.assign(label, labelStyles));
-
- return mount(DropdownValueComponent, {
+) =>
+ mount(DropdownValueComponent, {
propsData: {
labels,
labelFilterBasePath,
enableScopedLabels: true,
},
+ stubs: {
+ GlLabel: true,
+ },
});
-};
describe('DropdownValueComponent', () => {
let vm;
@@ -56,24 +51,17 @@ describe('DropdownValueComponent', () => {
describe('methods', () => {
describe('labelFilterUrl', () => {
it('returns URL string starting with labelFilterBasePath and encoded label.title', () => {
- expect(vm.find(DropdownValueScopedLabel).props('labelFilterUrl')).toBe(
- '/gitlab-org/my-project/issues?label_name[]=Foo%3A%3ABar',
+ expect(vm.find(GlLabel).props('target')).toBe(
+ '/gitlab-org/my-project/issues?label_name[]=Foo%20Label',
);
});
});
- describe('labelStyle', () => {
- it('returns object with `color` & `backgroundColor` properties from label.textColor & label.color', () => {
- expect(vm.find(DropdownValueScopedLabel).props('labelStyle')).toEqual({
- color: labelStyles.textColor,
- backgroundColor: labelStyles.color,
- });
- });
- });
-
describe('showScopedLabels', () => {
it('returns true if the label is scoped label', () => {
- expect(vm.findAll(DropdownValueScopedLabel).length).toEqual(1);
+ const labels = vm.findAll(GlLabel);
+ expect(labels.length).toEqual(2);
+ expect(labels.at(1).props('scoped')).toBe(true);
});
});
});
@@ -95,33 +83,10 @@ describe('DropdownValueComponent', () => {
vmEmptyLabels.destroy();
});
- it('renders label element with filter URL', () => {
- expect(vm.find('a').attributes('href')).toBe(
- '/gitlab-org/my-project/issues?label_name[]=Foo%20Label',
- );
- });
-
- it('renders label element and styles based on label details', () => {
- const labelEl = vm.find('a span.badge.color-label');
+ it('renders DropdownValueComponent element', () => {
+ const labelEl = vm.find(GlLabel);
expect(labelEl.exists()).toBe(true);
- expect(labelEl.attributes('style')).toContain(
- `background-color: rgb(${hexToRgb(labelStyles.color).join(', ')});`,
- );
- expect(labelEl.text().trim()).toBe(mockLabels[0].title);
- });
-
- describe('label is of scoped-label type', () => {
- it('renders a scoped-label-wrapper span to incorporate 2 anchors', () => {
- expect(vm.find('span.scoped-label-wrapper').exists()).toBe(true);
- });
-
- it('renders anchor tag containing question icon', () => {
- const anchor = vm.find('.scoped-label-wrapper a.scoped-label');
-
- expect(anchor.exists()).toBe(true);
- expect(anchor.find('i.fa-question-circle').exists()).toBe(true);
- });
});
});
});
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
new file mode 100644
index 00000000000..d996f48f9cc
--- /dev/null
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js
@@ -0,0 +1,55 @@
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+
+import { GlIcon } from '@gitlab/ui';
+import DropdownButton from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue';
+
+import labelSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
+
+import { mockConfig } from './mock_data';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+const createComponent = (initialState = mockConfig) => {
+ const store = new Vuex.Store(labelSelectModule());
+
+ store.dispatch('setInitialState', initialState);
+
+ return shallowMount(DropdownButton, {
+ localVue,
+ store,
+ });
+};
+
+describe('DropdownButton', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('template', () => {
+ it('renders component container element', () => {
+ expect(wrapper.is('gl-button-stub')).toBe(true);
+ });
+
+ it('renders button text element', () => {
+ const dropdownTextEl = wrapper.find('.dropdown-toggle-text');
+
+ expect(dropdownTextEl.exists()).toBe(true);
+ expect(dropdownTextEl.text()).toBe('Label');
+ });
+
+ it('renders chevron icon element', () => {
+ const iconEl = wrapper.find(GlIcon);
+
+ expect(iconEl.exists()).toBe(true);
+ expect(iconEl.props('name')).toBe('chevron-down');
+ });
+ });
+});
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
new file mode 100644
index 00000000000..9bc01d8723f
--- /dev/null
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js
@@ -0,0 +1,223 @@
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+
+import { GlButton, GlIcon, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui';
+import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue';
+
+import labelSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
+
+import { mockConfig, mockSuggestedColors } from './mock_data';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+const createComponent = (initialState = mockConfig) => {
+ const store = new Vuex.Store(labelSelectModule());
+
+ store.dispatch('setInitialState', initialState);
+
+ return shallowMount(DropdownContentsCreateView, {
+ localVue,
+ store,
+ });
+};
+
+describe('DropdownContentsCreateView', () => {
+ let wrapper;
+ const colors = Object.keys(mockSuggestedColors).map(color => ({
+ [color]: mockSuggestedColors[color],
+ }));
+
+ beforeEach(() => {
+ gon.suggested_label_colors = mockSuggestedColors;
+ wrapper = createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('computed', () => {
+ describe('disableCreate', () => {
+ it('returns `true` when label title and color is not defined', () => {
+ expect(wrapper.vm.disableCreate).toBe(true);
+ });
+
+ it('returns `true` when `labelCreateInProgress` is true', () => {
+ wrapper.setData({
+ labelTitle: 'Foo',
+ selectedColor: '#ff0000',
+ });
+ wrapper.vm.$store.dispatch('requestCreateLabel');
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.vm.disableCreate).toBe(true);
+ });
+ });
+
+ it('returns `false` when label title and color is defined and create request is not already in progress', () => {
+ wrapper.setData({
+ labelTitle: 'Foo',
+ selectedColor: '#ff0000',
+ });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.vm.disableCreate).toBe(false);
+ });
+ });
+ });
+
+ describe('suggestedColors', () => {
+ it('returns array of color objects containing color code and name', () => {
+ colors.forEach((color, index) => {
+ expect(wrapper.vm.suggestedColors[index]).toEqual(expect.objectContaining(color));
+ });
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('getColorCode', () => {
+ it('returns color code from color object', () => {
+ expect(wrapper.vm.getColorCode(colors[0])).toBe(Object.keys(colors[0]).pop());
+ });
+ });
+
+ describe('getColorName', () => {
+ it('returns color name from color object', () => {
+ expect(wrapper.vm.getColorName(colors[0])).toBe(Object.values(colors[0]).pop());
+ });
+ });
+
+ describe('handleColorClick', () => {
+ it('sets provided `color` param to `selectedColor` prop', () => {
+ wrapper.vm.handleColorClick(colors[0]);
+
+ expect(wrapper.vm.selectedColor).toBe(Object.keys(colors[0]).pop());
+ });
+ });
+
+ describe('handleCreateClick', () => {
+ it('calls action `createLabel` with object containing `labelTitle` & `selectedColor`', () => {
+ jest.spyOn(wrapper.vm, 'createLabel').mockImplementation();
+ wrapper.setData({
+ labelTitle: 'Foo',
+ selectedColor: '#ff0000',
+ });
+
+ wrapper.vm.handleCreateClick();
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.vm.createLabel).toHaveBeenCalledWith(
+ expect.objectContaining({
+ title: 'Foo',
+ color: '#ff0000',
+ }),
+ );
+ });
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('renders component container element with class "labels-select-contents-create"', () => {
+ expect(wrapper.attributes('class')).toContain('labels-select-contents-create');
+ });
+
+ it('renders dropdown back button element', () => {
+ const backBtnEl = wrapper
+ .find('.dropdown-title')
+ .findAll(GlButton)
+ .at(0);
+
+ expect(backBtnEl.exists()).toBe(true);
+ expect(backBtnEl.attributes('aria-label')).toBe('Go back');
+ expect(backBtnEl.find(GlIcon).props('name')).toBe('arrow-left');
+ });
+
+ it('renders dropdown title element', () => {
+ const headerEl = wrapper.find('.dropdown-title > span');
+
+ expect(headerEl.exists()).toBe(true);
+ expect(headerEl.text()).toBe('Create label');
+ });
+
+ it('renders dropdown close button element', () => {
+ const closeBtnEl = wrapper
+ .find('.dropdown-title')
+ .findAll(GlButton)
+ .at(1);
+
+ expect(closeBtnEl.exists()).toBe(true);
+ expect(closeBtnEl.attributes('aria-label')).toBe('Close');
+ expect(closeBtnEl.find(GlIcon).props('name')).toBe('close');
+ });
+
+ it('renders label title input element', () => {
+ const titleInputEl = wrapper.find('.dropdown-input').find(GlFormInput);
+
+ expect(titleInputEl.exists()).toBe(true);
+ expect(titleInputEl.attributes('placeholder')).toBe('Name new label');
+ expect(titleInputEl.attributes('autofocus')).toBe('true');
+ });
+
+ it('renders color block element for all suggested colors', () => {
+ const colorBlocksEl = wrapper.find('.dropdown-content').findAll(GlLink);
+
+ colorBlocksEl.wrappers.forEach((colorBlock, index) => {
+ expect(colorBlock.attributes('style')).toContain('background-color');
+ expect(colorBlock.attributes('title')).toBe(Object.values(colors[index]).pop());
+ });
+ });
+
+ it('renders color input element', () => {
+ wrapper.setData({
+ selectedColor: '#ff0000',
+ });
+
+ return wrapper.vm.$nextTick(() => {
+ const colorPreviewEl = wrapper.find(
+ '.color-input-container > .dropdown-label-color-preview',
+ );
+ const colorInputEl = wrapper.find('.color-input-container').find(GlFormInput);
+
+ expect(colorPreviewEl.exists()).toBe(true);
+ expect(colorPreviewEl.attributes('style')).toContain('background-color');
+ expect(colorInputEl.exists()).toBe(true);
+ expect(colorInputEl.attributes('placeholder')).toBe('Use custom color #FF0000');
+ expect(colorInputEl.attributes('value')).toBe('#ff0000');
+ });
+ });
+
+ it('renders create button element', () => {
+ const createBtnEl = wrapper
+ .find('.dropdown-actions')
+ .findAll(GlButton)
+ .at(0);
+
+ expect(createBtnEl.exists()).toBe(true);
+ expect(createBtnEl.text()).toContain('Create');
+ });
+
+ it('shows gl-loading-icon within create button element when `labelCreateInProgress` is `true`', () => {
+ wrapper.vm.$store.dispatch('requestCreateLabel');
+
+ return wrapper.vm.$nextTick(() => {
+ const loadingIconEl = wrapper.find('.dropdown-actions').find(GlLoadingIcon);
+
+ expect(loadingIconEl.exists()).toBe(true);
+ expect(loadingIconEl.isVisible()).toBe(true);
+ });
+ });
+
+ it('renders cancel button element', () => {
+ const cancelBtnEl = wrapper
+ .find('.dropdown-actions')
+ .findAll(GlButton)
+ .at(1);
+
+ expect(cancelBtnEl.exists()).toBe(true);
+ expect(cancelBtnEl.text()).toContain('Cancel');
+ });
+ });
+});
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
new file mode 100644
index 00000000000..487b917852e
--- /dev/null
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js
@@ -0,0 +1,265 @@
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+
+import { GlButton, GlLoadingIcon, GlIcon, GlSearchBoxByType, GlLink } from '@gitlab/ui';
+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';
+
+import defaultState from '~/vue_shared/components/sidebar/labels_select_vue/store/state';
+import mutations from '~/vue_shared/components/sidebar/labels_select_vue/store/mutations';
+import * as actions from '~/vue_shared/components/sidebar/labels_select_vue/store/actions';
+import * as getters from '~/vue_shared/components/sidebar/labels_select_vue/store/getters';
+
+import { mockConfig, mockLabels, mockRegularLabel } from './mock_data';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+const createComponent = (initialState = mockConfig) => {
+ const store = new Vuex.Store({
+ getters,
+ mutations,
+ state: {
+ ...defaultState(),
+ footerCreateLabelTitle: 'Create label',
+ footerManageLabelTitle: 'Manage labels',
+ },
+ actions: {
+ ...actions,
+ fetchLabels: jest.fn(),
+ },
+ });
+
+ store.dispatch('setInitialState', initialState);
+ store.dispatch('receiveLabelsSuccess', mockLabels);
+
+ return shallowMount(DropdownContentsLabelsView, {
+ localVue,
+ store,
+ });
+};
+
+describe('DropdownContentsLabelsView', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('computed', () => {
+ describe('visibleLabels', () => {
+ it('returns matching labels filtered with `searchKey`', () => {
+ wrapper.setData({
+ searchKey: 'bug',
+ });
+
+ expect(wrapper.vm.visibleLabels.length).toBe(1);
+ expect(wrapper.vm.visibleLabels[0].title).toBe('Bug');
+ });
+
+ it('returns all labels when `searchKey` is empty', () => {
+ wrapper.setData({
+ searchKey: '',
+ });
+
+ expect(wrapper.vm.visibleLabels.length).toBe(mockLabels.length);
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('getDropdownLabelBoxStyle', () => {
+ it('returns an object containing `backgroundColor` based on provided `label` param', () => {
+ expect(wrapper.vm.getDropdownLabelBoxStyle(mockRegularLabel)).toEqual(
+ expect.objectContaining({
+ backgroundColor: mockRegularLabel.color,
+ }),
+ );
+ });
+ });
+
+ describe('isLabelSelected', () => {
+ it('returns true when provided `label` param is one of the selected labels', () => {
+ expect(wrapper.vm.isLabelSelected(mockRegularLabel)).toBe(true);
+ });
+
+ it('returns false when provided `label` param is not one of the selected labels', () => {
+ expect(wrapper.vm.isLabelSelected(mockLabels[2])).toBe(false);
+ });
+ });
+
+ describe('handleKeyDown', () => {
+ it('decreases `currentHighlightItem` value by 1 when Up arrow key is pressed', () => {
+ wrapper.setData({
+ currentHighlightItem: 1,
+ });
+
+ wrapper.vm.handleKeyDown({
+ keyCode: UP_KEY_CODE,
+ });
+
+ expect(wrapper.vm.currentHighlightItem).toBe(0);
+ });
+
+ it('increases `currentHighlightItem` value by 1 when Down arrow key is pressed', () => {
+ wrapper.setData({
+ currentHighlightItem: 1,
+ });
+
+ wrapper.vm.handleKeyDown({
+ keyCode: DOWN_KEY_CODE,
+ });
+
+ expect(wrapper.vm.currentHighlightItem).toBe(2);
+ });
+
+ it('calls action `updateSelectedLabels` with currently highlighted label when Enter key is pressed', () => {
+ jest.spyOn(wrapper.vm, 'updateSelectedLabels').mockImplementation();
+ wrapper.setData({
+ currentHighlightItem: 1,
+ });
+
+ wrapper.vm.handleKeyDown({
+ keyCode: ENTER_KEY_CODE,
+ });
+
+ expect(wrapper.vm.updateSelectedLabels).toHaveBeenCalledWith([
+ {
+ ...mockLabels[1],
+ set: true,
+ },
+ ]);
+ });
+
+ it('calls action `toggleDropdownContents` when Esc key is pressed', () => {
+ jest.spyOn(wrapper.vm, 'toggleDropdownContents').mockImplementation();
+ wrapper.setData({
+ currentHighlightItem: 1,
+ });
+
+ wrapper.vm.handleKeyDown({
+ keyCode: ESC_KEY_CODE,
+ });
+
+ expect(wrapper.vm.toggleDropdownContents).toHaveBeenCalled();
+ });
+
+ it('calls action `scrollIntoViewIfNeeded` in next tick when any key is pressed', () => {
+ jest.spyOn(wrapper.vm, 'scrollIntoViewIfNeeded').mockImplementation();
+ wrapper.setData({
+ currentHighlightItem: 1,
+ });
+
+ wrapper.vm.handleKeyDown({
+ keyCode: DOWN_KEY_CODE,
+ });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.vm.scrollIntoViewIfNeeded).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('handleLabelClick', () => {
+ it('calls action `updateSelectedLabels` with provided `label` param', () => {
+ jest.spyOn(wrapper.vm, 'updateSelectedLabels').mockImplementation();
+
+ wrapper.vm.handleLabelClick(mockRegularLabel);
+
+ expect(wrapper.vm.updateSelectedLabels).toHaveBeenCalledWith([mockRegularLabel]);
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('renders component container element with class `labels-select-contents-list`', () => {
+ expect(wrapper.attributes('class')).toContain('labels-select-contents-list');
+ });
+
+ it('renders gl-loading-icon component when `labelsFetchInProgress` prop is true', () => {
+ wrapper.vm.$store.dispatch('requestLabels');
+
+ return wrapper.vm.$nextTick(() => {
+ const loadingIconEl = wrapper.find(GlLoadingIcon);
+
+ expect(loadingIconEl.exists()).toBe(true);
+ expect(loadingIconEl.attributes('class')).toContain('labels-fetch-loading');
+ });
+ });
+
+ it('renders dropdown title element', () => {
+ const titleEl = wrapper.find('.dropdown-title > span');
+
+ expect(titleEl.exists()).toBe(true);
+ expect(titleEl.text()).toBe('Assign labels');
+ });
+
+ it('renders dropdown close button element', () => {
+ const closeButtonEl = wrapper.find('.dropdown-title').find(GlButton);
+
+ expect(closeButtonEl.exists()).toBe(true);
+ expect(closeButtonEl.find(GlIcon).exists()).toBe(true);
+ expect(closeButtonEl.find(GlIcon).props('name')).toBe('close');
+ });
+
+ it('renders label search input element', () => {
+ const searchInputEl = wrapper.find(GlSearchBoxByType);
+
+ expect(searchInputEl.exists()).toBe(true);
+ expect(searchInputEl.attributes('autofocus')).toBe('true');
+ });
+
+ it('renders label elements for all labels', () => {
+ const labelsEl = wrapper.findAll('.dropdown-content li');
+ const labelItemEl = labelsEl.at(0).find(GlLink);
+
+ expect(labelsEl.length).toBe(mockLabels.length);
+ expect(labelItemEl.exists()).toBe(true);
+ expect(labelItemEl.find(GlIcon).props('name')).toBe('mobile-issue-close');
+ expect(labelItemEl.find('.dropdown-label-box').attributes('style')).toBe(
+ 'background-color: rgb(186, 218, 85);',
+ );
+ expect(labelItemEl.find(GlLink).text()).toContain(mockLabels[0].title);
+ });
+
+ it('renders label element with "is-focused" when value of `currentHighlightItem` is more than -1', () => {
+ wrapper.setData({
+ currentHighlightItem: 0,
+ });
+
+ return wrapper.vm.$nextTick(() => {
+ const labelsEl = wrapper.findAll('.dropdown-content li');
+ const labelItemEl = labelsEl.at(0).find(GlLink);
+
+ expect(labelItemEl.attributes('class')).toContain('is-focused');
+ });
+ });
+
+ it('renders element containing "No matching results" when `searchKey` does not match with any label', () => {
+ wrapper.setData({
+ searchKey: 'abc',
+ });
+
+ return wrapper.vm.$nextTick(() => {
+ const noMatchEl = wrapper.find('.dropdown-content li');
+
+ expect(noMatchEl.exists()).toBe(true);
+ expect(noMatchEl.text()).toContain('No matching results');
+ });
+ });
+
+ it('renders footer list items', () => {
+ const createLabelBtn = wrapper.find('.dropdown-footer').find(GlButton);
+ const manageLabelsLink = wrapper.find('.dropdown-footer').find(GlLink);
+
+ expect(createLabelBtn.exists()).toBe(true);
+ expect(createLabelBtn.text()).toBe('Create label');
+ expect(manageLabelsLink.exists()).toBe(true);
+ expect(manageLabelsLink.text()).toBe('Manage labels');
+ });
+ });
+});
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
new file mode 100644
index 00000000000..bb462acf11c
--- /dev/null
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_spec.js
@@ -0,0 +1,54 @@
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+
+import DropdownContents from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue';
+
+import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
+
+import { mockConfig } from './mock_data';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+const createComponent = (initialState = mockConfig) => {
+ const store = new Vuex.Store(labelsSelectModule());
+
+ store.dispatch('setInitialState', initialState);
+
+ return shallowMount(DropdownContents, {
+ localVue,
+ store,
+ });
+};
+
+describe('DropdownContent', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('computed', () => {
+ describe('dropdownContentsView', () => {
+ it('returns string "dropdown-contents-create-view" when `showDropdownContentsCreateView` prop is `true`', () => {
+ wrapper.vm.$store.dispatch('toggleDropdownContentsCreateView');
+
+ expect(wrapper.vm.dropdownContentsView).toBe('dropdown-contents-create-view');
+ });
+
+ it('returns string "dropdown-contents-labels-view" when `showDropdownContentsCreateView` prop is `false`', () => {
+ expect(wrapper.vm.dropdownContentsView).toBe('dropdown-contents-labels-view');
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('renders component container element with class `labels-select-dropdown-contents`', () => {
+ expect(wrapper.attributes('class')).toContain('labels-select-dropdown-contents');
+ });
+ });
+});
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
new file mode 100644
index 00000000000..c1d9be7393c
--- /dev/null
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js
@@ -0,0 +1,61 @@
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+
+import { GlButton, GlLoadingIcon } from '@gitlab/ui';
+import DropdownTitle from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue';
+
+import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
+
+import { mockConfig } from './mock_data';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+const createComponent = (initialState = mockConfig) => {
+ const store = new Vuex.Store(labelsSelectModule());
+
+ store.dispatch('setInitialState', initialState);
+
+ return shallowMount(DropdownTitle, {
+ localVue,
+ store,
+ propsData: {
+ labelsSelectInProgress: false,
+ },
+ });
+};
+
+describe('DropdownTitle', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('template', () => {
+ it('renders component container element with string "Labels"', () => {
+ expect(wrapper.text()).toContain('Labels');
+ });
+
+ it('renders edit link', () => {
+ const editBtnEl = wrapper.find(GlButton);
+
+ expect(editBtnEl.exists()).toBe(true);
+ expect(editBtnEl.text()).toBe('Edit');
+ });
+
+ it('renders loading icon element when `labelsSelectInProgress` prop is true', () => {
+ wrapper.setProps({
+ labelsSelectInProgress: true,
+ });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(true);
+ });
+ });
+ });
+});
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
new file mode 100644
index 00000000000..70311f8235f
--- /dev/null
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_spec.js
@@ -0,0 +1,84 @@
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+
+import { GlLabel } from '@gitlab/ui';
+import DropdownValue from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue';
+
+import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
+
+import { mockConfig, mockRegularLabel, mockScopedLabel } from './mock_data';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+const createComponent = (initialState = mockConfig, slots = {}) => {
+ const store = new Vuex.Store(labelsSelectModule());
+
+ store.dispatch('setInitialState', initialState);
+
+ return shallowMount(DropdownValue, {
+ localVue,
+ store,
+ slots,
+ });
+};
+
+describe('DropdownValue', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('methods', () => {
+ describe('labelFilterUrl', () => {
+ it('returns a label filter URL based on provided label param', () => {
+ expect(wrapper.vm.labelFilterUrl(mockRegularLabel)).toBe(
+ '/gitlab-org/my-project/issues?label_name[]=Foo%20Label',
+ );
+ });
+ });
+
+ describe('scopedLabel', () => {
+ it('returns `true` when provided label param is a scoped label', () => {
+ expect(wrapper.vm.scopedLabel(mockScopedLabel)).toBe(true);
+ });
+
+ it('returns `false` when provided label param is a regular label', () => {
+ expect(wrapper.vm.scopedLabel(mockRegularLabel)).toBe(false);
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('renders class `has-labels` on component container element when `selectedLabels` is not empty', () => {
+ expect(wrapper.attributes('class')).toContain('has-labels');
+ });
+
+ it('renders element containing `None` when `selectedLabels` is empty', () => {
+ const wrapperNoLabels = createComponent(
+ {
+ ...mockConfig,
+ selectedLabels: [],
+ },
+ {
+ default: 'None',
+ },
+ );
+ const noneEl = wrapperNoLabels.find('span.text-secondary');
+
+ expect(noneEl.exists()).toBe(true);
+ expect(noneEl.text()).toBe('None');
+
+ wrapperNoLabels.destroy();
+ });
+
+ it('renders labels when `selectedLabels` is not empty', () => {
+ expect(wrapper.findAll(GlLabel).length).toBe(2);
+ });
+ });
+});
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
new file mode 100644
index 00000000000..126fd5438c4
--- /dev/null
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js
@@ -0,0 +1,127 @@
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+
+import LabelsSelectRoot from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
+import DropdownTitle from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue';
+import DropdownValue from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue';
+import DropdownValueCollapsed from '~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue';
+import DropdownButton from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue';
+import DropdownContents from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue';
+
+import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
+
+import { mockConfig } from './mock_data';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+const createComponent = (config = mockConfig, slots = {}) =>
+ shallowMount(LabelsSelectRoot, {
+ localVue,
+ slots,
+ store: new Vuex.Store(labelsSelectModule()),
+ propsData: config,
+ });
+
+describe('LabelsSelectRoot', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('methods', () => {
+ describe('handleVuexActionDispatch', () => {
+ it('calls `handleDropdownClose` when params `action.type` is `toggleDropdownContents` and state has `showDropdownButton` & `showDropdownContents` props `false`', () => {
+ jest.spyOn(wrapper.vm, 'handleDropdownClose').mockImplementation();
+
+ wrapper.vm.handleVuexActionDispatch(
+ { type: 'toggleDropdownContents' },
+ {
+ showDropdownButton: false,
+ showDropdownContents: false,
+ labels: [{ id: 1 }, { id: 2, touched: true }],
+ },
+ );
+
+ expect(wrapper.vm.handleDropdownClose).toHaveBeenCalledWith(
+ expect.arrayContaining([
+ {
+ id: 2,
+ touched: true,
+ },
+ ]),
+ );
+ });
+ });
+
+ describe('handleDropdownClose', () => {
+ it('emits `updateSelectedLabels` & `onDropdownClose` events on component when provided `labels` param is not empty', () => {
+ wrapper.vm.handleDropdownClose([{ id: 1 }, { id: 2 }]);
+
+ expect(wrapper.emitted().updateSelectedLabels).toBeTruthy();
+ expect(wrapper.emitted().onDropdownClose).toBeTruthy();
+ });
+
+ it('emits only `onDropdownClose` event on component when provided `labels` param is empty', () => {
+ wrapper.vm.handleDropdownClose([]);
+
+ expect(wrapper.emitted().updateSelectedLabels).toBeFalsy();
+ expect(wrapper.emitted().onDropdownClose).toBeTruthy();
+ });
+ });
+
+ describe('handleCollapsedValueClick', () => {
+ it('emits `toggleCollapse` event on component', () => {
+ wrapper.vm.handleCollapsedValueClick();
+
+ expect(wrapper.emitted().toggleCollapse).toBeTruthy();
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('renders component with classes `labels-select-wrapper position-relative`', () => {
+ expect(wrapper.attributes('class')).toContain('labels-select-wrapper position-relative');
+ });
+
+ it('renders `dropdown-value-collapsed` component when `allowLabelCreate` prop is `true`', () => {
+ expect(wrapper.find(DropdownValueCollapsed).exists()).toBe(true);
+ });
+
+ it('renders `dropdown-title` component', () => {
+ expect(wrapper.find(DropdownTitle).exists()).toBe(true);
+ });
+
+ it('renders `dropdown-value` component with slot when `showDropdownButton` prop is `false`', () => {
+ const wrapperDropdownValue = createComponent(mockConfig, {
+ default: 'None',
+ });
+
+ const valueComp = wrapperDropdownValue.find(DropdownValue);
+
+ expect(valueComp.exists()).toBe(true);
+ expect(valueComp.text()).toBe('None');
+
+ wrapperDropdownValue.destroy();
+ });
+
+ it('renders `dropdown-button` component when `showDropdownButton` prop is `true`', () => {
+ wrapper.vm.$store.dispatch('toggleDropdownButton');
+
+ expect(wrapper.find(DropdownButton).exists()).toBe(true);
+ });
+
+ it('renders `dropdown-contents` component when `showDropdownButton` & `showDropdownContents` prop is `true`', () => {
+ wrapper.vm.$store.dispatch('toggleDropdownContents');
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find(DropdownContents).exists()).toBe(true);
+ });
+ });
+ });
+});
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
new file mode 100644
index 00000000000..a863cddbaee
--- /dev/null
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js
@@ -0,0 +1,66 @@
+export const mockRegularLabel = {
+ id: 26,
+ title: 'Foo Label',
+ description: 'Foobar',
+ color: '#BADA55',
+ textColor: '#FFFFFF',
+};
+
+export const mockScopedLabel = {
+ id: 27,
+ title: 'Foo::Bar',
+ description: 'Foobar',
+ color: '#0033CC',
+ textColor: '#FFFFFF',
+};
+
+export const mockLabels = [
+ mockRegularLabel,
+ mockScopedLabel,
+ {
+ id: 28,
+ title: 'Bug',
+ description: 'Label for bugs',
+ color: '#FF0000',
+ textColor: '#FFFFFF',
+ },
+];
+
+export const mockConfig = {
+ allowLabelEdit: true,
+ allowLabelCreate: true,
+ allowScopedLabels: true,
+ labelsListTitle: 'Assign labels',
+ labelsCreateTitle: 'Create label',
+ dropdownOnly: false,
+ selectedLabels: [mockRegularLabel, mockScopedLabel],
+ labelsSelectInProgress: false,
+ labelsFetchPath: '/gitlab-org/my-project/-/labels.json',
+ labelsManagePath: '/gitlab-org/my-project/-/labels',
+ labelsFilterBasePath: '/gitlab-org/my-project/issues',
+ scopedLabelsDocumentationPath: '/help/user/project/labels.md#scoped-labels-premium',
+};
+
+export const mockSuggestedColors = {
+ '#0033CC': 'UA blue',
+ '#428BCA': 'Moderate blue',
+ '#44AD8E': 'Lime green',
+ '#A8D695': 'Feijoa',
+ '#5CB85C': 'Slightly desaturated green',
+ '#69D100': 'Bright green',
+ '#004E00': 'Very dark lime green',
+ '#34495E': 'Very dark desaturated blue',
+ '#7F8C8D': 'Dark grayish cyan',
+ '#A295D6': 'Slightly desaturated blue',
+ '#5843AD': 'Dark moderate blue',
+ '#8E44AD': 'Dark moderate violet',
+ '#FFECDB': 'Very pale orange',
+ '#AD4363': 'Dark moderate pink',
+ '#D10069': 'Strong pink',
+ '#CC0033': 'Strong red',
+ '#FF0000': 'Pure red',
+ '#D9534F': 'Soft red',
+ '#D1D100': 'Strong yellow',
+ '#F0AD4E': 'Soft orange',
+ '#AD8D43': 'Dark moderate orange',
+};
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js
new file mode 100644
index 00000000000..6e2363ba96f
--- /dev/null
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js
@@ -0,0 +1,276 @@
+import MockAdapter from 'axios-mock-adapter';
+
+import defaultState from '~/vue_shared/components/sidebar/labels_select_vue/store/state';
+import * as types from '~/vue_shared/components/sidebar/labels_select_vue/store/mutation_types';
+import * as actions from '~/vue_shared/components/sidebar/labels_select_vue/store/actions';
+
+import testAction from 'helpers/vuex_action_helper';
+import axios from '~/lib/utils/axios_utils';
+
+describe('LabelsSelect Actions', () => {
+ let state;
+ const mockInitialState = {
+ labels: [],
+ selectedLabels: [],
+ };
+
+ beforeEach(() => {
+ state = Object.assign({}, defaultState());
+ });
+
+ describe('setInitialState', () => {
+ it('sets initial store state', done => {
+ testAction(
+ actions.setInitialState,
+ mockInitialState,
+ state,
+ [{ type: types.SET_INITIAL_STATE, payload: mockInitialState }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('toggleDropdownButton', () => {
+ it('toggles dropdown button', done => {
+ testAction(
+ actions.toggleDropdownButton,
+ {},
+ state,
+ [{ type: types.TOGGLE_DROPDOWN_BUTTON }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('toggleDropdownContents', () => {
+ it('toggles dropdown contents', done => {
+ testAction(
+ actions.toggleDropdownContents,
+ {},
+ state,
+ [{ type: types.TOGGLE_DROPDOWN_CONTENTS }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('toggleDropdownContentsCreateView', () => {
+ it('toggles dropdown create view', done => {
+ testAction(
+ actions.toggleDropdownContentsCreateView,
+ {},
+ state,
+ [{ type: types.TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('requestLabels', () => {
+ it('sets value of `state.labelsFetchInProgress` to `true`', done => {
+ testAction(actions.requestLabels, {}, state, [{ type: types.REQUEST_LABELS }], [], done);
+ });
+ });
+
+ describe('receiveLabelsSuccess', () => {
+ it('sets provided labels to `state.labels`', done => {
+ const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
+
+ testAction(
+ actions.receiveLabelsSuccess,
+ labels,
+ state,
+ [{ type: types.RECEIVE_SET_LABELS_SUCCESS, payload: labels }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveLabelsFailure', () => {
+ beforeEach(() => {
+ setFixtures('<div class="flash-container"></div>');
+ });
+
+ it('sets value `state.labelsFetchInProgress` to `false`', done => {
+ testAction(
+ actions.receiveLabelsFailure,
+ {},
+ state,
+ [{ type: types.RECEIVE_SET_LABELS_FAILURE }],
+ [],
+ done,
+ );
+ });
+
+ it('shows flash error', () => {
+ actions.receiveLabelsFailure({ commit: () => {} });
+
+ expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
+ 'Error fetching labels.',
+ );
+ });
+ });
+
+ describe('fetchLabels', () => {
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ state.labelsFetchPath = 'labels.json';
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('on success', () => {
+ it('dispatches `requestLabels` & `receiveLabelsSuccess` actions', done => {
+ const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
+ mock.onGet(/labels.json/).replyOnce(200, labels);
+
+ testAction(
+ actions.fetchLabels,
+ {},
+ state,
+ [],
+ [{ type: 'requestLabels' }, { type: 'receiveLabelsSuccess', payload: labels }],
+ done,
+ );
+ });
+ });
+
+ describe('on failure', () => {
+ it('dispatches `requestLabels` & `receiveLabelsFailure` actions', done => {
+ mock.onGet(/labels.json/).replyOnce(500, {});
+
+ testAction(
+ actions.fetchLabels,
+ {},
+ state,
+ [],
+ [{ type: 'requestLabels' }, { type: 'receiveLabelsFailure' }],
+ done,
+ );
+ });
+ });
+ });
+
+ describe('requestCreateLabel', () => {
+ it('sets value `state.labelCreateInProgress` to `true`', done => {
+ testAction(
+ actions.requestCreateLabel,
+ {},
+ state,
+ [{ type: types.REQUEST_CREATE_LABEL }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveCreateLabelSuccess', () => {
+ it('sets value `state.labelCreateInProgress` to `false`', done => {
+ testAction(
+ actions.receiveCreateLabelSuccess,
+ {},
+ state,
+ [{ type: types.RECEIVE_CREATE_LABEL_SUCCESS }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveCreateLabelFailure', () => {
+ beforeEach(() => {
+ setFixtures('<div class="flash-container"></div>');
+ });
+
+ it('sets value `state.labelCreateInProgress` to `false`', done => {
+ testAction(
+ actions.receiveCreateLabelFailure,
+ {},
+ state,
+ [{ type: types.RECEIVE_CREATE_LABEL_FAILURE }],
+ [],
+ done,
+ );
+ });
+
+ it('shows flash error', () => {
+ actions.receiveCreateLabelFailure({ commit: () => {} });
+
+ expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
+ 'Error creating label.',
+ );
+ });
+ });
+
+ describe('createLabel', () => {
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ state.labelsManagePath = 'labels.json';
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('on success', () => {
+ it('dispatches `requestCreateLabel`, `receiveCreateLabelSuccess` & `toggleDropdownContentsCreateView` actions', done => {
+ const label = { id: 1 };
+ mock.onPost(/labels.json/).replyOnce(200, label);
+
+ testAction(
+ actions.createLabel,
+ {},
+ state,
+ [],
+ [
+ { type: 'requestCreateLabel' },
+ { type: 'receiveCreateLabelSuccess' },
+ { type: 'toggleDropdownContentsCreateView' },
+ ],
+ done,
+ );
+ });
+ });
+
+ describe('on failure', () => {
+ it('dispatches `requestCreateLabel` & `receiveCreateLabelFailure` actions', done => {
+ mock.onPost(/labels.json/).replyOnce(500, {});
+
+ testAction(
+ actions.createLabel,
+ {},
+ state,
+ [],
+ [{ type: 'requestCreateLabel' }, { type: 'receiveCreateLabelFailure' }],
+ done,
+ );
+ });
+ });
+ });
+
+ describe('updateSelectedLabels', () => {
+ it('updates `state.labels` based on provided `labels` param', done => {
+ const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
+
+ testAction(
+ actions.updateSelectedLabels,
+ labels,
+ state,
+ [{ type: types.UPDATE_SELECTED_LABELS, payload: { labels } }],
+ [],
+ done,
+ );
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/getters_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/getters_spec.js
new file mode 100644
index 00000000000..bfceaa0828b
--- /dev/null
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/getters_spec.js
@@ -0,0 +1,31 @@
+import * as getters from '~/vue_shared/components/sidebar/labels_select_vue/store/getters';
+
+describe('LabelsSelect Getters', () => {
+ describe('dropdownButtonText', () => {
+ it('returns string "Label" when state.labels has no selected labels', () => {
+ const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
+
+ expect(getters.dropdownButtonText({ labels })).toBe('Label');
+ });
+
+ it('returns label title when state.labels has only 1 label', () => {
+ const labels = [{ id: 1, title: 'Foobar', set: true }];
+
+ expect(getters.dropdownButtonText({ labels })).toBe('Foobar');
+ });
+
+ it('returns first label title and remaining labels count when state.labels has more than 1 label', () => {
+ const labels = [{ id: 1, title: 'Foo', set: true }, { id: 2, title: 'Bar', set: true }];
+
+ expect(getters.dropdownButtonText({ labels })).toBe('Foo +1 more');
+ });
+ });
+
+ describe('selectedLabelsList', () => {
+ it('returns array of IDs of all labels within `state.selectedLabels`', () => {
+ const selectedLabels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
+
+ expect(getters.selectedLabelsList({ selectedLabels })).toEqual([1, 2, 3, 4]);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js
new file mode 100644
index 00000000000..f6ca98fcc71
--- /dev/null
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js
@@ -0,0 +1,172 @@
+import mutations from '~/vue_shared/components/sidebar/labels_select_vue/store/mutations';
+import * as types from '~/vue_shared/components/sidebar/labels_select_vue/store/mutation_types';
+
+describe('LabelsSelect Mutations', () => {
+ describe(`${types.SET_INITIAL_STATE}`, () => {
+ it('initializes provided props to store state', () => {
+ const state = {};
+ mutations[types.SET_INITIAL_STATE](state, {
+ labels: 'foo',
+ });
+
+ expect(state.labels).toEqual('foo');
+ });
+ });
+
+ describe(`${types.TOGGLE_DROPDOWN_BUTTON}`, () => {
+ it('toggles value of `state.showDropdownButton`', () => {
+ const state = {
+ showDropdownButton: false,
+ };
+ mutations[types.TOGGLE_DROPDOWN_BUTTON](state);
+
+ expect(state.showDropdownButton).toBe(true);
+ });
+ });
+
+ describe(`${types.TOGGLE_DROPDOWN_CONTENTS}`, () => {
+ it('toggles value of `state.showDropdownButton` when `state.dropdownOnly` is false', () => {
+ const state = {
+ dropdownOnly: false,
+ showDropdownButton: false,
+ };
+ mutations[types.TOGGLE_DROPDOWN_CONTENTS](state);
+
+ expect(state.showDropdownButton).toBe(true);
+ });
+
+ it('toggles value of `state.showDropdownContents`', () => {
+ const state = {
+ showDropdownContents: false,
+ };
+ mutations[types.TOGGLE_DROPDOWN_CONTENTS](state);
+
+ expect(state.showDropdownContents).toBe(true);
+ });
+
+ it('sets value of `state.showDropdownContentsCreateView` to `false` when `showDropdownContents` is true', () => {
+ const state = {
+ showDropdownContents: false,
+ showDropdownContentsCreateView: true,
+ };
+ mutations[types.TOGGLE_DROPDOWN_CONTENTS](state);
+
+ expect(state.showDropdownContentsCreateView).toBe(false);
+ });
+ });
+
+ describe(`${types.TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW}`, () => {
+ it('toggles value of `state.showDropdownContentsCreateView`', () => {
+ const state = {
+ showDropdownContentsCreateView: false,
+ };
+ mutations[types.TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW](state);
+
+ expect(state.showDropdownContentsCreateView).toBe(true);
+ });
+ });
+
+ describe(`${types.REQUEST_LABELS}`, () => {
+ it('sets value of `state.labelsFetchInProgress` to true', () => {
+ const state = {
+ labelsFetchInProgress: false,
+ };
+ mutations[types.REQUEST_LABELS](state);
+
+ expect(state.labelsFetchInProgress).toBe(true);
+ });
+ });
+
+ describe(`${types.RECEIVE_SET_LABELS_SUCCESS}`, () => {
+ const selectedLabels = [{ id: 2 }, { id: 4 }];
+ const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
+
+ it('sets value of `state.labelsFetchInProgress` to false', () => {
+ const state = {
+ selectedLabels,
+ labelsFetchInProgress: true,
+ };
+ mutations[types.RECEIVE_SET_LABELS_SUCCESS](state, labels);
+
+ expect(state.labelsFetchInProgress).toBe(false);
+ });
+
+ it('sets provided `labels` to `state.labels` along with `set` prop based on `state.selectedLabels`', () => {
+ const selectedLabelIds = selectedLabels.map(label => label.id);
+ const state = {
+ selectedLabels,
+ labelsFetchInProgress: true,
+ };
+ mutations[types.RECEIVE_SET_LABELS_SUCCESS](state, labels);
+
+ state.labels.forEach(label => {
+ if (selectedLabelIds.includes(label.id)) {
+ expect(label.set).toBe(true);
+ }
+ });
+ });
+ });
+
+ describe(`${types.RECEIVE_SET_LABELS_FAILURE}`, () => {
+ it('sets value of `state.labelsFetchInProgress` to false', () => {
+ const state = {
+ labelsFetchInProgress: true,
+ };
+ mutations[types.RECEIVE_SET_LABELS_FAILURE](state);
+
+ expect(state.labelsFetchInProgress).toBe(false);
+ });
+ });
+
+ describe(`${types.REQUEST_CREATE_LABEL}`, () => {
+ it('sets value of `state.labelCreateInProgress` to true', () => {
+ const state = {
+ labelCreateInProgress: false,
+ };
+ mutations[types.REQUEST_CREATE_LABEL](state);
+
+ expect(state.labelCreateInProgress).toBe(true);
+ });
+ });
+
+ describe(`${types.RECEIVE_CREATE_LABEL_SUCCESS}`, () => {
+ it('sets value of `state.labelCreateInProgress` to false', () => {
+ const state = {
+ labelCreateInProgress: false,
+ };
+ mutations[types.RECEIVE_CREATE_LABEL_SUCCESS](state);
+
+ expect(state.labelCreateInProgress).toBe(false);
+ });
+ });
+
+ describe(`${types.RECEIVE_CREATE_LABEL_FAILURE}`, () => {
+ it('sets value of `state.labelCreateInProgress` to false', () => {
+ const state = {
+ labelCreateInProgress: false,
+ };
+ mutations[types.RECEIVE_CREATE_LABEL_FAILURE](state);
+
+ expect(state.labelCreateInProgress).toBe(false);
+ });
+ });
+
+ describe(`${types.UPDATE_SELECTED_LABELS}`, () => {
+ const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
+
+ it('updates `state.labels` to include `touched` and `set` props based on provided `labels` param', () => {
+ const updatedLabelIds = [2, 4];
+ const state = {
+ labels,
+ };
+ mutations[types.UPDATE_SELECTED_LABELS](state, { labels });
+
+ state.labels.forEach(label => {
+ if (updatedLabelIds.includes(label.id)) {
+ expect(label.touched).toBe(true);
+ expect(label.set).toBe(true);
+ }
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
index a8bbc80d2df..a2e2d2447d5 100644
--- a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
+++ b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
@@ -1,4 +1,4 @@
-import { GlSkeletonLoading } from '@gitlab/ui';
+import { GlSkeletonLoading, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import UserPopover from '~/vue_shared/components/user_popover/user_popover.vue';
import Icon from '~/vue_shared/components/icon.vue';
@@ -11,6 +11,7 @@ const DEFAULT_PROPS = {
location: 'Vienna',
bio: null,
organization: null,
+ jobTitle: null,
status: null,
},
};
@@ -39,6 +40,9 @@ describe('User Popover Component', () => {
target: findTarget(),
...props,
},
+ stubs: {
+ 'gl-sprintf': GlSprintf,
+ },
...options,
});
};
@@ -56,6 +60,7 @@ describe('User Popover Component', () => {
location: null,
bio: null,
organization: null,
+ jobTitle: null,
status: null,
},
},
@@ -85,51 +90,125 @@ describe('User Popover Component', () => {
});
describe('job data', () => {
- it('should show only bio if no organization is available', () => {
- const user = { ...DEFAULT_PROPS.user, bio: 'Engineer' };
+ const findWorkInformation = () => wrapper.find({ ref: 'workInformation' });
+ const findBio = () => wrapper.find({ ref: 'bio' });
+
+ it('should show only bio if organization and job title are not available', () => {
+ const user = { ...DEFAULT_PROPS.user, bio: 'My super interesting bio' };
createWrapper({ user });
- expect(wrapper.text()).toContain('Engineer');
+ expect(findBio().text()).toBe('My super interesting bio');
+ expect(findWorkInformation().exists()).toBe(false);
});
- it('should show only organization if no bio is available', () => {
+ it('should show only organization if job title is not available', () => {
const user = { ...DEFAULT_PROPS.user, organization: 'GitLab' };
createWrapper({ user });
- expect(wrapper.text()).toContain('GitLab');
+ expect(findWorkInformation().text()).toBe('GitLab');
+ });
+
+ it('should show only job title if organization is not available', () => {
+ const user = { ...DEFAULT_PROPS.user, jobTitle: 'Frontend Engineer' };
+
+ createWrapper({ user });
+
+ expect(findWorkInformation().text()).toBe('Frontend Engineer');
+ });
+
+ it('should show organization and job title if they are both available', () => {
+ const user = {
+ ...DEFAULT_PROPS.user,
+ organization: 'GitLab',
+ jobTitle: 'Frontend Engineer',
+ };
+
+ createWrapper({ user });
+
+ expect(findWorkInformation().text()).toBe('Frontend Engineer at GitLab');
+ });
+
+ it('should display bio and job info in separate lines', () => {
+ const user = {
+ ...DEFAULT_PROPS.user,
+ bio: 'My super interesting bio',
+ organization: 'GitLab',
+ };
+
+ createWrapper({ user });
+
+ expect(findBio().text()).toBe('My super interesting bio');
+ expect(findWorkInformation().text()).toBe('GitLab');
});
- it('should display bio and organization in separate lines', () => {
- const user = { ...DEFAULT_PROPS.user, bio: 'Engineer', organization: 'GitLab' };
+ it('should not encode special characters in bio', () => {
+ const user = {
+ ...DEFAULT_PROPS.user,
+ bio: 'I like <html> & CSS',
+ };
createWrapper({ user });
- expect(wrapper.find('.js-bio').text()).toContain('Engineer');
- expect(wrapper.find('.js-organization').text()).toContain('GitLab');
+ expect(findBio().text()).toBe('I like <html> & CSS');
});
- it('should not encode special characters in bio and organization', () => {
+ it('should not encode special characters in organization', () => {
const user = {
...DEFAULT_PROPS.user,
- bio: 'Manager & Team Lead',
organization: 'Me & my <funky> Company',
};
createWrapper({ user });
- expect(wrapper.find('.js-bio').text()).toContain('Manager & Team Lead');
- expect(wrapper.find('.js-organization').text()).toContain('Me & my <funky> Company');
+ expect(findWorkInformation().text()).toBe('Me & my <funky> Company');
+ });
+
+ it('should not encode special characters in job title', () => {
+ const user = {
+ ...DEFAULT_PROPS.user,
+ jobTitle: 'Manager & Team Lead',
+ };
+
+ createWrapper({ user });
+
+ expect(findWorkInformation().text()).toBe('Manager & Team Lead');
+ });
+
+ it('should not encode special characters when both job title and organization are set', () => {
+ const user = {
+ ...DEFAULT_PROPS.user,
+ jobTitle: 'Manager & Team Lead',
+ organization: 'Me & my <funky> Company',
+ };
+
+ createWrapper({ user });
+
+ expect(findWorkInformation().text()).toBe('Manager & Team Lead at Me & my <funky> Company');
});
it('shows icon for bio', () => {
+ const user = {
+ ...DEFAULT_PROPS.user,
+ bio: 'My super interesting bio',
+ };
+
+ createWrapper({ user });
+
expect(wrapper.findAll(Icon).filter(icon => icon.props('name') === 'profile').length).toEqual(
1,
);
});
it('shows icon for organization', () => {
+ const user = {
+ ...DEFAULT_PROPS.user,
+ organization: 'GitLab',
+ };
+
+ createWrapper({ user });
+
expect(wrapper.findAll(Icon).filter(icon => icon.props('name') === 'work').length).toEqual(1);
});
});