summaryrefslogtreecommitdiff
path: root/spec/frontend/issuable/related_issues
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-09-19 01:45:44 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-09-19 01:45:44 +0000
commit85dc423f7090da0a52c73eb66faf22ddb20efff9 (patch)
tree9160f299afd8c80c038f08e1545be119f5e3f1e1 /spec/frontend/issuable/related_issues
parent15c2c8c66dbe422588e5411eee7e68f1fa440bb8 (diff)
downloadgitlab-ce-85dc423f7090da0a52c73eb66faf22ddb20efff9.tar.gz
Add latest changes from gitlab-org/gitlab@13-4-stable-ee
Diffstat (limited to 'spec/frontend/issuable/related_issues')
-rw-r--r--spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js289
-rw-r--r--spec/frontend/issuable/related_issues/components/issue_token_spec.js241
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_block_spec.js206
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_list_spec.js190
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_root_spec.js341
-rw-r--r--spec/frontend/issuable/related_issues/stores/related_issues_store_spec.js111
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]);
+ });
+ });
+});