summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/frontend/issuable/components/related_issuable_item_spec.js233
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_root_spec.js367
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js32
-rw-r--r--spec/frontend/work_items/components/work_item_detail_modal_spec.js134
-rw-r--r--spec/frontend/work_items/mock_data.js26
5 files changed, 480 insertions, 312 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/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);
});
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js
index f4e6d43812d..ec2e833552a 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js
@@ -17,6 +17,12 @@ import HistoryItem from '~/vue_shared/components/registry/history_item.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import waitForPromises from 'helpers/wait_for_promises';
import getPackagePipelines from '~/packages_and_registries/package_registry/graphql/queries/get_package_pipelines.query.graphql';
+import Tracking from '~/tracking';
+import {
+ TRACKING_ACTION_CLICK_PIPELINE_LINK,
+ TRACKING_ACTION_CLICK_COMMIT_LINK,
+ TRACKING_LABEL_PACKAGE_HISTORY,
+} from '~/packages_and_registries/package_registry/constants';
Vue.use(VueApollo);
@@ -181,7 +187,6 @@ describe('Package History', () => {
it('link', () => {
const linkElement = findElementLink(element);
const exist = Boolean(link);
-
expect(linkElement.exists()).toBe(exist);
if (exist) {
expect(linkElement.attributes('href')).toBe(link);
@@ -189,4 +194,29 @@ describe('Package History', () => {
});
},
);
+ describe('tracking', () => {
+ let eventSpy;
+ const category = 'UI::Packages';
+
+ beforeEach(() => {
+ mountComponent();
+ eventSpy = jest.spyOn(Tracking, 'event');
+ });
+
+ it('clicking pipeline link tracks the right action', () => {
+ wrapper.vm.trackPipelineClick();
+ expect(eventSpy).toHaveBeenCalledWith(category, TRACKING_ACTION_CLICK_PIPELINE_LINK, {
+ category,
+ label: TRACKING_LABEL_PACKAGE_HISTORY,
+ });
+ });
+
+ it('clicking commit link tracks the right action', () => {
+ wrapper.vm.trackCommitClick();
+ expect(eventSpy).toHaveBeenCalledWith(category, TRACKING_ACTION_CLICK_COMMIT_LINK, {
+ category,
+ label: TRACKING_LABEL_PACKAGE_HISTORY,
+ });
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_detail_modal_spec.js b/spec/frontend/work_items/components/work_item_detail_modal_spec.js
index 70b1261bdb7..01891012f99 100644
--- a/spec/frontend/work_items/components/work_item_detail_modal_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_modal_spec.js
@@ -7,6 +7,13 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import WorkItemDetail from '~/work_items/components/work_item_detail.vue';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
import deleteWorkItemFromTaskMutation from '~/work_items/graphql/delete_task_from_work_item.mutation.graphql';
+import deleteWorkItemMutation from '~/work_items/graphql/delete_work_item.mutation.graphql';
+import {
+ deleteWorkItemFromTaskMutationErrorResponse,
+ deleteWorkItemFromTaskMutationResponse,
+ deleteWorkItemMutationErrorResponse,
+ deleteWorkItemResponse,
+} from '../mock_data';
describe('WorkItemDetailModal component', () => {
let wrapper;
@@ -25,28 +32,38 @@ describe('WorkItemDetailModal component', () => {
},
};
+ const defaultPropsData = {
+ issueGid: 'gid://gitlab/WorkItem/1',
+ workItemId: 'gid://gitlab/WorkItem/2',
+ };
+
const findModal = () => wrapper.findComponent(GlModal);
const findAlert = () => wrapper.findComponent(GlAlert);
const findWorkItemDetail = () => wrapper.findComponent(WorkItemDetail);
- const createComponent = ({ workItemId = '1', issueGid = '2', error = false } = {}) => {
+ const createComponent = ({
+ lockVersion,
+ lineNumberStart,
+ lineNumberEnd,
+ error = false,
+ deleteWorkItemFromTaskMutationHandler = jest
+ .fn()
+ .mockResolvedValue(deleteWorkItemFromTaskMutationResponse),
+ deleteWorkItemMutationHandler = jest.fn().mockResolvedValue(deleteWorkItemResponse),
+ } = {}) => {
const apolloProvider = createMockApollo([
- [
- deleteWorkItemFromTaskMutation,
- jest.fn().mockResolvedValue({
- data: {
- workItemDeleteTask: {
- workItem: { id: 123, descriptionHtml: 'updated work item desc' },
- errors: [],
- },
- },
- }),
- ],
+ [deleteWorkItemFromTaskMutation, deleteWorkItemFromTaskMutationHandler],
+ [deleteWorkItemMutation, deleteWorkItemMutationHandler],
]);
wrapper = shallowMount(WorkItemDetailModal, {
apolloProvider,
- propsData: { workItemId, issueGid },
+ propsData: {
+ ...defaultPropsData,
+ lockVersion,
+ lineNumberStart,
+ lineNumberEnd,
+ },
data() {
return {
error,
@@ -67,8 +84,8 @@ describe('WorkItemDetailModal component', () => {
expect(findWorkItemDetail().props()).toEqual({
isModal: true,
- workItemId: '1',
- workItemParentId: '2',
+ workItemId: defaultPropsData.workItemId,
+ workItemParentId: defaultPropsData.issueGid,
});
});
@@ -109,16 +126,85 @@ describe('WorkItemDetailModal component', () => {
});
describe('delete work item', () => {
- it('emits workItemDeleted and closes modal', async () => {
- createComponent();
- const newDesc = 'updated work item desc';
-
- findWorkItemDetail().vm.$emit('deleteWorkItem');
-
- await waitForPromises();
+ describe('when there is task data', () => {
+ it('emits workItemDeleted and closes modal', async () => {
+ const mutationMock = jest.fn().mockResolvedValue(deleteWorkItemFromTaskMutationResponse);
+ createComponent({
+ lockVersion: 1,
+ lineNumberStart: '3',
+ lineNumberEnd: '3',
+ deleteWorkItemFromTaskMutationHandler: mutationMock,
+ });
+ const newDesc = 'updated work item desc';
+
+ findWorkItemDetail().vm.$emit('deleteWorkItem');
+ await waitForPromises();
+
+ expect(wrapper.emitted('workItemDeleted')).toEqual([[newDesc]]);
+ expect(hideModal).toHaveBeenCalled();
+ expect(mutationMock).toHaveBeenCalledWith({
+ input: {
+ id: defaultPropsData.issueGid,
+ lockVersion: 1,
+ taskData: { id: defaultPropsData.workItemId, lineNumberEnd: 3, lineNumberStart: 3 },
+ },
+ });
+ });
+
+ it.each`
+ errorType | mutationMock | errorMessage
+ ${'an error in the mutation response'} | ${jest.fn().mockResolvedValue(deleteWorkItemFromTaskMutationErrorResponse)} | ${'Error'}
+ ${'a network error'} | ${jest.fn().mockRejectedValue(new Error('GraphQL networkError'))} | ${'GraphQL networkError'}
+ `(
+ 'shows an error message when there is $errorType',
+ async ({ mutationMock, errorMessage }) => {
+ createComponent({
+ lockVersion: 1,
+ lineNumberStart: '3',
+ lineNumberEnd: '3',
+ deleteWorkItemFromTaskMutationHandler: mutationMock,
+ });
+
+ findWorkItemDetail().vm.$emit('deleteWorkItem');
+ await waitForPromises();
+
+ expect(wrapper.emitted('workItemDeleted')).toBeUndefined();
+ expect(hideModal).not.toHaveBeenCalled();
+ expect(findAlert().text()).toBe(errorMessage);
+ },
+ );
+ });
- expect(wrapper.emitted('workItemDeleted')).toEqual([[newDesc]]);
- expect(hideModal).toHaveBeenCalled();
+ describe('when there is no task data', () => {
+ it('emits workItemDeleted and closes modal', async () => {
+ const mutationMock = jest.fn().mockResolvedValue(deleteWorkItemResponse);
+ createComponent({ deleteWorkItemMutationHandler: mutationMock });
+
+ findWorkItemDetail().vm.$emit('deleteWorkItem');
+ await waitForPromises();
+
+ expect(wrapper.emitted('workItemDeleted')).toEqual([[defaultPropsData.workItemId]]);
+ expect(hideModal).toHaveBeenCalled();
+ expect(mutationMock).toHaveBeenCalledWith({ input: { id: defaultPropsData.workItemId } });
+ });
+
+ it.each`
+ errorType | mutationMock | errorMessage
+ ${'an error in the mutation response'} | ${jest.fn().mockResolvedValue(deleteWorkItemMutationErrorResponse)} | ${'Error'}
+ ${'a network error'} | ${jest.fn().mockRejectedValue(new Error('GraphQL networkError'))} | ${'GraphQL networkError'}
+ `(
+ 'shows an error message when there is $errorType',
+ async ({ mutationMock, errorMessage }) => {
+ createComponent({ deleteWorkItemMutationHandler: mutationMock });
+
+ findWorkItemDetail().vm.$emit('deleteWorkItem');
+ await waitForPromises();
+
+ expect(wrapper.emitted('workItemDeleted')).toBeUndefined();
+ expect(hideModal).not.toHaveBeenCalled();
+ expect(findAlert().text()).toBe(errorMessage);
+ },
+ );
});
});
});
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index aa0f7317bd0..7232d588add 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -289,6 +289,32 @@ export const deleteWorkItemFailureResponse = {
],
};
+export const deleteWorkItemMutationErrorResponse = {
+ data: {
+ workItemDelete: {
+ errors: ['Error'],
+ },
+ },
+};
+
+export const deleteWorkItemFromTaskMutationResponse = {
+ data: {
+ workItemDeleteTask: {
+ workItem: { id: 123, descriptionHtml: 'updated work item desc' },
+ errors: [],
+ },
+ },
+};
+
+export const deleteWorkItemFromTaskMutationErrorResponse = {
+ data: {
+ workItemDeleteTask: {
+ workItem: { id: 123, descriptionHtml: 'updated work item desc' },
+ errors: ['Error'],
+ },
+ },
+};
+
export const workItemTitleSubscriptionResponse = {
data: {
issuableTitleUpdated: {