diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-09-19 01:45:44 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-09-19 01:45:44 +0000 |
commit | 85dc423f7090da0a52c73eb66faf22ddb20efff9 (patch) | |
tree | 9160f299afd8c80c038f08e1545be119f5e3f1e1 /spec/frontend/issuable/related_issues | |
parent | 15c2c8c66dbe422588e5411eee7e68f1fa440bb8 (diff) | |
download | gitlab-ce-85dc423f7090da0a52c73eb66faf22ddb20efff9.tar.gz |
Add latest changes from gitlab-org/gitlab@13-4-stable-ee
Diffstat (limited to 'spec/frontend/issuable/related_issues')
6 files changed, 1378 insertions, 0 deletions
diff --git a/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js b/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js new file mode 100644 index 00000000000..bfbe4ec8e70 --- /dev/null +++ b/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js @@ -0,0 +1,289 @@ +import { mount, shallowMount } from '@vue/test-utils'; +import { issuableTypesMap, linkedIssueTypesMap, PathIdSeparator } from '~/related_issues/constants'; +import AddIssuableForm from '~/related_issues/components/add_issuable_form.vue'; + +const issuable1 = { + id: 200, + reference: 'foo/bar#123', + displayReference: '#123', + title: 'some title', + path: '/foo/bar/issues/123', + state: 'opened', +}; + +const issuable2 = { + id: 201, + reference: 'foo/bar#124', + displayReference: '#124', + title: 'some other thing', + path: '/foo/bar/issues/124', + state: 'opened', +}; + +const pathIdSeparator = PathIdSeparator.Issue; + +const findFormInput = wrapper => wrapper.find('.js-add-issuable-form-input').element; + +const findRadioInput = (inputs, value) => inputs.filter(input => input.element.value === value)[0]; + +const findRadioInputs = wrapper => wrapper.findAll('[name="linked-issue-type-radio"]'); + +const constructWrapper = props => { + return shallowMount(AddIssuableForm, { + propsData: { + inputValue: '', + pendingReferences: [], + pathIdSeparator, + ...props, + }, + }); +}; + +describe('AddIssuableForm', () => { + let wrapper; + + afterEach(() => { + // Jest doesn't blur an item even if it is destroyed, + // so blur the input manually after each test + const input = findFormInput(wrapper); + if (input) input.blur(); + + wrapper.destroy(); + }); + + describe('with data', () => { + describe('without references', () => { + describe('without any input text', () => { + beforeEach(() => { + wrapper = shallowMount(AddIssuableForm, { + propsData: { + inputValue: '', + pendingReferences: [], + pathIdSeparator, + }, + }); + }); + + it('should have disabled submit button', () => { + expect(wrapper.vm.$refs.addButton.disabled).toBe(true); + expect(wrapper.vm.$refs.loadingIcon).toBeUndefined(); + }); + }); + + describe('with input text', () => { + beforeEach(() => { + wrapper = shallowMount(AddIssuableForm, { + propsData: { + inputValue: 'foo', + pendingReferences: [], + pathIdSeparator, + }, + }); + }); + + it('should not have disabled submit button', () => { + expect(wrapper.vm.$refs.addButton.disabled).toBe(false); + }); + }); + }); + + describe('with references', () => { + const inputValue = 'foo #123'; + + beforeEach(() => { + wrapper = mount(AddIssuableForm, { + propsData: { + inputValue, + pendingReferences: [issuable1.reference, issuable2.reference], + pathIdSeparator, + }, + }); + }); + + it('should put input value in place', () => { + expect(findFormInput(wrapper).value).toEqual(inputValue); + }); + + it('should render pending issuables items', () => { + expect(wrapper.findAll('.js-add-issuable-form-token-list-item').length).toEqual(2); + }); + + it('should not have disabled submit button', () => { + expect(wrapper.vm.$refs.addButton.disabled).toBe(false); + }); + }); + + describe('when issuable type is "issue"', () => { + beforeEach(() => { + wrapper = mount(AddIssuableForm, { + propsData: { + inputValue: '', + issuableType: issuableTypesMap.ISSUE, + pathIdSeparator, + pendingReferences: [], + }, + }); + }); + + it('does not show radio inputs', () => { + expect(findRadioInputs(wrapper).length).toBe(0); + }); + }); + + describe('when issuable type is "epic"', () => { + beforeEach(() => { + wrapper = shallowMount(AddIssuableForm, { + propsData: { + inputValue: '', + issuableType: issuableTypesMap.EPIC, + pathIdSeparator, + pendingReferences: [], + }, + }); + }); + + it('does not show radio inputs', () => { + expect(findRadioInputs(wrapper).length).toBe(0); + }); + }); + + describe('when it is a Linked Issues form', () => { + beforeEach(() => { + wrapper = mount(AddIssuableForm, { + propsData: { + inputValue: '', + showCategorizedIssues: true, + issuableType: issuableTypesMap.ISSUE, + pathIdSeparator, + pendingReferences: [], + }, + }); + }); + + it('shows radio inputs to allow categorisation of blocking issues', () => { + expect(findRadioInputs(wrapper).length).toBeGreaterThan(0); + }); + + describe('form radio buttons', () => { + let radioInputs; + + beforeEach(() => { + radioInputs = findRadioInputs(wrapper); + }); + + it('shows "relates to" option', () => { + expect(findRadioInput(radioInputs, linkedIssueTypesMap.RELATES_TO)).not.toBeNull(); + }); + + it('shows "blocks" option', () => { + expect(findRadioInput(radioInputs, linkedIssueTypesMap.BLOCKS)).not.toBeNull(); + }); + + it('shows "is blocked by" option', () => { + expect(findRadioInput(radioInputs, linkedIssueTypesMap.IS_BLOCKED_BY)).not.toBeNull(); + }); + + it('shows 3 options in total', () => { + expect(radioInputs.length).toBe(3); + }); + }); + + describe('when the form is submitted', () => { + it('emits an event with a "relates_to" link type when the "relates to" radio input selected', done => { + jest.spyOn(wrapper.vm, '$emit').mockImplementation(() => {}); + + wrapper.vm.linkedIssueType = linkedIssueTypesMap.RELATES_TO; + wrapper.vm.onFormSubmit(); + + wrapper.vm.$nextTick(() => { + expect(wrapper.vm.$emit).toHaveBeenCalledWith('addIssuableFormSubmit', { + pendingReferences: '', + linkedIssueType: linkedIssueTypesMap.RELATES_TO, + }); + done(); + }); + }); + + it('emits an event with a "blocks" link type when the "blocks" radio input selected', done => { + jest.spyOn(wrapper.vm, '$emit').mockImplementation(() => {}); + + wrapper.vm.linkedIssueType = linkedIssueTypesMap.BLOCKS; + wrapper.vm.onFormSubmit(); + + wrapper.vm.$nextTick(() => { + expect(wrapper.vm.$emit).toHaveBeenCalledWith('addIssuableFormSubmit', { + pendingReferences: '', + linkedIssueType: linkedIssueTypesMap.BLOCKS, + }); + done(); + }); + }); + + it('emits an event with a "is_blocked_by" link type when the "is blocked by" radio input selected', done => { + jest.spyOn(wrapper.vm, '$emit').mockImplementation(() => {}); + + wrapper.vm.linkedIssueType = linkedIssueTypesMap.IS_BLOCKED_BY; + wrapper.vm.onFormSubmit(); + + wrapper.vm.$nextTick(() => { + expect(wrapper.vm.$emit).toHaveBeenCalledWith('addIssuableFormSubmit', { + pendingReferences: '', + linkedIssueType: linkedIssueTypesMap.IS_BLOCKED_BY, + }); + done(); + }); + }); + + it('shows error message when error is present', done => { + const itemAddFailureMessage = 'Something went wrong while submitting.'; + wrapper.setProps({ + hasError: true, + itemAddFailureMessage, + }); + + wrapper.vm.$nextTick(() => { + expect(wrapper.find('.gl-field-error').exists()).toBe(true); + expect(wrapper.find('.gl-field-error').text()).toContain(itemAddFailureMessage); + done(); + }); + }); + }); + }); + }); + + describe('computed', () => { + describe('transformedAutocompleteSources', () => { + const autoCompleteSources = { + issues: 'http://localhost/autocomplete/issues', + epics: 'http://localhost/autocomplete/epics', + }; + + it('returns autocomplete object', () => { + wrapper = constructWrapper({ + autoCompleteSources, + }); + + expect(wrapper.vm.transformedAutocompleteSources).toBe(autoCompleteSources); + + wrapper = constructWrapper({ + autoCompleteSources, + confidential: false, + }); + + expect(wrapper.vm.transformedAutocompleteSources).toBe(autoCompleteSources); + }); + + it('returns autocomplete sources with query `confidential_only`, when it is confidential', () => { + wrapper = constructWrapper({ + autoCompleteSources, + confidential: true, + }); + + const actualSources = wrapper.vm.transformedAutocompleteSources; + + expect(actualSources.epics).toContain('?confidential_only=true'); + expect(actualSources.issues).toContain('?confidential_only=true'); + }); + }); + }); +}); diff --git a/spec/frontend/issuable/related_issues/components/issue_token_spec.js b/spec/frontend/issuable/related_issues/components/issue_token_spec.js new file mode 100644 index 00000000000..553721fa783 --- /dev/null +++ b/spec/frontend/issuable/related_issues/components/issue_token_spec.js @@ -0,0 +1,241 @@ +import Vue from 'vue'; +import { PathIdSeparator } from '~/related_issues/constants'; +import issueToken from '~/related_issues/components/issue_token.vue'; + +describe('IssueToken', () => { + const idKey = 200; + const displayReference = 'foo/bar#123'; + const title = 'some title'; + const pathIdSeparator = PathIdSeparator.Issue; + const eventNamespace = 'pendingIssuable'; + let IssueToken; + let vm; + + beforeEach(() => { + IssueToken = Vue.extend(issueToken); + }); + + afterEach(() => { + if (vm) { + vm.$destroy(); + } + }); + + describe('with reference supplied', () => { + beforeEach(() => { + vm = new IssueToken({ + propsData: { + idKey, + eventNamespace, + displayReference, + pathIdSeparator, + }, + }).$mount(); + }); + + it('shows reference', () => { + expect(vm.$el.textContent.trim()).toEqual(displayReference); + }); + + it('does not link without path specified', () => { + expect(vm.$refs.link.tagName.toLowerCase()).toEqual('span'); + expect(vm.$refs.link.getAttribute('href')).toBeNull(); + }); + }); + + describe('with reference and title supplied', () => { + beforeEach(() => { + vm = new IssueToken({ + propsData: { + idKey, + eventNamespace, + displayReference, + pathIdSeparator, + title, + }, + }).$mount(); + }); + + it('shows reference and title', () => { + expect(vm.$refs.reference.textContent.trim()).toEqual(displayReference); + expect(vm.$refs.title.textContent.trim()).toEqual(title); + }); + }); + + describe('with path supplied', () => { + const path = '/foo/bar/issues/123'; + beforeEach(() => { + vm = new IssueToken({ + propsData: { + idKey, + eventNamespace, + displayReference, + pathIdSeparator, + title, + path, + }, + }).$mount(); + }); + + it('links reference and title', () => { + expect(vm.$refs.link.getAttribute('href')).toEqual(path); + }); + }); + + describe('with state supplied', () => { + describe("`state: 'opened'`", () => { + beforeEach(() => { + vm = new IssueToken({ + propsData: { + idKey, + eventNamespace, + displayReference, + pathIdSeparator, + state: 'opened', + }, + }).$mount(); + }); + + it('shows green circle icon', () => { + expect(vm.$el.querySelector('.issue-token-state-icon-open.fa.fa-circle-o')).toBeDefined(); + }); + }); + + describe("`state: 'reopened'`", () => { + beforeEach(() => { + vm = new IssueToken({ + propsData: { + idKey, + eventNamespace, + displayReference, + pathIdSeparator, + state: 'reopened', + }, + }).$mount(); + }); + + it('shows green circle icon', () => { + expect(vm.$el.querySelector('.issue-token-state-icon-open.fa.fa-circle-o')).toBeDefined(); + }); + }); + + describe("`state: 'closed'`", () => { + beforeEach(() => { + vm = new IssueToken({ + propsData: { + idKey, + eventNamespace, + displayReference, + pathIdSeparator, + state: 'closed', + }, + }).$mount(); + }); + + it('shows red minus icon', () => { + expect(vm.$el.querySelector('.issue-token-state-icon-closed.fa.fa-minus')).toBeDefined(); + }); + }); + }); + + describe('with reference, title, state', () => { + const state = 'opened'; + beforeEach(() => { + vm = new IssueToken({ + propsData: { + idKey, + eventNamespace, + displayReference, + pathIdSeparator, + title, + state, + }, + }).$mount(); + }); + + it('shows reference, title, and state', () => { + const stateIcon = vm.$refs.reference.querySelector('svg'); + + expect(stateIcon.getAttribute('aria-label')).toEqual(state); + expect(vm.$refs.reference.textContent.trim()).toEqual(displayReference); + expect(vm.$refs.title.textContent.trim()).toEqual(title); + }); + }); + + describe('with canRemove', () => { + describe('`canRemove: false` (default)', () => { + beforeEach(() => { + vm = new IssueToken({ + propsData: { + idKey, + eventNamespace, + displayReference, + pathIdSeparator, + }, + }).$mount(); + }); + + it('does not have remove button', () => { + expect(vm.$el.querySelector('.issue-token-remove-button')).toBeNull(); + }); + }); + + describe('`canRemove: true`', () => { + beforeEach(() => { + vm = new IssueToken({ + propsData: { + idKey, + eventNamespace, + displayReference, + pathIdSeparator, + canRemove: true, + }, + }).$mount(); + }); + + it('has remove button', () => { + expect(vm.$el.querySelector('.issue-token-remove-button')).toBeDefined(); + }); + }); + }); + + describe('methods', () => { + beforeEach(() => { + vm = new IssueToken({ + propsData: { + idKey, + eventNamespace, + displayReference, + pathIdSeparator, + }, + }).$mount(); + }); + + it('when getting checked', () => { + jest.spyOn(vm, '$emit').mockImplementation(() => {}); + vm.onRemoveRequest(); + + expect(vm.$emit).toHaveBeenCalledWith('pendingIssuableRemoveRequest', vm.idKey); + }); + }); + + describe('tooltip', () => { + beforeEach(() => { + vm = new IssueToken({ + propsData: { + idKey, + eventNamespace, + displayReference, + pathIdSeparator, + canRemove: true, + }, + }).$mount(); + }); + + it('should not be escaped', () => { + const { originalTitle } = vm.$refs.removeButton.dataset; + + expect(originalTitle).toEqual(`Remove ${displayReference}`); + }); + }); +}); diff --git a/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js b/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js new file mode 100644 index 00000000000..0f88e4d71fe --- /dev/null +++ b/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js @@ -0,0 +1,206 @@ +import { shallowMount, mount } from '@vue/test-utils'; +import { GlButton, GlIcon } from '@gitlab/ui'; +import { + issuable1, + issuable2, + issuable3, +} from 'jest/vue_shared/components/issue/related_issuable_mock_data'; +import RelatedIssuesBlock from '~/related_issues/components/related_issues_block.vue'; +import { + linkedIssueTypesMap, + linkedIssueTypesTextMap, + PathIdSeparator, +} from '~/related_issues/constants'; + +describe('RelatedIssuesBlock', () => { + let wrapper; + + const findIssueCountBadgeAddButton = () => wrapper.find(GlButton); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('with defaults', () => { + beforeEach(() => { + wrapper = mount(RelatedIssuesBlock, { + propsData: { + pathIdSeparator: PathIdSeparator.Issue, + issuableType: 'issue', + }, + }); + }); + + it('displays "Linked issues" in the header', () => { + expect(wrapper.find('.card-title').text()).toContain('Linked issues'); + }); + + it('unable to add new related issues', () => { + expect(findIssueCountBadgeAddButton().exists()).toBe(false); + }); + + it('add related issues form is hidden', () => { + expect(wrapper.find('.js-add-related-issues-form-area').exists()).toBe(false); + }); + }); + + describe('with headerText slot', () => { + it('displays header text slot data', () => { + const headerText = '<div>custom header text</div>'; + + wrapper = shallowMount(RelatedIssuesBlock, { + propsData: { + pathIdSeparator: PathIdSeparator.Issue, + issuableType: 'issue', + }, + slots: { headerText }, + }); + + expect(wrapper.find('.card-title').html()).toContain(headerText); + }); + }); + + describe('with headerActions slot', () => { + it('displays header actions slot data', () => { + const headerActions = '<button data-testid="custom-button">custom button</button>'; + + wrapper = shallowMount(RelatedIssuesBlock, { + propsData: { + pathIdSeparator: PathIdSeparator.Issue, + issuableType: 'issue', + }, + slots: { headerActions }, + }); + + expect(wrapper.find('[data-testid="custom-button"]').html()).toBe(headerActions); + }); + }); + + describe('with isFetching=true', () => { + beforeEach(() => { + wrapper = mount(RelatedIssuesBlock, { + propsData: { + pathIdSeparator: PathIdSeparator.Issue, + isFetching: true, + issuableType: 'issue', + }, + }); + }); + + it('should show `...` badge count', () => { + expect(wrapper.vm.badgeLabel).toBe('...'); + }); + }); + + describe('with canAddRelatedIssues=true', () => { + beforeEach(() => { + wrapper = mount(RelatedIssuesBlock, { + propsData: { + pathIdSeparator: PathIdSeparator.Issue, + canAdmin: true, + issuableType: 'issue', + }, + }); + }); + + it('can add new related issues', () => { + expect(findIssueCountBadgeAddButton().exists()).toBe(true); + }); + }); + + describe('with isFormVisible=true', () => { + beforeEach(() => { + wrapper = mount(RelatedIssuesBlock, { + propsData: { + pathIdSeparator: PathIdSeparator.Issue, + isFormVisible: true, + issuableType: 'issue', + }, + }); + }); + + it('shows add related issues form', () => { + expect(wrapper.find('.js-add-related-issues-form-area').exists()).toBe(true); + }); + }); + + describe('showCategorizedIssues prop', () => { + const issueList = () => wrapper.findAll('.js-related-issues-token-list-item'); + const categorizedHeadings = () => wrapper.findAll('h4'); + const headingTextAt = index => + categorizedHeadings() + .at(index) + .text(); + const mountComponent = showCategorizedIssues => { + wrapper = mount(RelatedIssuesBlock, { + propsData: { + pathIdSeparator: PathIdSeparator.Issue, + relatedIssues: [issuable1, issuable2, issuable3], + issuableType: 'issue', + showCategorizedIssues, + }, + }); + }; + + describe('when showCategorizedIssues=true', () => { + beforeEach(() => mountComponent(true)); + + it('should render issue tokens items', () => { + expect(issueList()).toHaveLength(3); + }); + + it('shows "Blocks" heading', () => { + const blocks = linkedIssueTypesTextMap[linkedIssueTypesMap.BLOCKS]; + + expect(headingTextAt(0)).toBe(blocks); + }); + + it('shows "Is blocked by" heading', () => { + const isBlockedBy = linkedIssueTypesTextMap[linkedIssueTypesMap.IS_BLOCKED_BY]; + + expect(headingTextAt(1)).toBe(isBlockedBy); + }); + + it('shows "Relates to" heading', () => { + const relatesTo = linkedIssueTypesTextMap[linkedIssueTypesMap.RELATES_TO]; + + expect(headingTextAt(2)).toBe(relatesTo); + }); + }); + + describe('when showCategorizedIssues=false', () => { + it('should render issues as a flat list with no header', () => { + mountComponent(false); + + expect(issueList()).toHaveLength(3); + expect(categorizedHeadings()).toHaveLength(0); + }); + }); + }); + + describe('renders correct icon when', () => { + [ + { + icon: 'issues', + issuableType: 'issue', + }, + { + icon: 'epic', + issuableType: 'epic', + }, + ].forEach(({ issuableType, icon }) => { + it(`issuableType=${issuableType} is passed`, () => { + wrapper = shallowMount(RelatedIssuesBlock, { + propsData: { + pathIdSeparator: PathIdSeparator.Issue, + issuableType, + }, + }); + + const iconComponent = wrapper.find(GlIcon); + expect(iconComponent.exists()).toBe(true); + expect(iconComponent.props('name')).toBe(icon); + }); + }); + }); +}); diff --git a/spec/frontend/issuable/related_issues/components/related_issues_list_spec.js b/spec/frontend/issuable/related_issues/components/related_issues_list_spec.js new file mode 100644 index 00000000000..6cf0b9d21ea --- /dev/null +++ b/spec/frontend/issuable/related_issues/components/related_issues_list_spec.js @@ -0,0 +1,190 @@ +import { mount, shallowMount } from '@vue/test-utils'; +import { + issuable1, + issuable2, + issuable3, + issuable4, + issuable5, +} from 'jest/vue_shared/components/issue/related_issuable_mock_data'; +import IssueDueDate from '~/boards/components/issue_due_date.vue'; +import RelatedIssuesList from '~/related_issues/components/related_issues_list.vue'; +import { PathIdSeparator } from '~/related_issues/constants'; + +describe('RelatedIssuesList', () => { + let wrapper; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('with defaults', () => { + const heading = 'Related to'; + + beforeEach(() => { + wrapper = shallowMount(RelatedIssuesList, { + propsData: { + pathIdSeparator: PathIdSeparator.Issue, + issuableType: 'issue', + heading, + }, + }); + }); + + it('shows a heading', () => { + expect(wrapper.find('h4').text()).toContain(heading); + }); + + it('should not show loading icon', () => { + expect(wrapper.vm.$refs.loadingIcon).toBeUndefined(); + }); + }); + + describe('with isFetching=true', () => { + beforeEach(() => { + wrapper = shallowMount(RelatedIssuesList, { + propsData: { + pathIdSeparator: PathIdSeparator.Issue, + isFetching: true, + issuableType: 'issue', + }, + }); + }); + + it('should show loading icon', () => { + expect(wrapper.vm.$refs.loadingIcon).toBeDefined(); + }); + }); + + describe('methods', () => { + beforeEach(() => { + wrapper = shallowMount(RelatedIssuesList, { + propsData: { + pathIdSeparator: PathIdSeparator.Issue, + relatedIssues: [issuable1, issuable2, issuable3, issuable4, issuable5], + issuableType: 'issue', + }, + }); + }); + + it('updates the order correctly when an item is moved to the top', () => { + const beforeAfterIds = wrapper.vm.getBeforeAfterId( + wrapper.vm.$el.querySelector('ul li:first-child'), + ); + + expect(beforeAfterIds.beforeId).toBeNull(); + expect(beforeAfterIds.afterId).toBe(2); + }); + + it('updates the order correctly when an item is moved to the bottom', () => { + const beforeAfterIds = wrapper.vm.getBeforeAfterId( + wrapper.vm.$el.querySelector('ul li:last-child'), + ); + + expect(beforeAfterIds.beforeId).toBe(4); + expect(beforeAfterIds.afterId).toBeNull(); + }); + + it('updates the order correctly when an item is swapped with adjacent item', () => { + const beforeAfterIds = wrapper.vm.getBeforeAfterId( + wrapper.vm.$el.querySelector('ul li:nth-child(3)'), + ); + + expect(beforeAfterIds.beforeId).toBe(2); + expect(beforeAfterIds.afterId).toBe(4); + }); + + it('updates the order correctly when an item is moved somewhere in the middle', () => { + const beforeAfterIds = wrapper.vm.getBeforeAfterId( + wrapper.vm.$el.querySelector('ul li:nth-child(4)'), + ); + + expect(beforeAfterIds.beforeId).toBe(3); + expect(beforeAfterIds.afterId).toBe(5); + }); + }); + + describe('issuableOrderingId returns correct issuable order id when', () => { + it('issuableType is epic', () => { + wrapper = shallowMount(RelatedIssuesList, { + propsData: { + pathIdSeparator: PathIdSeparator.Issue, + issuableType: 'issue', + }, + }); + + expect(wrapper.vm.issuableOrderingId(issuable1)).toBe(issuable1.epicIssueId); + }); + + it('issuableType is issue', () => { + wrapper = shallowMount(RelatedIssuesList, { + propsData: { + pathIdSeparator: PathIdSeparator.Issue, + issuableType: 'epic', + }, + }); + + expect(wrapper.vm.issuableOrderingId(issuable1)).toBe(issuable1.id); + }); + }); + + describe('renders correct ordering id when', () => { + let relatedIssues; + + beforeAll(() => { + relatedIssues = [issuable1, issuable2, issuable3, issuable4, issuable5]; + }); + + it('issuableType is epic', () => { + wrapper = shallowMount(RelatedIssuesList, { + propsData: { + pathIdSeparator: PathIdSeparator.Issue, + issuableType: 'epic', + relatedIssues, + }, + }); + + const listItems = wrapper.vm.$el.querySelectorAll('.list-item'); + + Array.from(listItems).forEach((item, index) => { + expect(Number(item.dataset.orderingId)).toBe(relatedIssues[index].id); + }); + }); + + it('issuableType is issue', () => { + wrapper = shallowMount(RelatedIssuesList, { + propsData: { + pathIdSeparator: PathIdSeparator.Issue, + issuableType: 'issue', + relatedIssues, + }, + }); + + const listItems = wrapper.vm.$el.querySelectorAll('.list-item'); + + Array.from(listItems).forEach((item, index) => { + expect(Number(item.dataset.orderingId)).toBe(relatedIssues[index].epicIssueId); + }); + }); + }); + + describe('related item contents', () => { + beforeAll(() => { + wrapper = mount(RelatedIssuesList, { + propsData: { + issuableType: 'issue', + pathIdSeparator: PathIdSeparator.Issue, + relatedIssues: [issuable1], + }, + }); + }); + + it('shows due date', () => { + expect( + wrapper + .find(IssueDueDate) + .find('.board-card-info-text') + .text(), + ).toBe('Nov 22, 2010'); + }); + }); +}); diff --git a/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js b/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js new file mode 100644 index 00000000000..2544d0bd030 --- /dev/null +++ b/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js @@ -0,0 +1,341 @@ +import { mount, shallowMount } from '@vue/test-utils'; +import MockAdapter from 'axios-mock-adapter'; +import waitForPromises from 'helpers/wait_for_promises'; +import { + defaultProps, + issuable1, + issuable2, +} from 'jest/vue_shared/components/issue/related_issuable_mock_data'; +import RelatedIssuesRoot from '~/related_issues/components/related_issues_root.vue'; +import relatedIssuesService from '~/related_issues/services/related_issues_service'; +import { linkedIssueTypesMap } from '~/related_issues/constants'; +import axios from '~/lib/utils/axios_utils'; +import { deprecatedCreateFlash as createFlash } from '~/flash'; + +jest.mock('~/flash'); + +describe('RelatedIssuesRoot', () => { + let wrapper; + let mock; + + beforeEach(() => { + mock = new MockAdapter(axios); + mock.onGet(defaultProps.endpoint).reply(200, []); + }); + + afterEach(() => { + mock.restore(); + if (wrapper) { + wrapper.destroy(); + wrapper = null; + } + }); + + const createComponent = (mountFn = mount) => { + wrapper = mountFn(RelatedIssuesRoot, { + propsData: defaultProps, + }); + + // Wait for fetch request `fetchRelatedIssues` to complete before starting to test + return waitForPromises(); + }; + + describe('methods', () => { + describe('onRelatedIssueRemoveRequest', () => { + beforeEach(() => { + jest + .spyOn(relatedIssuesService.prototype, 'fetchRelatedIssues') + .mockReturnValue(Promise.reject()); + + return createComponent().then(() => { + wrapper.vm.store.setRelatedIssues([issuable1]); + }); + }); + + it('remove related issue and succeeds', () => { + mock.onDelete(issuable1.referencePath).reply(200, { issues: [] }); + + wrapper.vm.onRelatedIssueRemoveRequest(issuable1.id); + + return axios.waitForAll().then(() => { + expect(wrapper.vm.state.relatedIssues).toEqual([]); + }); + }); + + it('remove related issue, fails, and restores to related issues', () => { + mock.onDelete(issuable1.referencePath).reply(422, {}); + + wrapper.vm.onRelatedIssueRemoveRequest(issuable1.id); + + return axios.waitForAll().then(() => { + expect(wrapper.vm.state.relatedIssues).toHaveLength(1); + expect(wrapper.vm.state.relatedIssues[0].id).toEqual(issuable1.id); + }); + }); + }); + + describe('onToggleAddRelatedIssuesForm', () => { + beforeEach(() => createComponent(shallowMount)); + + it('toggle related issues form to visible', () => { + wrapper.vm.onToggleAddRelatedIssuesForm(); + + expect(wrapper.vm.isFormVisible).toEqual(true); + }); + + it('show add related issues form to hidden', () => { + wrapper.vm.isFormVisible = true; + + wrapper.vm.onToggleAddRelatedIssuesForm(); + + expect(wrapper.vm.isFormVisible).toEqual(false); + }); + }); + + describe('onPendingIssueRemoveRequest', () => { + beforeEach(() => + createComponent().then(() => { + wrapper.vm.store.setPendingReferences([issuable1.reference]); + }), + ); + + it('remove pending related issue', () => { + expect(wrapper.vm.state.pendingReferences).toHaveLength(1); + + wrapper.vm.onPendingIssueRemoveRequest(0); + + expect(wrapper.vm.state.pendingReferences).toHaveLength(0); + }); + }); + + describe('onPendingFormSubmit', () => { + beforeEach(() => { + jest + .spyOn(relatedIssuesService.prototype, 'fetchRelatedIssues') + .mockReturnValue(Promise.reject()); + + return createComponent().then(() => { + jest.spyOn(wrapper.vm, 'processAllReferences'); + jest.spyOn(wrapper.vm.service, 'addRelatedIssues'); + createFlash.mockClear(); + }); + }); + + it('processes references before submitting', () => { + const input = '#123'; + const linkedIssueType = linkedIssueTypesMap.RELATES_TO; + const emitObj = { + pendingReferences: input, + linkedIssueType, + }; + + wrapper.vm.onPendingFormSubmit(emitObj); + + expect(wrapper.vm.processAllReferences).toHaveBeenCalledWith(input); + expect(wrapper.vm.service.addRelatedIssues).toHaveBeenCalledWith([input], linkedIssueType); + }); + + it('submit zero pending issue as related issue', () => { + wrapper.vm.store.setPendingReferences([]); + wrapper.vm.onPendingFormSubmit({}); + + return waitForPromises().then(() => { + expect(wrapper.vm.state.pendingReferences).toHaveLength(0); + expect(wrapper.vm.state.relatedIssues).toHaveLength(0); + }); + }); + + it('submit pending issue as related issue', () => { + mock.onPost(defaultProps.endpoint).reply(200, { + issuables: [issuable1], + result: { + message: 'something was successfully related', + status: 'success', + }, + }); + + wrapper.vm.store.setPendingReferences([issuable1.reference]); + wrapper.vm.onPendingFormSubmit({}); + + return waitForPromises().then(() => { + expect(wrapper.vm.state.pendingReferences).toHaveLength(0); + expect(wrapper.vm.state.relatedIssues).toHaveLength(1); + expect(wrapper.vm.state.relatedIssues[0].id).toEqual(issuable1.id); + }); + }); + + it('submit multiple pending issues as related issues', () => { + mock.onPost(defaultProps.endpoint).reply(200, { + issuables: [issuable1, issuable2], + result: { + message: 'something was successfully related', + status: 'success', + }, + }); + + wrapper.vm.store.setPendingReferences([issuable1.reference, issuable2.reference]); + wrapper.vm.onPendingFormSubmit({}); + + return waitForPromises().then(() => { + expect(wrapper.vm.state.pendingReferences).toHaveLength(0); + expect(wrapper.vm.state.relatedIssues).toHaveLength(2); + expect(wrapper.vm.state.relatedIssues[0].id).toEqual(issuable1.id); + expect(wrapper.vm.state.relatedIssues[1].id).toEqual(issuable2.id); + }); + }); + + it('displays a message from the backend upon error', () => { + const input = '#123'; + const message = 'error'; + + mock.onPost(defaultProps.endpoint).reply(409, { message }); + wrapper.vm.store.setPendingReferences([issuable1.reference, issuable2.reference]); + + expect(createFlash).not.toHaveBeenCalled(); + wrapper.vm.onPendingFormSubmit(input); + + return waitForPromises().then(() => { + expect(createFlash).toHaveBeenCalledWith(message); + }); + }); + }); + + describe('onPendingFormCancel', () => { + beforeEach(() => + createComponent().then(() => { + wrapper.vm.isFormVisible = true; + wrapper.vm.inputValue = 'foo'; + }), + ); + + it('when canceling and hiding add issuable form', () => { + wrapper.vm.onPendingFormCancel(); + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.isFormVisible).toEqual(false); + expect(wrapper.vm.inputValue).toEqual(''); + expect(wrapper.vm.state.pendingReferences).toHaveLength(0); + }); + }); + }); + + describe('fetchRelatedIssues', () => { + beforeEach(() => createComponent()); + + it('sets isFetching while fetching', () => { + wrapper.vm.fetchRelatedIssues(); + + expect(wrapper.vm.isFetching).toEqual(true); + + return waitForPromises().then(() => { + expect(wrapper.vm.isFetching).toEqual(false); + }); + }); + + it('should fetch related issues', () => { + mock.onGet(defaultProps.endpoint).reply(200, [issuable1, issuable2]); + + wrapper.vm.fetchRelatedIssues(); + + return waitForPromises().then(() => { + expect(wrapper.vm.state.relatedIssues).toHaveLength(2); + expect(wrapper.vm.state.relatedIssues[0].id).toEqual(issuable1.id); + expect(wrapper.vm.state.relatedIssues[1].id).toEqual(issuable2.id); + }); + }); + }); + + describe('onInput', () => { + beforeEach(() => createComponent()); + + it('fill in issue number reference and adds to pending related issues', () => { + const input = '#123 '; + wrapper.vm.onInput({ + untouchedRawReferences: [input.trim()], + touchedReference: input, + }); + + expect(wrapper.vm.state.pendingReferences).toHaveLength(1); + expect(wrapper.vm.state.pendingReferences[0]).toEqual('#123'); + }); + + it('fill in with full reference', () => { + const input = 'asdf/qwer#444 '; + wrapper.vm.onInput({ untouchedRawReferences: [input.trim()], touchedReference: input }); + + expect(wrapper.vm.state.pendingReferences).toHaveLength(1); + expect(wrapper.vm.state.pendingReferences[0]).toEqual('asdf/qwer#444'); + }); + + it('fill in with issue link', () => { + const link = 'http://localhost:3000/foo/bar/issues/111'; + const input = `${link} `; + wrapper.vm.onInput({ untouchedRawReferences: [input.trim()], touchedReference: input }); + + expect(wrapper.vm.state.pendingReferences).toHaveLength(1); + expect(wrapper.vm.state.pendingReferences[0]).toEqual(link); + }); + + it('fill in with multiple references', () => { + const input = 'asdf/qwer#444 #12 '; + wrapper.vm.onInput({ + untouchedRawReferences: input.trim().split(/\s/), + touchedReference: 2, + }); + + expect(wrapper.vm.state.pendingReferences).toHaveLength(2); + expect(wrapper.vm.state.pendingReferences[0]).toEqual('asdf/qwer#444'); + expect(wrapper.vm.state.pendingReferences[1]).toEqual('#12'); + }); + + it('fill in with some invalid things', () => { + const input = 'something random '; + wrapper.vm.onInput({ + untouchedRawReferences: input.trim().split(/\s/), + touchedReference: 2, + }); + + expect(wrapper.vm.state.pendingReferences).toHaveLength(2); + expect(wrapper.vm.state.pendingReferences[0]).toEqual('something'); + expect(wrapper.vm.state.pendingReferences[1]).toEqual('random'); + }); + }); + + describe('onBlur', () => { + beforeEach(() => + createComponent().then(() => { + jest.spyOn(wrapper.vm, 'processAllReferences').mockImplementation(() => {}); + }), + ); + + it('add any references to pending when blurring', () => { + const input = '#123'; + + wrapper.vm.onBlur(input); + + expect(wrapper.vm.processAllReferences).toHaveBeenCalledWith(input); + }); + }); + + describe('processAllReferences', () => { + beforeEach(() => createComponent()); + + it('add valid reference to pending', () => { + const input = '#123'; + wrapper.vm.processAllReferences(input); + + expect(wrapper.vm.state.pendingReferences).toHaveLength(1); + expect(wrapper.vm.state.pendingReferences[0]).toEqual('#123'); + }); + + it('add any valid references to pending', () => { + const input = 'asdf #123'; + wrapper.vm.processAllReferences(input); + + expect(wrapper.vm.state.pendingReferences).toHaveLength(2); + expect(wrapper.vm.state.pendingReferences[0]).toEqual('asdf'); + expect(wrapper.vm.state.pendingReferences[1]).toEqual('#123'); + }); + }); + }); +}); diff --git a/spec/frontend/issuable/related_issues/stores/related_issues_store_spec.js b/spec/frontend/issuable/related_issues/stores/related_issues_store_spec.js new file mode 100644 index 00000000000..ada1c44560f --- /dev/null +++ b/spec/frontend/issuable/related_issues/stores/related_issues_store_spec.js @@ -0,0 +1,111 @@ +import { + issuable1, + issuable2, + issuable3, + issuable4, + issuable5, +} from 'jest/vue_shared/components/issue/related_issuable_mock_data'; +import RelatedIssuesStore from '~/related_issues/stores/related_issues_store'; + +describe('RelatedIssuesStore', () => { + let store; + + beforeEach(() => { + store = new RelatedIssuesStore(); + }); + + describe('setRelatedIssues', () => { + it('defaults to empty array', () => { + expect(store.state.relatedIssues).toEqual([]); + }); + + it('sets issues', () => { + const relatedIssues = [issuable1]; + store.setRelatedIssues(relatedIssues); + + expect(store.state.relatedIssues).toEqual(relatedIssues); + }); + }); + + describe('addRelatedIssues', () => { + it('adds related issues', () => { + store.state.relatedIssues = [issuable1]; + store.addRelatedIssues([issuable2, issuable3]); + + expect(store.state.relatedIssues).toEqual([issuable1, issuable2, issuable3]); + }); + }); + + describe('removeRelatedIssue', () => { + it('removes issue', () => { + store.state.relatedIssues = [issuable1]; + + store.removeRelatedIssue(issuable1); + + expect(store.state.relatedIssues).toEqual([]); + }); + + it('removes issue with multiple in store', () => { + store.state.relatedIssues = [issuable1, issuable2]; + + store.removeRelatedIssue(issuable1); + + expect(store.state.relatedIssues).toEqual([issuable2]); + }); + }); + + describe('updateIssueOrder', () => { + it('updates issue order', () => { + store.state.relatedIssues = [issuable1, issuable2, issuable3, issuable4, issuable5]; + + expect(store.state.relatedIssues[3].id).toBe(issuable4.id); + store.updateIssueOrder(3, 0); + + expect(store.state.relatedIssues[0].id).toBe(issuable4.id); + }); + }); + + describe('setPendingReferences', () => { + it('defaults to empty array', () => { + expect(store.state.pendingReferences).toEqual([]); + }); + + it('sets pending references', () => { + const relatedIssues = [issuable1.reference]; + store.setPendingReferences(relatedIssues); + + expect(store.state.pendingReferences).toEqual(relatedIssues); + }); + }); + + describe('addPendingReferences', () => { + it('adds a reference', () => { + store.state.pendingReferences = [issuable1.reference]; + store.addPendingReferences([issuable2.reference, issuable3.reference]); + + expect(store.state.pendingReferences).toEqual([ + issuable1.reference, + issuable2.reference, + issuable3.reference, + ]); + }); + }); + + describe('removePendingRelatedIssue', () => { + it('removes issue', () => { + store.state.pendingReferences = [issuable1.reference]; + + store.removePendingRelatedIssue(0); + + expect(store.state.pendingReferences).toEqual([]); + }); + + it('removes issue with multiple in store', () => { + store.state.pendingReferences = [issuable1.reference, issuable2.reference]; + + store.removePendingRelatedIssue(0); + + expect(store.state.pendingReferences).toEqual([issuable2.reference]); + }); + }); +}); |