diff options
Diffstat (limited to 'spec/frontend/vue_shared')
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); }); }); |