Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/issuable')
-rw-r--r--spec/frontend/issuable/components/related_issuable_item_spec.js233
-rw-r--r--spec/frontend/issuable/popover/components/issue_popover_spec.js62
-rw-r--r--spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js4
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_block_spec.js71
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_root_spec.js367
5 files changed, 416 insertions, 321 deletions
diff --git a/spec/frontend/issuable/components/related_issuable_item_spec.js b/spec/frontend/issuable/components/related_issuable_item_spec.js
index 6b48f83041a..3f9f048605a 100644
--- a/spec/frontend/issuable/components/related_issuable_item_spec.js
+++ b/spec/frontend/issuable/components/related_issuable_item_spec.js
@@ -1,23 +1,25 @@
-import { mount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import { GlIcon, GlLink, GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
import IssueDueDate from '~/boards/components/issue_due_date.vue';
import { formatDate } from '~/lib/utils/datetime_utility';
+import { updateHistory } from '~/lib/utils/url_utility';
+import { __ } from '~/locale';
import RelatedIssuableItem from '~/issuable/components/related_issuable_item.vue';
+import IssueMilestone from '~/issuable/components/issue_milestone.vue';
+import IssueAssignees from '~/issuable/components/issue_assignees.vue';
+import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
import { defaultAssignees, defaultMilestone } from './related_issuable_mock_data';
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ updateHistory: jest.fn(),
+}));
+
describe('RelatedIssuableItem', () => {
let wrapper;
- function mountComponent({ mountMethod = mount, stubs = {}, props = {}, slots = {} } = {}) {
- wrapper = mountMethod(RelatedIssuableItem, {
- propsData: props,
- slots,
- stubs,
- });
- }
-
- const props = {
+ const defaultProps = {
idKey: 1,
displayReference: 'gitlab-org/gitlab-test#1',
pathIdSeparator: '#',
@@ -31,84 +33,94 @@ describe('RelatedIssuableItem', () => {
assignees: defaultAssignees,
eventNamespace: 'relatedIssue',
};
- const slots = {
- dueDate: '<div class="js-due-date-slot"></div>',
- weight: '<div class="js-weight-slot"></div>',
- };
-
- const findRemoveButton = () => wrapper.find({ ref: 'removeButton' });
- const findLockIcon = () => wrapper.find({ ref: 'lockIcon' });
- beforeEach(() => {
- mountComponent({ props, slots });
- });
+ const findIcon = () => wrapper.findComponent(GlIcon);
+ const findIssueDueDate = () => wrapper.findComponent(IssueDueDate);
+ const findLockIcon = () => wrapper.find('[data-testid="lockIcon"]');
+ const findRemoveButton = () => wrapper.findComponent(GlButton);
+ const findTitleLink = () => wrapper.findComponent(GlLink);
+ const findWorkItemDetailModal = () => wrapper.findComponent(WorkItemDetailModal);
+
+ function mountComponent({ data = {}, props = {} } = {}) {
+ wrapper = shallowMount(RelatedIssuableItem, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ data() {
+ return data;
+ },
+ });
+ }
afterEach(() => {
wrapper.destroy();
});
it('contains issuable-info-container class when canReorder is false', () => {
- expect(wrapper.props('canReorder')).toBe(false);
- expect(wrapper.find('.issuable-info-container').exists()).toBe(true);
+ mountComponent({ props: { canReorder: false } });
+
+ expect(wrapper.classes('issuable-info-container')).toBe(true);
});
it('does not render token state', () => {
+ mountComponent();
+
expect(wrapper.find('.text-secondary svg').exists()).toBe(false);
});
it('does not render remove button', () => {
- expect(wrapper.find({ ref: 'removeButton' }).exists()).toBe(false);
+ mountComponent();
+
+ expect(findRemoveButton().exists()).toBe(false);
});
describe('token title', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
it('links to computedPath', () => {
- expect(wrapper.find('.item-title a').attributes('href')).toEqual(wrapper.props('path'));
+ expect(findTitleLink().attributes('href')).toBe(defaultProps.path);
});
it('renders confidential icon', () => {
- expect(wrapper.find('.confidential-icon').exists()).toBe(true);
+ expect(findIcon().attributes('title')).toBe(__('Confidential'));
});
it('renders title', () => {
- expect(wrapper.find('.item-title a').text()).toEqual(props.title);
+ expect(findTitleLink().text()).toBe(defaultProps.title);
});
});
describe('token state', () => {
- const tokenState = () => wrapper.find({ ref: 'iconElementXL' });
-
- beforeEach(() => {
- wrapper.setProps({ state: 'opened' });
- });
-
- it('renders if hasState', () => {
- expect(tokenState().exists()).toBe(true);
- });
-
it('renders state title', () => {
- const stateTitle = tokenState().attributes('title');
- const formattedCreateDate = formatDate(props.createdAt);
+ mountComponent({ props: { state: 'opened' } });
+ const stateTitle = findIcon().attributes('title');
+ const formattedCreateDate = formatDate(defaultProps.createdAt);
expect(stateTitle).toContain('<span class="bold">Created</span>');
expect(stateTitle).toContain(`<span class="text-tertiary">${formattedCreateDate}</span>`);
});
it('renders aria label', () => {
- expect(tokenState().attributes('aria-label')).toEqual('opened');
+ mountComponent({ props: { state: 'opened' } });
+
+ expect(findIcon().attributes('arialabel')).toBe('opened');
});
it('renders open icon when open state', () => {
- expect(tokenState().classes('issue-token-state-icon-open')).toBe(true);
+ mountComponent({ props: { state: 'opened' } });
+
+ expect(findIcon().props('name')).toBe('issue-open-m');
+ expect(findIcon().classes('issue-token-state-icon-open')).toBe(true);
});
- it('renders close icon when close state', async () => {
- wrapper.setProps({
- state: 'closed',
- closedAt: '2018-12-01T00:00:00.00Z',
- });
- await nextTick();
+ it('renders close icon when close state', () => {
+ mountComponent({ props: { state: 'closed', closedAt: '2018-12-01T00:00:00.00Z' } });
- expect(tokenState().classes('issue-token-state-icon-closed')).toBe(true);
+ expect(findIcon().props('name')).toBe('issue-close');
+ expect(findIcon().classes('issue-token-state-icon-closed')).toBe(true);
});
});
@@ -116,75 +128,66 @@ describe('RelatedIssuableItem', () => {
const tokenMetadata = () => wrapper.find('.item-meta');
it('renders item path and ID', () => {
+ mountComponent();
const pathAndID = tokenMetadata().find('.item-path-id').text();
expect(pathAndID).toContain('gitlab-org/gitlab-test');
expect(pathAndID).toContain('#1');
});
- it('renders milestone icon and name', () => {
- const milestoneIcon = tokenMetadata().find('.item-milestone svg');
- const milestoneTitle = tokenMetadata().find('.item-milestone .milestone-title');
+ it('renders milestone', () => {
+ mountComponent();
- expect(milestoneIcon.attributes('data-testid')).toBe('clock-icon');
- expect(milestoneTitle.text()).toContain('Milestone title');
+ expect(wrapper.findComponent(IssueMilestone).props('milestone')).toEqual(
+ defaultProps.milestone,
+ );
});
it('renders due date component with correct due date', () => {
- expect(wrapper.find(IssueDueDate).props('date')).toBe(props.dueDate);
+ mountComponent();
+
+ expect(findIssueDueDate().props('date')).toBe(defaultProps.dueDate);
});
- it('does not render red icon for overdue issue that is closed', async () => {
- mountComponent({
- props: {
- ...props,
- closedAt: '2018-12-01T00:00:00.00Z',
- },
- });
- await nextTick();
+ it('does not render red icon for overdue issue that is closed', () => {
+ mountComponent({ props: { closedAt: '2018-12-01T00:00:00.00Z' } });
- expect(wrapper.find(IssueDueDate).props('closed')).toBe(true);
+ expect(findIssueDueDate().props('closed')).toBe(true);
});
});
describe('token assignees', () => {
it('renders assignees avatars', () => {
- // Expect 2 times 2 because assignees are rendered twice, due to layout issues
- expect(wrapper.findAll('.item-assignees .user-avatar-link').length).toBeDefined();
+ mountComponent();
- expect(wrapper.find('.item-assignees .avatar-counter').text()).toContain('+2');
+ expect(wrapper.findComponent(IssueAssignees).props('assignees')).toEqual(
+ defaultProps.assignees,
+ );
});
});
describe('remove button', () => {
beforeEach(() => {
- wrapper.setProps({ canRemove: true });
+ mountComponent({ props: { canRemove: true }, data: { removeDisabled: true } });
});
it('renders if canRemove', () => {
- expect(findRemoveButton().exists()).toBe(true);
+ expect(findRemoveButton().props('icon')).toBe('close');
+ expect(findRemoveButton().attributes('aria-label')).toBe(__('Remove'));
});
it('does not render the lock icon', () => {
expect(findLockIcon().exists()).toBe(false);
});
- it('renders disabled button when removeDisabled', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ removeDisabled: true });
- await nextTick();
-
- expect(findRemoveButton().attributes('disabled')).toEqual('disabled');
+ it('renders disabled button when removeDisabled', () => {
+ expect(findRemoveButton().attributes('disabled')).toBe('true');
});
- it('triggers onRemoveRequest when clicked', async () => {
- findRemoveButton().trigger('click');
- await nextTick();
- const { relatedIssueRemoveRequest } = wrapper.emitted();
+ it('triggers onRemoveRequest when clicked', () => {
+ findRemoveButton().vm.$emit('click');
- expect(relatedIssueRemoveRequest.length).toBe(1);
- expect(relatedIssueRemoveRequest[0]).toEqual([props.idKey]);
+ expect(wrapper.emitted('relatedIssueRemoveRequest')).toEqual([[defaultProps.idKey]]);
});
});
@@ -192,10 +195,7 @@ describe('RelatedIssuableItem', () => {
const lockedMessage = 'Issues created from a vulnerability cannot be removed';
beforeEach(() => {
- wrapper.setProps({
- isLocked: true,
- lockedMessage,
- });
+ mountComponent({ props: { isLocked: true, lockedMessage } });
});
it('does not render the remove button', () => {
@@ -206,4 +206,67 @@ describe('RelatedIssuableItem', () => {
expect(findLockIcon().attributes('title')).toBe(lockedMessage);
});
});
+
+ describe('work item modal', () => {
+ const workItem = 'gid://gitlab/WorkItem/1';
+
+ it('renders', () => {
+ mountComponent();
+
+ expect(findWorkItemDetailModal().props('workItemId')).toBe(workItem);
+ });
+
+ describe('when work item is issue and the related issue title is clicked', () => {
+ it('does not open', () => {
+ mountComponent({ props: { workItemType: 'ISSUE' } });
+ wrapper.vm.$refs.modal.show = jest.fn();
+
+ findTitleLink().vm.$emit('click', { preventDefault: () => {} });
+
+ expect(wrapper.vm.$refs.modal.show).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when work item is task and the related issue title is clicked', () => {
+ beforeEach(() => {
+ mountComponent({ props: { workItemType: 'TASK' } });
+ wrapper.vm.$refs.modal.show = jest.fn();
+ findTitleLink().vm.$emit('click', { preventDefault: () => {} });
+ });
+
+ it('opens', () => {
+ expect(wrapper.vm.$refs.modal.show).toHaveBeenCalled();
+ });
+
+ it('updates the url params with the work item id', () => {
+ expect(updateHistory).toHaveBeenCalledWith({
+ url: `${TEST_HOST}/?work_item_id=1`,
+ replace: true,
+ });
+ });
+ });
+
+ describe('when it emits "workItemDeleted" event', () => {
+ it('emits "relatedIssueRemoveRequest" event', () => {
+ mountComponent();
+
+ findWorkItemDetailModal().vm.$emit('workItemDeleted', workItem);
+
+ expect(wrapper.emitted('relatedIssueRemoveRequest')).toEqual([[workItem]]);
+ });
+ });
+
+ describe('when it emits "close" event', () => {
+ it('removes the work item id from the url params', () => {
+ mountComponent();
+
+ findWorkItemDetailModal().vm.$emit('close');
+
+ expect(updateHistory).toHaveBeenCalledWith({
+ url: `${TEST_HOST}/`,
+ replace: true,
+ });
+ });
+ });
+ });
});
diff --git a/spec/frontend/issuable/popover/components/issue_popover_spec.js b/spec/frontend/issuable/popover/components/issue_popover_spec.js
index 3e77e750f3a..444165f61c7 100644
--- a/spec/frontend/issuable/popover/components/issue_popover_spec.js
+++ b/spec/frontend/issuable/popover/components/issue_popover_spec.js
@@ -1,33 +1,23 @@
-import { GlSkeletonLoader } from '@gitlab/ui';
+import { GlIcon, GlSkeletonLoader } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import issueQueryResponse from 'test_fixtures/graphql/issuable/popover/queries/issue.query.graphql.json';
+import issueQuery from 'ee_else_ce/issuable/popover/queries/issue.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import IssueDueDate from '~/boards/components/issue_due_date.vue';
+import IssueMilestone from '~/issuable/components/issue_milestone.vue';
import StatusBox from '~/issuable/components/status_box.vue';
import IssuePopover from '~/issuable/popover/components/issue_popover.vue';
-import issueQuery from '~/issuable/popover/queries/issue.query.graphql';
+import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
describe('Issue Popover', () => {
let wrapper;
Vue.use(VueApollo);
- const issueQueryResponse = {
- data: {
- project: {
- __typename: 'Project',
- id: '1',
- issue: {
- __typename: 'Issue',
- id: 'gid://gitlab/Issue/1',
- createdAt: '2020-07-01T04:08:01Z',
- state: 'opened',
- title: 'Issue title',
- },
- },
- },
- };
+ const findWorkItemIcon = () => wrapper.findComponent(WorkItemTypeIcon);
const mountComponent = ({
queryResponse = jest.fn().mockResolvedValue(issueQueryResponse),
@@ -53,6 +43,12 @@ describe('Issue Popover', () => {
expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true);
});
+ it('should not show any work item icon while apollo is loading', () => {
+ mountComponent();
+
+ expect(findWorkItemIcon().exists()).toBe(false);
+ });
+
describe('when loaded', () => {
beforeEach(() => {
mountComponent();
@@ -74,8 +70,40 @@ describe('Issue Popover', () => {
expect(wrapper.find('h5').text()).toBe(issueQueryResponse.data.project.issue.title);
});
+ it('shows the work type icon', () => {
+ expect(findWorkItemIcon().props('workItemType')).toBe(
+ issueQueryResponse.data.project.issue.type,
+ );
+ });
+
it('shows reference', () => {
expect(wrapper.text()).toContain('foo/bar#1');
});
+
+ it('shows confidential icon', () => {
+ const icon = wrapper.findComponent(GlIcon);
+
+ expect(icon.exists()).toBe(true);
+ expect(icon.props('name')).toBe('eye-slash');
+ });
+
+ it('shows due date', () => {
+ const component = wrapper.findComponent(IssueDueDate);
+
+ expect(component.exists()).toBe(true);
+ expect(component.props('date')).toBe('2020-07-05');
+ expect(component.props('closed')).toBe(false);
+ });
+
+ it('shows milestone', () => {
+ const component = wrapper.findComponent(IssueMilestone);
+
+ expect(component.exists()).toBe(true);
+ expect(component.props('milestone')).toMatchObject({
+ title: '15.2',
+ startDate: '2020-07-01',
+ dueDate: '2020-07-30',
+ });
+ });
});
});
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
index ce98a16dbb7..16d4459f597 100644
--- a/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js
+++ b/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js
@@ -157,8 +157,8 @@ describe('AddIssuableForm', () => {
describe('categorized issuables', () => {
it.each`
issuableType | pathIdSeparator | contextHeader | contextFooter
- ${issuableTypesMap.ISSUE} | ${PathIdSeparator.Issue} | ${'The current issue'} | ${'the following issue(s)'}
- ${issuableTypesMap.EPIC} | ${PathIdSeparator.Epic} | ${'The current epic'} | ${'the following epic(s)'}
+ ${issuableTypesMap.ISSUE} | ${PathIdSeparator.Issue} | ${'The current issue'} | ${'the following issues'}
+ ${issuableTypesMap.EPIC} | ${PathIdSeparator.Epic} | ${'The current epic'} | ${'the following epics'}
`(
'show header text as "$contextHeader" and footer text as "$contextFooter" issuableType is set to $issuableType',
({ issuableType, contextHeader, contextFooter }) => {
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
index 7a350df0ba6..772cc75a205 100644
--- a/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js
+++ b/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js
@@ -1,5 +1,6 @@
-import { GlButton, GlIcon } from '@gitlab/ui';
-import { shallowMount, mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import { GlIcon } from '@gitlab/ui';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import {
issuable1,
issuable2,
@@ -17,7 +18,9 @@ import {
describe('RelatedIssuesBlock', () => {
let wrapper;
- const findIssueCountBadgeAddButton = () => wrapper.find(GlButton);
+ const findToggleButton = () => wrapper.findByTestId('toggle-links');
+ const findRelatedIssuesBody = () => wrapper.findByTestId('related-issues-body');
+ const findIssueCountBadgeAddButton = () => wrapper.findByTestId('related-issues-plus-button');
afterEach(() => {
if (wrapper) {
@@ -28,7 +31,7 @@ describe('RelatedIssuesBlock', () => {
describe('with defaults', () => {
beforeEach(() => {
- wrapper = mount(RelatedIssuesBlock, {
+ wrapper = mountExtended(RelatedIssuesBlock, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
issuableType: issuableTypesMap.ISSUE,
@@ -37,13 +40,13 @@ describe('RelatedIssuesBlock', () => {
});
it.each`
- issuableType | pathIdSeparator | titleText | helpLinkText | addButtonText
- ${'issue'} | ${PathIdSeparator.Issue} | ${'Linked issues'} | ${'Read more about related issues'} | ${'Add a related issue'}
- ${'epic'} | ${PathIdSeparator.Epic} | ${'Linked epics'} | ${'Read more about related epics'} | ${'Add a related epic'}
+ issuableType | pathIdSeparator | titleText | helpLinkText | addButtonText
+ ${'issue'} | ${PathIdSeparator.Issue} | ${'Linked items'} | ${'Read more about related issues'} | ${'Add a related issue'}
+ ${'epic'} | ${PathIdSeparator.Epic} | ${'Linked epics'} | ${'Read more about related epics'} | ${'Add a related epic'}
`(
'displays "$titleText" in the header, "$helpLinkText" aria-label for help link, and "$addButtonText" aria-label for add button when issuableType is set to "$issuableType"',
({ issuableType, pathIdSeparator, titleText, helpLinkText, addButtonText }) => {
- wrapper = mount(RelatedIssuesBlock, {
+ wrapper = mountExtended(RelatedIssuesBlock, {
propsData: {
pathIdSeparator,
issuableType,
@@ -73,7 +76,7 @@ describe('RelatedIssuesBlock', () => {
it('displays header text slot data', () => {
const headerText = '<div>custom header text</div>';
- wrapper = shallowMount(RelatedIssuesBlock, {
+ wrapper = shallowMountExtended(RelatedIssuesBlock, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
issuableType: 'issue',
@@ -89,7 +92,7 @@ describe('RelatedIssuesBlock', () => {
it('displays header actions slot data', () => {
const headerActions = '<button data-testid="custom-button">custom button</button>';
- wrapper = shallowMount(RelatedIssuesBlock, {
+ wrapper = shallowMountExtended(RelatedIssuesBlock, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
issuableType: 'issue',
@@ -103,7 +106,7 @@ describe('RelatedIssuesBlock', () => {
describe('with isFetching=true', () => {
beforeEach(() => {
- wrapper = mount(RelatedIssuesBlock, {
+ wrapper = mountExtended(RelatedIssuesBlock, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
isFetching: true,
@@ -119,7 +122,7 @@ describe('RelatedIssuesBlock', () => {
describe('with canAddRelatedIssues=true', () => {
beforeEach(() => {
- wrapper = mount(RelatedIssuesBlock, {
+ wrapper = mountExtended(RelatedIssuesBlock, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
canAdmin: true,
@@ -135,7 +138,7 @@ describe('RelatedIssuesBlock', () => {
describe('with isFormVisible=true', () => {
beforeEach(() => {
- wrapper = mount(RelatedIssuesBlock, {
+ wrapper = mountExtended(RelatedIssuesBlock, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
isFormVisible: true,
@@ -159,7 +162,7 @@ describe('RelatedIssuesBlock', () => {
const categorizedHeadings = () => wrapper.findAll('h4');
const headingTextAt = (index) => categorizedHeadings().at(index).text();
const mountComponent = (showCategorizedIssues) => {
- wrapper = mount(RelatedIssuesBlock, {
+ wrapper = mountExtended(RelatedIssuesBlock, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
relatedIssues: [issuable1, issuable2, issuable3],
@@ -217,7 +220,7 @@ describe('RelatedIssuesBlock', () => {
},
].forEach(({ issuableType, icon }) => {
it(`issuableType=${issuableType} is passed`, () => {
- wrapper = shallowMount(RelatedIssuesBlock, {
+ wrapper = shallowMountExtended(RelatedIssuesBlock, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
issuableType,
@@ -230,4 +233,42 @@ describe('RelatedIssuesBlock', () => {
});
});
});
+
+ describe('toggle', () => {
+ beforeEach(() => {
+ wrapper = shallowMountExtended(RelatedIssuesBlock, {
+ propsData: {
+ pathIdSeparator: PathIdSeparator.Issue,
+ relatedIssues: [issuable1, issuable2, issuable3],
+ issuableType: issuableTypesMap.ISSUE,
+ },
+ });
+ });
+
+ it('is expanded by default', () => {
+ expect(findToggleButton().props('icon')).toBe('chevron-lg-up');
+ expect(findToggleButton().props('disabled')).toBe(false);
+ expect(findRelatedIssuesBody().exists()).toBe(true);
+ });
+
+ it('expands on click toggle button', async () => {
+ findToggleButton().vm.$emit('click');
+ await nextTick();
+
+ expect(findToggleButton().props('icon')).toBe('chevron-lg-down');
+ expect(findRelatedIssuesBody().exists()).toBe(false);
+ });
+ });
+
+ it('toggle button is disabled when issue has no related items', () => {
+ wrapper = shallowMountExtended(RelatedIssuesBlock, {
+ propsData: {
+ pathIdSeparator: PathIdSeparator.Issue,
+ relatedIssues: [],
+ issuableType: 'issue',
+ },
+ });
+
+ expect(findToggleButton().props('disabled')).toBe(true);
+ });
});
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
index 1a03ea58b60..b518d2fbdec 100644
--- a/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js
+++ b/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js
@@ -1,4 +1,4 @@
-import { mount, shallowMount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
@@ -9,8 +9,9 @@ import {
} from 'jest/issuable/components/related_issuable_mock_data';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import RelatedIssuesRoot from '~/related_issues/components/related_issues_root.vue';
import { linkedIssueTypesMap } from '~/related_issues/constants';
+import RelatedIssuesBlock from '~/related_issues/components/related_issues_block.vue';
+import RelatedIssuesRoot from '~/related_issues/components/related_issues_root.vue';
import relatedIssuesService from '~/related_issues/services/related_issues_service';
jest.mock('~/flash');
@@ -19,6 +20,8 @@ describe('RelatedIssuesRoot', () => {
let wrapper;
let mock;
+ const findRelatedIssuesBlock = () => wrapper.findComponent(RelatedIssuesBlock);
+
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet(defaultProps.endpoint).reply(200, []);
@@ -26,100 +29,114 @@ describe('RelatedIssuesRoot', () => {
afterEach(() => {
mock.restore();
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
+ wrapper.destroy();
});
- const createComponent = (mountFn = mount) => {
- wrapper = mountFn(RelatedIssuesRoot, {
- propsData: defaultProps,
+ const createComponent = ({ props = {}, data = {} } = {}) => {
+ wrapper = mount(RelatedIssuesRoot, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ data() {
+ return data;
+ },
});
// 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(() => {
+ describe('events', () => {
+ describe('when "relatedIssueRemoveRequest" event is emitted', () => {
+ describe('when emitted value is a numerical issue', () => {
+ beforeEach(async () => {
+ jest
+ .spyOn(relatedIssuesService.prototype, 'fetchRelatedIssues')
+ .mockReturnValue(Promise.reject());
+ await createComponent();
wrapper.vm.store.setRelatedIssues([issuable1]);
});
- });
- it('remove related issue and succeeds', () => {
- mock.onDelete(issuable1.referencePath).reply(200, { issues: [] });
+ it('removes related issue on API success', async () => {
+ mock.onDelete(issuable1.referencePath).reply(200, { issues: [] });
+
+ findRelatedIssuesBlock().vm.$emit('relatedIssueRemoveRequest', issuable1.id);
+ await axios.waitForAll();
+
+ expect(findRelatedIssuesBlock().props('relatedIssues')).toEqual([]);
+ });
+
+ it('does not remove related issue on API error', async () => {
+ mock.onDelete(issuable1.referencePath).reply(422, {});
- wrapper.vm.onRelatedIssueRemoveRequest(issuable1.id);
+ findRelatedIssuesBlock().vm.$emit('relatedIssueRemoveRequest', issuable1.id);
+ await axios.waitForAll();
- return axios.waitForAll().then(() => {
- expect(wrapper.vm.state.relatedIssues).toEqual([]);
+ expect(findRelatedIssuesBlock().props('relatedIssues')).toEqual([
+ expect.objectContaining({ id: issuable1.id }),
+ ]);
});
});
- it('remove related issue, fails, and restores to related issues', () => {
- mock.onDelete(issuable1.referencePath).reply(422, {});
+ describe('when emitted value is a work item id', () => {
+ it('removes related issue', async () => {
+ const workItem = `gid://gitlab/WorkItem/${issuable1.id}`;
+ createComponent({ data: { state: { relatedIssues: [issuable1] } } });
- wrapper.vm.onRelatedIssueRemoveRequest(issuable1.id);
+ findRelatedIssuesBlock().vm.$emit('relatedIssueRemoveRequest', workItem);
+ await nextTick();
- return axios.waitForAll().then(() => {
- expect(wrapper.vm.state.relatedIssues).toHaveLength(1);
- expect(wrapper.vm.state.relatedIssues[0].id).toEqual(issuable1.id);
+ expect(findRelatedIssuesBlock().props('relatedIssues')).toEqual([]);
});
});
});
- describe('onToggleAddRelatedIssuesForm', () => {
- beforeEach(() => createComponent(shallowMount));
+ describe('when "toggleAddRelatedIssuesForm" event is emitted', () => {
+ it('toggles related issues form to visible from hidden', async () => {
+ createComponent();
- it('toggle related issues form to visible', () => {
- wrapper.vm.onToggleAddRelatedIssuesForm();
+ findRelatedIssuesBlock().vm.$emit('toggleAddRelatedIssuesForm');
+ await nextTick();
- expect(wrapper.vm.isFormVisible).toEqual(true);
+ expect(findRelatedIssuesBlock().props('isFormVisible')).toBe(true);
});
- it('show add related issues form to hidden', () => {
- wrapper.vm.isFormVisible = true;
+ it('toggles related issues form to hidden from visible', async () => {
+ createComponent({ data: { isFormVisible: true } });
- wrapper.vm.onToggleAddRelatedIssuesForm();
+ findRelatedIssuesBlock().vm.$emit('toggleAddRelatedIssuesForm');
+ await nextTick();
- expect(wrapper.vm.isFormVisible).toEqual(false);
+ expect(findRelatedIssuesBlock().props('isFormVisible')).toBe(false);
});
});
- describe('onPendingIssueRemoveRequest', () => {
- beforeEach(() =>
- createComponent().then(() => {
- wrapper.vm.store.setPendingReferences([issuable1.reference]);
- }),
- );
+ describe('when "pendingIssuableRemoveRequest" event is emitted', () => {
+ beforeEach(() => {
+ createComponent();
+ wrapper.vm.store.setPendingReferences([issuable1.reference]);
+ });
- it('remove pending related issue', () => {
- expect(wrapper.vm.state.pendingReferences).toHaveLength(1);
+ it('removes pending related issue', async () => {
+ expect(findRelatedIssuesBlock().props('pendingReferences')).toHaveLength(1);
- wrapper.vm.onPendingIssueRemoveRequest(0);
+ findRelatedIssuesBlock().vm.$emit('pendingIssuableRemoveRequest', 0);
+ await nextTick();
- expect(wrapper.vm.state.pendingReferences).toHaveLength(0);
+ expect(findRelatedIssuesBlock().props('pendingReferences')).toHaveLength(0);
});
});
- describe('onPendingFormSubmit', () => {
- beforeEach(() => {
+ describe('when "addIssuableFormSubmit" event is emitted', () => {
+ beforeEach(async () => {
jest
.spyOn(relatedIssuesService.prototype, 'fetchRelatedIssues')
.mockReturnValue(Promise.reject());
-
- return createComponent().then(() => {
- jest.spyOn(wrapper.vm, 'processAllReferences');
- jest.spyOn(wrapper.vm.service, 'addRelatedIssues');
- createFlash.mockClear();
- });
+ await createComponent();
+ jest.spyOn(wrapper.vm, 'processAllReferences');
+ jest.spyOn(wrapper.vm.service, 'addRelatedIssues');
+ createFlash.mockClear();
});
it('processes references before submitting', () => {
@@ -130,23 +147,22 @@ describe('RelatedIssuesRoot', () => {
linkedIssueType,
};
- wrapper.vm.onPendingFormSubmit(emitObj);
+ findRelatedIssuesBlock().vm.$emit('addIssuableFormSubmit', emitObj);
expect(wrapper.vm.processAllReferences).toHaveBeenCalledWith(input);
expect(wrapper.vm.service.addRelatedIssues).toHaveBeenCalledWith([input], linkedIssueType);
});
- it('submit zero pending issue as related issue', () => {
+ it('submits zero pending issues 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);
- });
+ findRelatedIssuesBlock().vm.$emit('addIssuableFormSubmit', {});
+
+ expect(findRelatedIssuesBlock().props('pendingReferences')).toHaveLength(0);
+ expect(findRelatedIssuesBlock().props('relatedIssues')).toHaveLength(0);
});
- it('submit pending issue as related issue', () => {
+ it('submits pending issue as related issue', async () => {
mock.onPost(defaultProps.endpoint).reply(200, {
issuables: [issuable1],
result: {
@@ -154,18 +170,18 @@ describe('RelatedIssuesRoot', () => {
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);
- });
+ findRelatedIssuesBlock().vm.$emit('addIssuableFormSubmit', {});
+ await waitForPromises();
+
+ expect(findRelatedIssuesBlock().props('pendingReferences')).toHaveLength(0);
+ expect(findRelatedIssuesBlock().props('relatedIssues')).toEqual([
+ expect.objectContaining({ id: issuable1.id }),
+ ]);
});
- it('submit multiple pending issues as related issues', () => {
+ it('submits multiple pending issues as related issues', async () => {
mock.onPost(defaultProps.endpoint).reply(200, {
issuables: [issuable1, issuable2],
result: {
@@ -173,201 +189,148 @@ describe('RelatedIssuesRoot', () => {
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);
- });
+ findRelatedIssuesBlock().vm.$emit('addIssuableFormSubmit', {});
+ await waitForPromises();
+
+ expect(findRelatedIssuesBlock().props('pendingReferences')).toHaveLength(0);
+ expect(findRelatedIssuesBlock().props('relatedIssues')).toEqual([
+ expect.objectContaining({ id: issuable1.id }),
+ expect.objectContaining({ id: issuable2.id }),
+ ]);
});
- it('displays a message from the backend upon error', () => {
+ it('displays a message from the backend upon error', async () => {
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', async () => {
- wrapper.vm.onPendingFormCancel();
+ findRelatedIssuesBlock().vm.$emit('addIssuableFormSubmit', input);
+ await waitForPromises();
- await nextTick();
- expect(wrapper.vm.isFormVisible).toEqual(false);
- expect(wrapper.vm.inputValue).toEqual('');
- expect(wrapper.vm.state.pendingReferences).toHaveLength(0);
+ expect(createFlash).toHaveBeenCalledWith({ message });
});
});
- describe('fetchRelatedIssues', () => {
- beforeEach(() => createComponent());
-
- it('sets isFetching while fetching', async () => {
- wrapper.vm.fetchRelatedIssues();
+ describe('when "addIssuableFormCancel" event is emitted', () => {
+ beforeEach(() => createComponent({ data: { isFormVisible: true, inputValue: 'foo' } }));
- expect(wrapper.vm.isFetching).toEqual(true);
+ it('hides form and resets input', async () => {
+ findRelatedIssuesBlock().vm.$emit('addIssuableFormCancel');
+ await nextTick();
- await waitForPromises();
- expect(wrapper.vm.isFetching).toEqual(false);
- });
-
- it('should fetch related issues', async () => {
- mock.onGet(defaultProps.endpoint).reply(200, [issuable1, issuable2]);
-
- wrapper.vm.fetchRelatedIssues();
-
- await waitForPromises();
- 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);
+ expect(findRelatedIssuesBlock().props('isFormVisible')).toBe(false);
+ expect(findRelatedIssuesBlock().props('inputValue')).toBe('');
+ expect(findRelatedIssuesBlock().props('pendingReferences')).toHaveLength(0);
});
});
- describe('onInput', () => {
- beforeEach(() => createComponent());
-
- it('fill in issue number reference and adds to pending related issues', () => {
+ describe('when "addIssuableFormInput" event is emitted', () => {
+ it('updates pending references with issue reference', async () => {
const input = '#123 ';
- wrapper.vm.onInput({
+ createComponent();
+
+ findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
untouchedRawReferences: [input.trim()],
touchedReference: input,
});
+ await nextTick();
- expect(wrapper.vm.state.pendingReferences).toHaveLength(1);
- expect(wrapper.vm.state.pendingReferences[0]).toEqual('#123');
+ expect(findRelatedIssuesBlock().props('pendingReferences')).toEqual([input.trim()]);
});
- it('fill in with full reference', () => {
+ it('updates pending references with full reference', async () => {
const input = 'asdf/qwer#444 ';
- wrapper.vm.onInput({ untouchedRawReferences: [input.trim()], touchedReference: input });
+ createComponent();
+
+ findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
+ untouchedRawReferences: [input.trim()],
+ touchedReference: input,
+ });
+ await nextTick();
- expect(wrapper.vm.state.pendingReferences).toHaveLength(1);
- expect(wrapper.vm.state.pendingReferences[0]).toEqual('asdf/qwer#444');
+ expect(findRelatedIssuesBlock().props('pendingReferences')).toEqual([input.trim()]);
});
- it('fill in with issue link', () => {
+ it('updates pending references with issue link', async () => {
const link = 'http://localhost:3000/foo/bar/issues/111';
const input = `${link} `;
- wrapper.vm.onInput({ untouchedRawReferences: [input.trim()], touchedReference: input });
+ createComponent();
- expect(wrapper.vm.state.pendingReferences).toHaveLength(1);
- expect(wrapper.vm.state.pendingReferences[0]).toEqual(link);
+ findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
+ untouchedRawReferences: [input.trim()],
+ touchedReference: input,
+ });
+ await nextTick();
+
+ expect(findRelatedIssuesBlock().props('pendingReferences')).toEqual([link]);
});
- it('fill in with multiple references', () => {
+ it('updates pending references with multiple references', async () => {
const input = 'asdf/qwer#444 #12 ';
- wrapper.vm.onInput({
+ createComponent();
+
+ findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
untouchedRawReferences: input.trim().split(/\s/),
touchedReference: '2',
});
+ await nextTick();
- 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');
+ expect(findRelatedIssuesBlock().props('pendingReferences')).toEqual([
+ 'asdf/qwer#444',
+ '#12',
+ ]);
});
- it('fill in with some invalid things', () => {
+ it('updates pending references with invalid values', async () => {
const input = 'something random ';
- wrapper.vm.onInput({
+ createComponent();
+
+ findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
untouchedRawReferences: input.trim().split(/\s/),
touchedReference: '2',
});
+ await nextTick();
- expect(wrapper.vm.state.pendingReferences).toHaveLength(2);
- expect(wrapper.vm.state.pendingReferences[0]).toEqual('something');
- expect(wrapper.vm.state.pendingReferences[1]).toEqual('random');
+ expect(findRelatedIssuesBlock().props('pendingReferences')).toEqual([
+ 'something',
+ 'random',
+ ]);
});
- it.each`
- pathIdSeparator
- ${'#'}
- ${'&'}
- `(
- 'prepends $pathIdSeparator when user enters a numeric value [0-9]',
- async ({ pathIdSeparator }) => {
+ it.each(['#', '&'])(
+ 'prepends %s when user enters a numeric value [0-9]',
+ async (pathIdSeparator) => {
const input = '23';
+ createComponent({ props: { pathIdSeparator } });
- await wrapper.setProps({
- pathIdSeparator,
- });
-
- wrapper.vm.onInput({
+ findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
untouchedRawReferences: input.trim().split(/\s/),
touchedReference: input,
});
+ await nextTick();
- expect(wrapper.vm.inputValue).toBe(`${pathIdSeparator}${input}`);
+ expect(findRelatedIssuesBlock().props('inputValue')).toBe(`${pathIdSeparator}${input}`);
},
);
-
- it('prepends # when user enters a number', async () => {
- const input = 23;
-
- wrapper.vm.onInput({
- untouchedRawReferences: String(input).trim().split(/\s/),
- touchedReference: input,
- });
-
- expect(wrapper.vm.inputValue).toBe(`#${input}`);
- });
});
- 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('when "addIssuableFormBlur" event is emitted', () => {
+ beforeEach(() => {
+ createComponent();
+ jest.spyOn(wrapper.vm, 'processAllReferences').mockImplementation(() => {});
});
- });
-
- describe('processAllReferences', () => {
- beforeEach(() => createComponent());
- it('add valid reference to pending', () => {
+ it('adds any references to pending when blurring', () => {
const input = '#123';
- wrapper.vm.processAllReferences(input);
- expect(wrapper.vm.state.pendingReferences).toHaveLength(1);
- expect(wrapper.vm.state.pendingReferences[0]).toEqual('#123');
- });
+ findRelatedIssuesBlock().vm.$emit('addIssuableFormBlur', input);
- 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');
+ expect(wrapper.vm.processAllReferences).toHaveBeenCalledWith(input);
});
});
});