summaryrefslogtreecommitdiff
path: root/spec/frontend/work_items/components
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/work_items/components')
-rw-r--r--spec/frontend/work_items/components/item_state_spec.js16
-rw-r--r--spec/frontend/work_items/components/item_title_spec.js2
-rw-r--r--spec/frontend/work_items/components/work_item_actions_spec.js85
-rw-r--r--spec/frontend/work_items/components/work_item_assignees_spec.js120
-rw-r--r--spec/frontend/work_items/components/work_item_detail_modal_spec.js134
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js87
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js119
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_spec.js205
-rw-r--r--spec/frontend/work_items/components/work_item_state_spec.js16
-rw-r--r--spec/frontend/work_items/components/work_item_title_spec.js21
-rw-r--r--spec/frontend/work_items/components/work_item_type_icon_spec.js47
-rw-r--r--spec/frontend/work_items/components/work_item_weight_spec.js87
12 files changed, 711 insertions, 228 deletions
diff --git a/spec/frontend/work_items/components/item_state_spec.js b/spec/frontend/work_items/components/item_state_spec.js
index 79b76f3c061..c3cc2fbc556 100644
--- a/spec/frontend/work_items/components/item_state_spec.js
+++ b/spec/frontend/work_items/components/item_state_spec.js
@@ -1,3 +1,4 @@
+import { GlFormSelect } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { STATE_OPEN, STATE_CLOSED } from '~/work_items/constants';
import ItemState from '~/work_items/components/item_state.vue';
@@ -6,6 +7,7 @@ describe('ItemState', () => {
let wrapper;
const findLabel = () => wrapper.find('label').text();
+ const findFormSelect = () => wrapper.findComponent(GlFormSelect);
const selectedValue = () => wrapper.find('option:checked').element.value;
const clickOpen = () => wrapper.findAll('option').at(0).setSelected();
@@ -51,4 +53,18 @@ describe('ItemState', () => {
expect(wrapper.emitted('changed')).toBeUndefined();
});
+
+ describe('form select disabled prop', () => {
+ describe.each`
+ description | disabled | value
+ ${'when not disabled'} | ${false} | ${undefined}
+ ${'when disabled'} | ${true} | ${'disabled'}
+ `('$description', ({ disabled, value }) => {
+ it(`renders form select component with disabled=${value}`, () => {
+ createComponent({ disabled });
+
+ expect(findFormSelect().attributes('disabled')).toBe(value);
+ });
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/item_title_spec.js b/spec/frontend/work_items/components/item_title_spec.js
index a55f448c9a2..de20369eb1b 100644
--- a/spec/frontend/work_items/components/item_title_spec.js
+++ b/spec/frontend/work_items/components/item_title_spec.js
@@ -37,7 +37,7 @@ describe('ItemTitle', () => {
disabled: true,
});
- expect(wrapper.classes()).toContain('gl-cursor-not-allowed');
+ expect(wrapper.classes()).toContain('gl-cursor-text');
expect(findInputEl().attributes('contenteditable')).toBe('false');
});
diff --git a/spec/frontend/work_items/components/work_item_actions_spec.js b/spec/frontend/work_items/components/work_item_actions_spec.js
index 137a0a7326d..a1f1d47ab90 100644
--- a/spec/frontend/work_items/components/work_item_actions_spec.js
+++ b/spec/frontend/work_items/components/work_item_actions_spec.js
@@ -1,5 +1,5 @@
-import { GlDropdownItem, GlModal } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { GlModal } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import WorkItemActions from '~/work_items/components/work_item_actions.vue';
describe('WorkItemActions component', () => {
@@ -7,12 +7,19 @@ describe('WorkItemActions component', () => {
let glModalDirective;
const findModal = () => wrapper.findComponent(GlModal);
- const findDeleteButton = () => wrapper.findComponent(GlDropdownItem);
+ const findConfidentialityToggleButton = () =>
+ wrapper.findByTestId('confidentiality-toggle-action');
+ const findDeleteButton = () => wrapper.findByTestId('delete-action');
- const createComponent = ({ canDelete = true } = {}) => {
+ const createComponent = ({
+ canUpdate = true,
+ canDelete = true,
+ isConfidential = false,
+ isParentConfidential = false,
+ } = {}) => {
glModalDirective = jest.fn();
- wrapper = shallowMount(WorkItemActions, {
- propsData: { workItemId: '123', canDelete },
+ wrapper = shallowMountExtended(WorkItemActions, {
+ propsData: { workItemId: '123', canUpdate, canDelete, isConfidential, isParentConfidential },
directives: {
glModal: {
bind(_, { value }) {
@@ -34,27 +41,69 @@ describe('WorkItemActions component', () => {
expect(findModal().props('visible')).toBe(false);
});
- it('shows confirm modal when clicking Delete work item', () => {
+ it('renders dropdown actions', () => {
createComponent();
- findDeleteButton().vm.$emit('click');
-
- expect(glModalDirective).toHaveBeenCalled();
+ expect(findConfidentialityToggleButton().exists()).toBe(true);
+ expect(findDeleteButton().exists()).toBe(true);
});
- it('emits event when clicking OK button', () => {
- createComponent();
+ describe('toggle confidentiality action', () => {
+ it.each`
+ isConfidential | buttonText
+ ${true} | ${'Turn off confidentiality'}
+ ${false} | ${'Turn on confidentiality'}
+ `(
+ 'renders confidentiality toggle button with text "$buttonText"',
+ ({ isConfidential, buttonText }) => {
+ createComponent({ isConfidential });
+
+ expect(findConfidentialityToggleButton().text()).toBe(buttonText);
+ },
+ );
+
+ it('emits `toggleWorkItemConfidentiality` event when clicked', () => {
+ createComponent();
- findModal().vm.$emit('ok');
+ findConfidentialityToggleButton().vm.$emit('click');
- expect(wrapper.emitted('deleteWorkItem')).toEqual([[]]);
+ expect(wrapper.emitted('toggleWorkItemConfidentiality')[0]).toEqual([true]);
+ });
+
+ it.each`
+ props | propName | value
+ ${{ isParentConfidential: true }} | ${'isParentConfidential'} | ${true}
+ ${{ canUpdate: false }} | ${'canUpdate'} | ${false}
+ `('does not render when $propName is $value', ({ props }) => {
+ createComponent(props);
+
+ expect(findConfidentialityToggleButton().exists()).toBe(false);
+ });
});
- it('does not render when canDelete is false', () => {
- createComponent({
- canDelete: false,
+ describe('delete action', () => {
+ it('shows confirm modal when clicked', () => {
+ createComponent();
+
+ findDeleteButton().vm.$emit('click');
+
+ expect(glModalDirective).toHaveBeenCalled();
+ });
+
+ it('emits event when clicking OK button', () => {
+ createComponent();
+
+ findModal().vm.$emit('ok');
+
+ expect(wrapper.emitted('deleteWorkItem')).toEqual([[]]);
});
- expect(wrapper.html()).toBe('');
+ it('does not render when canDelete is false', () => {
+ createComponent({
+ canDelete: false,
+ });
+
+ expect(wrapper.findByTestId('delete-action').exists()).toBe(false);
+ });
});
});
diff --git a/spec/frontend/work_items/components/work_item_assignees_spec.js b/spec/frontend/work_items/components/work_item_assignees_spec.js
index 299949a4baa..f0ef8aee7a9 100644
--- a/spec/frontend/work_items/components/work_item_assignees_spec.js
+++ b/spec/frontend/work_items/components/work_item_assignees_spec.js
@@ -5,14 +5,15 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { mockTracking } from 'helpers/tracking_helper';
-import { stripTypenames } from 'helpers/graphql_helpers';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
import currentUserQuery from '~/graphql_shared/queries/current_user.query.graphql';
+import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
+import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue';
import { i18n, TASK_TYPE_NAME, TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
-import { temporaryConfig, resolvers } from '~/work_items/graphql/provider';
+import { temporaryConfig } from '~/work_items/graphql/provider';
import {
projectMembersResponseWithCurrentUser,
mockAssignees,
@@ -20,6 +21,7 @@ import {
currentUserResponse,
currentUserNullResponse,
projectMembersResponseWithoutCurrentUser,
+ updateWorkItemMutationResponse,
} from '../mock_data';
Vue.use(VueApollo);
@@ -33,6 +35,7 @@ describe('WorkItemAssignees component', () => {
const findAssigneeLinks = () => wrapper.findAllComponents(GlLink);
const findTokenSelector = () => wrapper.findComponent(GlTokenSelector);
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
+ const findInviteMembersTrigger = () => wrapper.findComponent(InviteMembersTrigger);
const findEmptyState = () => wrapper.findByTestId('empty-state');
const findAssignSelfButton = () => wrapper.findByTestId('assign-self');
@@ -43,6 +46,9 @@ describe('WorkItemAssignees component', () => {
.mockResolvedValue(projectMembersResponseWithCurrentUser);
const successCurrentUserQueryHandler = jest.fn().mockResolvedValue(currentUserResponse);
const noCurrentUserQueryHandler = jest.fn().mockResolvedValue(currentUserNullResponse);
+ const successUpdateWorkItemMutationHandler = jest
+ .fn()
+ .mockResolvedValue(updateWorkItemMutationResponse);
const errorHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
@@ -50,15 +56,18 @@ describe('WorkItemAssignees component', () => {
assignees = mockAssignees,
searchQueryHandler = successSearchQueryHandler,
currentUserQueryHandler = successCurrentUserQueryHandler,
+ updateWorkItemMutationHandler = successUpdateWorkItemMutationHandler,
allowsMultipleAssignees = true,
+ canInviteMembers = false,
canUpdate = true,
} = {}) => {
const apolloProvider = createMockApollo(
[
[userSearchQuery, searchQueryHandler],
[currentUserQuery, currentUserQueryHandler],
+ [updateWorkItemMutation, updateWorkItemMutationHandler],
],
- resolvers,
+ {},
{
typePolicies: temporaryConfig.cacheConfig.typePolicies,
},
@@ -82,6 +91,7 @@ describe('WorkItemAssignees component', () => {
allowsMultipleAssignees,
workItemType: TASK_TYPE_NAME,
canUpdate,
+ canInviteMembers,
},
attachTo: document.body,
apolloProvider,
@@ -120,15 +130,6 @@ describe('WorkItemAssignees component', () => {
expect(findTokenSelector().element.contains(document.activeElement)).toBe(true);
});
- it('calls a mutation on clicking outside the token selector', async () => {
- createComponent();
- findTokenSelector().vm.$emit('input', [mockAssignees[0]]);
- findTokenSelector().vm.$emit('blur', new FocusEvent({ relatedTarget: null }));
- await waitForPromises();
-
- expect(findTokenSelector().props('selectedTokens')).toEqual([mockAssignees[0]]);
- });
-
it('passes `false` to `viewOnly` token selector prop if user can update assignees', () => {
createComponent();
@@ -141,6 +142,36 @@ describe('WorkItemAssignees component', () => {
expect(findTokenSelector().props('viewOnly')).toBe(true);
});
+ describe('when clicking outside the token selector', () => {
+ function arrange(args) {
+ createComponent(args);
+ findTokenSelector().vm.$emit('input', [mockAssignees[0]]);
+ findTokenSelector().vm.$emit('blur', new FocusEvent({ relatedTarget: null }));
+ }
+
+ it('calls a mutation with correct variables', () => {
+ arrange({ assignees: [] });
+
+ expect(successUpdateWorkItemMutationHandler).toHaveBeenCalledWith({
+ input: {
+ assigneesWidget: { assigneeIds: [mockAssignees[0].id] },
+ id: 'gid://gitlab/WorkItem/1',
+ },
+ });
+ });
+
+ it('emits an error and resets assignees if mutation was rejected', async () => {
+ arrange({ updateWorkItemMutationHandler: errorHandler, assignees: [mockAssignees[1]] });
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[i18n.updateError]]);
+ expect(findTokenSelector().props('selectedTokens')).toEqual([
+ { ...mockAssignees[1], class: expect.anything() },
+ ]);
+ });
+ });
+
describe('when searching for users', () => {
beforeEach(() => {
createComponent();
@@ -204,7 +235,7 @@ describe('WorkItemAssignees component', () => {
expect(findTokenSelector().props('dropdownItems')).toHaveLength(2);
});
- it('should search for users with correct key after text input', async () => {
+ it('searches for users with correct key after text input', async () => {
const searchKey = 'Hello';
findTokenSelector().vm.$emit('focus');
@@ -225,6 +256,18 @@ describe('WorkItemAssignees component', () => {
expect(wrapper.emitted('error')).toEqual([[i18n.fetchError]]);
});
+ it('updates localAssignees when assignees prop is updated', async () => {
+ createComponent({ assignees: [] });
+
+ expect(findTokenSelector().props('selectedTokens')).toEqual([]);
+
+ await wrapper.setProps({ assignees: [mockAssignees[0]] });
+
+ expect(findTokenSelector().props('selectedTokens')).toEqual([
+ { ...mockAssignees[0], class: expect.anything() },
+ ]);
+ });
+
describe('when assigning to current user', () => {
it('does not show `Assign myself` button if current user is loading', () => {
createComponent();
@@ -261,23 +304,21 @@ describe('WorkItemAssignees component', () => {
expect(findAssignSelfButton().exists()).toBe(true);
});
- it('calls update work item assignees mutation with current user as a variable on button click', () => {
- // TODO: replace this test as soon as we have a real mutation implemented
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockImplementation(jest.fn());
-
+ it('calls update work item assignees mutation with current user as a variable on button click', async () => {
+ const { currentUser } = currentUserResponse.data;
findTokenSelector().trigger('mouseover');
findAssignSelfButton().vm.$emit('click', new MouseEvent('click'));
+ await nextTick();
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith(
- expect.objectContaining({
- variables: {
- input: {
- assignees: [stripTypenames(currentUserResponse.data.currentUser)],
- id: workItemId,
- },
+ expect(findTokenSelector().props('selectedTokens')).toMatchObject([currentUser]);
+ expect(successUpdateWorkItemMutationHandler).toHaveBeenCalledWith({
+ input: {
+ id: workItemId,
+ assigneesWidget: {
+ assigneeIds: [currentUser.id],
},
- }),
- );
+ },
+ });
});
});
@@ -286,9 +327,7 @@ describe('WorkItemAssignees component', () => {
await waitForPromises();
expect(findTokenSelector().props('dropdownItems')[0]).toEqual(
- expect.objectContaining({
- ...stripTypenames(currentUserResponse.data.currentUser),
- }),
+ expect.objectContaining(currentUserResponse.data.currentUser),
);
});
@@ -303,9 +342,10 @@ describe('WorkItemAssignees component', () => {
});
it('adds current user to the top of dropdown items', () => {
- expect(findTokenSelector().props('dropdownItems')[0]).toEqual(
- stripTypenames(currentUserResponse.data.currentUser),
- );
+ expect(findTokenSelector().props('dropdownItems')[0]).toEqual({
+ ...currentUserResponse.data.currentUser,
+ class: expect.anything(),
+ });
});
it('does not add current user if search is not empty', async () => {
@@ -313,7 +353,7 @@ describe('WorkItemAssignees component', () => {
await waitForPromises();
expect(findTokenSelector().props('dropdownItems')[0]).not.toEqual(
- stripTypenames(currentUserResponse.data.currentUser),
+ currentUserResponse.data.currentUser,
);
});
});
@@ -405,4 +445,18 @@ describe('WorkItemAssignees component', () => {
});
});
});
+
+ describe('invite members', () => {
+ it('does not render `Invite members` link if user has no permission to invite members', () => {
+ createComponent();
+
+ expect(findInviteMembersTrigger().exists()).toBe(false);
+ });
+
+ it('renders `Invite members` link if user has a permission to invite members', () => {
+ createComponent({ canInviteMembers: true });
+
+ expect(findInviteMembersTrigger().exists()).toBe(true);
+ });
+ });
});
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/components/work_item_links/work_item_links_form_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js
index 93bf7286aa7..434c1db8a2c 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js
@@ -1,13 +1,20 @@
import Vue from 'vue';
-import { GlForm, GlFormCombobox } from '@gitlab/ui';
+import { GlForm, GlFormInput, GlFormCombobox } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import WorkItemLinksForm from '~/work_items/components/work_item_links/work_item_links_form.vue';
import projectWorkItemsQuery from '~/work_items/graphql/project_work_items.query.graphql';
+import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
+import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
-import { availableWorkItemsResponse, updateWorkItemMutationResponse } from '../../mock_data';
+import {
+ availableWorkItemsResponse,
+ projectWorkItemTypesQueryResponse,
+ createWorkItemMutationResponse,
+ updateWorkItemMutationResponse,
+} from '../../mock_data';
Vue.use(VueApollo);
@@ -15,14 +22,21 @@ describe('WorkItemLinksForm', () => {
let wrapper;
const updateMutationResolver = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
+ const createMutationResolver = jest.fn().mockResolvedValue(createWorkItemMutationResponse);
- const createComponent = async ({ listResponse = availableWorkItemsResponse } = {}) => {
+ const createComponent = async ({
+ listResponse = availableWorkItemsResponse,
+ typesResponse = projectWorkItemTypesQueryResponse,
+ parentConfidential = false,
+ } = {}) => {
wrapper = shallowMountExtended(WorkItemLinksForm, {
apolloProvider: createMockApollo([
[projectWorkItemsQuery, jest.fn().mockResolvedValue(listResponse)],
+ [projectWorkItemTypesQuery, jest.fn().mockResolvedValue(typesResponse)],
[updateWorkItemMutation, updateMutationResolver],
+ [createWorkItemMutation, createMutationResolver],
]),
- propsData: { issuableGid: 'gid://gitlab/WorkItem/1' },
+ propsData: { issuableGid: 'gid://gitlab/WorkItem/1', parentConfidential },
provide: {
projectPath: 'project/path',
},
@@ -33,6 +47,7 @@ describe('WorkItemLinksForm', () => {
const findForm = () => wrapper.findComponent(GlForm);
const findCombobox = () => wrapper.findComponent(GlFormCombobox);
+ const findInput = () => wrapper.findComponent(GlFormInput);
const findAddChildButton = () => wrapper.findByTestId('add-child-button');
beforeEach(async () => {
@@ -47,19 +62,73 @@ describe('WorkItemLinksForm', () => {
expect(findForm().exists()).toBe(true);
});
- it('passes available work items as prop when typing in combobox', async () => {
- findCombobox().vm.$emit('input', 'Task');
+ it('creates child task in non confidential parent', async () => {
+ findInput().vm.$emit('input', 'Create task test');
+
+ findForm().vm.$emit('submit', {
+ preventDefault: jest.fn(),
+ });
await waitForPromises();
+ expect(createMutationResolver).toHaveBeenCalledWith({
+ input: {
+ title: 'Create task test',
+ projectPath: 'project/path',
+ workItemTypeId: 'gid://gitlab/WorkItems::Type/3',
+ hierarchyWidget: {
+ parentId: 'gid://gitlab/WorkItem/1',
+ },
+ confidential: false,
+ },
+ });
+ });
+
+ it('creates child task in confidential parent', async () => {
+ await createComponent({ parentConfidential: true });
+
+ findInput().vm.$emit('input', 'Create confidential task');
- expect(findCombobox().exists()).toBe(true);
- expect(findCombobox().props('tokenList').length).toBe(2);
+ findForm().vm.$emit('submit', {
+ preventDefault: jest.fn(),
+ });
+ await waitForPromises();
+ expect(createMutationResolver).toHaveBeenCalledWith({
+ input: {
+ title: 'Create confidential task',
+ projectPath: 'project/path',
+ workItemTypeId: 'gid://gitlab/WorkItems::Type/3',
+ hierarchyWidget: {
+ parentId: 'gid://gitlab/WorkItem/1',
+ },
+ confidential: true,
+ },
+ });
});
- it('selects and add child', async () => {
+ // Follow up issue to turn this functionality back on https://gitlab.com/gitlab-org/gitlab/-/issues/368757
+ // eslint-disable-next-line jest/no-disabled-tests
+ it.skip('selects and add child', async () => {
findCombobox().vm.$emit('input', availableWorkItemsResponse.data.workspace.workItems.edges[0]);
findAddChildButton().vm.$emit('click');
await waitForPromises();
expect(updateMutationResolver).toHaveBeenCalled();
});
+
+ // eslint-disable-next-line jest/no-disabled-tests
+ describe.skip('when typing in combobox', () => {
+ beforeEach(async () => {
+ findCombobox().vm.$emit('input', 'Task');
+ await waitForPromises();
+ await jest.runOnlyPendingTimers();
+ });
+
+ it('passes available work items as prop', () => {
+ expect(findCombobox().exists()).toBe(true);
+ expect(findCombobox().props('tokenList').length).toBe(2);
+ });
+
+ it('passes action to create task', () => {
+ expect(findCombobox().props('actionList').length).toBe(1);
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js
index f8471b7f167..287ec022d3f 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js
@@ -1,75 +1,24 @@
-import Vue from 'vue';
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { cloneDeep } from 'lodash';
-import VueApollo from 'vue-apollo';
-import createMockApollo from 'helpers/mock_apollo_helper';
+
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import waitForPromises from 'helpers/wait_for_promises';
import WorkItemLinksMenu from '~/work_items/components/work_item_links/work_item_links_menu.vue';
-import changeWorkItemParentMutation from '~/work_items/graphql/change_work_item_parent_link.mutation.graphql';
-import getWorkItemLinksQuery from '~/work_items/graphql/work_item_links.query.graphql';
-import { WIDGET_TYPE_HIERARCHY } from '~/work_items/constants';
-import { workItemHierarchyResponse, changeWorkItemParentMutationResponse } from '../../mock_data';
-
-Vue.use(VueApollo);
-
-const PARENT_ID = 'gid://gitlab/WorkItem/1';
-const WORK_ITEM_ID = 'gid://gitlab/WorkItem/3';
describe('WorkItemLinksMenu', () => {
let wrapper;
- let mockApollo;
-
- const $toast = {
- show: jest.fn(),
- };
-
- const createComponent = async ({
- data = {},
- mutationHandler = jest.fn().mockResolvedValue(changeWorkItemParentMutationResponse),
- } = {}) => {
- mockApollo = createMockApollo([
- [getWorkItemLinksQuery, jest.fn().mockResolvedValue(workItemHierarchyResponse)],
- [changeWorkItemParentMutation, mutationHandler],
- ]);
-
- mockApollo.clients.defaultClient.cache.writeQuery({
- query: getWorkItemLinksQuery,
- variables: {
- id: PARENT_ID,
- },
- data: workItemHierarchyResponse.data,
- });
- wrapper = shallowMountExtended(WorkItemLinksMenu, {
- data() {
- return {
- ...data,
- };
- },
- propsData: {
- workItemId: WORK_ITEM_ID,
- parentWorkItemId: PARENT_ID,
- },
- apolloProvider: mockApollo,
- mocks: {
- $toast,
- },
- });
-
- await waitForPromises();
+ const createComponent = () => {
+ wrapper = shallowMountExtended(WorkItemLinksMenu);
};
const findDropdown = () => wrapper.find(GlDropdown);
const findRemoveDropdownItem = () => wrapper.find(GlDropdownItem);
beforeEach(async () => {
- await createComponent();
+ createComponent();
});
afterEach(() => {
wrapper.destroy();
- mockApollo = null;
});
it('renders dropdown and dropdown items', () => {
@@ -77,65 +26,9 @@ describe('WorkItemLinksMenu', () => {
expect(findRemoveDropdownItem().exists()).toBe(true);
});
- it('calls correct mutation with correct variables', async () => {
- const mutationHandler = jest.fn().mockResolvedValue(changeWorkItemParentMutationResponse);
-
- createComponent({ mutationHandler });
-
- findRemoveDropdownItem().vm.$emit('click');
-
- await waitForPromises();
-
- expect(mutationHandler).toHaveBeenCalledWith({
- id: WORK_ITEM_ID,
- parentId: null,
- });
- });
-
- it('shows toast when mutation succeeds', async () => {
- const mutationHandler = jest.fn().mockResolvedValue(changeWorkItemParentMutationResponse);
-
- createComponent({ mutationHandler });
-
- findRemoveDropdownItem().vm.$emit('click');
-
- await waitForPromises();
-
- expect($toast.show).toHaveBeenCalledWith('Child removed', {
- action: { onClick: expect.anything(), text: 'Undo' },
- });
- });
-
- it('updates the cache when mutation succeeds', async () => {
- const mutationHandler = jest.fn().mockResolvedValue(changeWorkItemParentMutationResponse);
-
- createComponent({ mutationHandler });
-
- mockApollo.clients.defaultClient.cache.readQuery = jest.fn(
- () => workItemHierarchyResponse.data,
- );
-
- mockApollo.clients.defaultClient.cache.writeQuery = jest.fn();
-
+ it('emits removeChild event on click Remove', () => {
findRemoveDropdownItem().vm.$emit('click');
- await waitForPromises();
-
- // Remove the work item from parent's children
- const resp = cloneDeep(workItemHierarchyResponse);
- const index = resp.data.workItem.widgets
- .find((widget) => widget.type === WIDGET_TYPE_HIERARCHY)
- .children.nodes.findIndex((child) => child.id === WORK_ITEM_ID);
- resp.data.workItem.widgets
- .find((widget) => widget.type === WIDGET_TYPE_HIERARCHY)
- .children.nodes.splice(index, 1);
-
- expect(mockApollo.clients.defaultClient.cache.writeQuery).toHaveBeenCalledWith(
- expect.objectContaining({
- query: expect.anything(),
- variables: { id: PARENT_ID },
- data: resp.data,
- }),
- );
+ expect(wrapper.emitted('removeChild')).toHaveLength(1);
});
});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
index 2ec9b1ec0ac..00f508f1548 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
@@ -1,34 +1,85 @@
import Vue, { nextTick } from 'vue';
-import { GlBadge } from '@gitlab/ui';
+import { GlButton, GlIcon, GlAlert } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import SidebarEventHub from '~/sidebar/event_hub';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import WorkItemLinks from '~/work_items/components/work_item_links/work_item_links.vue';
+import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
+import changeWorkItemParentMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import getWorkItemLinksQuery from '~/work_items/graphql/work_item_links.query.graphql';
-import { workItemHierarchyResponse, workItemHierarchyEmptyResponse } from '../../mock_data';
+import {
+ workItemHierarchyResponse,
+ workItemHierarchyEmptyResponse,
+ workItemHierarchyNoUpdatePermissionResponse,
+ changeWorkItemParentMutationResponse,
+ workItemQueryResponse,
+} from '../../mock_data';
Vue.use(VueApollo);
describe('WorkItemLinks', () => {
let wrapper;
+ let mockApollo;
+
+ const WORK_ITEM_ID = 'gid://gitlab/WorkItem/2';
+
+ const $toast = {
+ show: jest.fn(),
+ };
+
+ const mutationChangeParentHandler = jest
+ .fn()
+ .mockResolvedValue(changeWorkItemParentMutationResponse);
+
+ const childWorkItemQueryHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
+
+ const findChildren = () => wrapper.findAll('[data-testid="links-child"]');
+
+ const createComponent = async ({
+ data = {},
+ fetchHandler = jest.fn().mockResolvedValue(workItemHierarchyResponse),
+ mutationHandler = mutationChangeParentHandler,
+ } = {}) => {
+ mockApollo = createMockApollo(
+ [
+ [getWorkItemLinksQuery, fetchHandler],
+ [changeWorkItemParentMutation, mutationHandler],
+ [workItemQuery, childWorkItemQueryHandler],
+ ],
+ {},
+ { addTypename: true },
+ );
- const createComponent = async ({ response = workItemHierarchyResponse } = {}) => {
wrapper = shallowMountExtended(WorkItemLinks, {
- apolloProvider: createMockApollo([
- [getWorkItemLinksQuery, jest.fn().mockResolvedValue(response)],
- ]),
+ data() {
+ return {
+ ...data,
+ };
+ },
+ provide: {
+ projectPath: 'project/path',
+ },
propsData: { issuableId: 1 },
+ apolloProvider: mockApollo,
+ mocks: {
+ $toast,
+ },
});
await waitForPromises();
};
+ const findAlert = () => wrapper.findComponent(GlAlert);
const findToggleButton = () => wrapper.findByTestId('toggle-links');
const findLinksBody = () => wrapper.findByTestId('links-body');
const findEmptyState = () => wrapper.findByTestId('links-empty');
const findToggleAddFormButton = () => wrapper.findByTestId('toggle-add-form');
const findAddLinksForm = () => wrapper.findByTestId('add-links-form');
+ const findFirstLinksMenu = () => wrapper.findByTestId('links-menu');
+ const findChildrenCount = () => wrapper.findByTestId('children-count');
beforeEach(async () => {
await createComponent();
@@ -36,6 +87,7 @@ describe('WorkItemLinks', () => {
afterEach(() => {
wrapper.destroy();
+ mockApollo = null;
});
it('is expanded by default', () => {
@@ -43,7 +95,7 @@ describe('WorkItemLinks', () => {
expect(findLinksBody().exists()).toBe(true);
});
- it('expands on click toggle button', async () => {
+ it('collapses on click toggle button', async () => {
findToggleButton().vm.$emit('click');
await nextTick();
@@ -67,7 +119,9 @@ describe('WorkItemLinks', () => {
describe('when no child links', () => {
beforeEach(async () => {
- await createComponent({ response: workItemHierarchyEmptyResponse });
+ await createComponent({
+ fetchHandler: jest.fn().mockResolvedValue(workItemHierarchyEmptyResponse),
+ });
});
it('displays empty state if there are no children', () => {
@@ -78,9 +132,140 @@ describe('WorkItemLinks', () => {
it('renders all hierarchy widget children', () => {
expect(findLinksBody().exists()).toBe(true);
+ expect(findChildren()).toHaveLength(4);
+ expect(findFirstLinksMenu().exists()).toBe(true);
+ });
+
+ it('shows alert when list loading fails', async () => {
+ const errorMessage = 'Some error';
+ await createComponent({
+ fetchHandler: jest.fn().mockRejectedValue(new Error(errorMessage)),
+ });
+
+ await nextTick();
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(errorMessage);
+ });
+
+ it('renders widget child icon and tooltip', () => {
+ expect(findChildren().at(0).findComponent(GlIcon).props('name')).toBe('issue-open-m');
+ expect(findChildren().at(1).findComponent(GlIcon).props('name')).toBe('issue-close');
+ });
+
+ it('renders confidentiality icon when child item is confidential', () => {
const children = wrapper.findAll('[data-testid="links-child"]');
+ const confidentialIcon = children.at(0).find('[data-testid="confidential-icon"]');
+
+ expect(confidentialIcon.exists()).toBe(true);
+ expect(confidentialIcon.props('name')).toBe('eye-slash');
+ });
+
+ it('displays number if children', () => {
+ expect(findChildrenCount().exists()).toBe(true);
+
+ expect(findChildrenCount().text()).toContain('4');
+ });
+
+ it('refetches child items when `confidentialityUpdated` event is emitted on SidebarEventhub', async () => {
+ const fetchHandler = jest.fn().mockResolvedValue(workItemHierarchyResponse);
+ await createComponent({
+ fetchHandler,
+ });
+ await waitForPromises();
+
+ SidebarEventHub.$emit('confidentialityUpdated');
+ await nextTick();
+
+ // First call is done on component mount.
+ // Second call is done on confidentialityUpdated event.
+ expect(fetchHandler).toHaveBeenCalledTimes(2);
+ });
+
+ describe('when no permission to update', () => {
+ beforeEach(async () => {
+ await createComponent({
+ fetchHandler: jest.fn().mockResolvedValue(workItemHierarchyNoUpdatePermissionResponse),
+ });
+ });
- expect(children).toHaveLength(4);
- expect(children.at(0).findComponent(GlBadge).text()).toBe('Open');
+ it('does not display button to toggle Add form', () => {
+ expect(findToggleAddFormButton().exists()).toBe(false);
+ });
+
+ it('does not display link menu on children', () => {
+ expect(findFirstLinksMenu().exists()).toBe(false);
+ });
+ });
+
+ describe('remove child', () => {
+ beforeEach(async () => {
+ await createComponent({ mutationHandler: mutationChangeParentHandler });
+ });
+
+ it('calls correct mutation with correct variables', async () => {
+ findFirstLinksMenu().vm.$emit('removeChild');
+
+ await waitForPromises();
+
+ expect(mutationChangeParentHandler).toHaveBeenCalledWith({
+ input: {
+ id: WORK_ITEM_ID,
+ hierarchyWidget: {
+ parentId: null,
+ },
+ },
+ });
+ });
+
+ it('shows toast when mutation succeeds', async () => {
+ findFirstLinksMenu().vm.$emit('removeChild');
+
+ await waitForPromises();
+
+ expect($toast.show).toHaveBeenCalledWith('Child removed', {
+ action: { onClick: expect.anything(), text: 'Undo' },
+ });
+ });
+
+ it('renders correct number of children after removal', async () => {
+ expect(findChildren()).toHaveLength(4);
+
+ findFirstLinksMenu().vm.$emit('removeChild');
+ await waitForPromises();
+
+ expect(findChildren()).toHaveLength(3);
+ });
+ });
+
+ describe('prefetching child items', () => {
+ beforeEach(async () => {
+ await createComponent();
+ });
+
+ const findChildLink = () => findChildren().at(0).findComponent(GlButton);
+
+ it('does not fetch the child work item before hovering work item links', () => {
+ expect(childWorkItemQueryHandler).not.toHaveBeenCalled();
+ });
+
+ it('fetches the child work item if link is hovered for 250+ ms', async () => {
+ findChildLink().vm.$emit('mouseover');
+ jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ await waitForPromises();
+
+ expect(childWorkItemQueryHandler).toHaveBeenCalledWith({
+ id: 'gid://gitlab/WorkItem/2',
+ });
+ });
+
+ it('does not fetch the child work item if link is hovered for less than 250 ms', async () => {
+ findChildLink().vm.$emit('mouseover');
+ jest.advanceTimersByTime(200);
+ findChildLink().vm.$emit('mouseout');
+ await waitForPromises();
+
+ expect(childWorkItemQueryHandler).not.toHaveBeenCalled();
+ });
});
});
diff --git a/spec/frontend/work_items/components/work_item_state_spec.js b/spec/frontend/work_items/components/work_item_state_spec.js
index b379d1fc846..6b23a6e4795 100644
--- a/spec/frontend/work_items/components/work_item_state_spec.js
+++ b/spec/frontend/work_items/components/work_item_state_spec.js
@@ -29,6 +29,7 @@ describe('WorkItemState component', () => {
const createComponent = ({
state = STATE_OPEN,
mutationHandler = mutationSuccessHandler,
+ canUpdate = true,
} = {}) => {
const { id, workItemType } = workItemQueryResponse.data.workItem;
wrapper = shallowMount(WorkItemState, {
@@ -39,6 +40,7 @@ describe('WorkItemState component', () => {
state,
workItemType,
},
+ canUpdate,
},
});
};
@@ -53,6 +55,20 @@ describe('WorkItemState component', () => {
expect(findItemState().props('state')).toBe(workItemQueryResponse.data.workItem.state);
});
+ describe('item state disabled prop', () => {
+ describe.each`
+ description | canUpdate | value
+ ${'when cannot update'} | ${false} | ${true}
+ ${'when can update'} | ${true} | ${false}
+ `('$description', ({ canUpdate, value }) => {
+ it(`renders item state component with disabled=${value}`, () => {
+ createComponent({ canUpdate });
+
+ expect(findItemState().props('disabled')).toBe(value);
+ });
+ });
+ });
+
describe('when updating the state', () => {
it('calls a mutation', () => {
createComponent();
diff --git a/spec/frontend/work_items/components/work_item_title_spec.js b/spec/frontend/work_items/components/work_item_title_spec.js
index a48449bb636..c0d966abab8 100644
--- a/spec/frontend/work_items/components/work_item_title_spec.js
+++ b/spec/frontend/work_items/components/work_item_title_spec.js
@@ -20,7 +20,11 @@ describe('WorkItemTitle component', () => {
const findItemTitle = () => wrapper.findComponent(ItemTitle);
- const createComponent = ({ workItemParentId, mutationHandler = mutationSuccessHandler } = {}) => {
+ const createComponent = ({
+ workItemParentId,
+ mutationHandler = mutationSuccessHandler,
+ canUpdate = true,
+ } = {}) => {
const { id, title, workItemType } = workItemQueryResponse.data.workItem;
wrapper = shallowMount(WorkItemTitle, {
apolloProvider: createMockApollo([
@@ -32,6 +36,7 @@ describe('WorkItemTitle component', () => {
workItemTitle: title,
workItemType: workItemType.name,
workItemParentId,
+ canUpdate,
},
});
};
@@ -46,6 +51,20 @@ describe('WorkItemTitle component', () => {
expect(findItemTitle().props('title')).toBe(workItemQueryResponse.data.workItem.title);
});
+ describe('item title disabled prop', () => {
+ describe.each`
+ description | canUpdate | value
+ ${'when cannot update'} | ${false} | ${true}
+ ${'when can update'} | ${true} | ${false}
+ `('$description', ({ canUpdate, value }) => {
+ it(`renders item title component with disabled=${value}`, () => {
+ createComponent({ canUpdate });
+
+ expect(findItemTitle().props('disabled')).toBe(value);
+ });
+ });
+ });
+
describe('when updating the title', () => {
it('calls a mutation', () => {
const title = 'new title!';
diff --git a/spec/frontend/work_items/components/work_item_type_icon_spec.js b/spec/frontend/work_items/components/work_item_type_icon_spec.js
new file mode 100644
index 00000000000..85466578e18
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_type_icon_spec.js
@@ -0,0 +1,47 @@
+import { GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
+
+let wrapper;
+
+function createComponent(propsData) {
+ wrapper = shallowMount(WorkItemTypeIcon, { propsData });
+}
+
+describe('Work Item type component', () => {
+ const findIcon = () => wrapper.findComponent(GlIcon);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe.each`
+ workItemType | workItemIconName | iconName | text
+ ${'TASK'} | ${''} | ${'issue-type-task'} | ${'Task'}
+ ${''} | ${'issue-type-task'} | ${'issue-type-task'} | ${''}
+ ${'ISSUE'} | ${''} | ${'issue-type-issue'} | ${'Issue'}
+ ${''} | ${'issue-type-issue'} | ${'issue-type-issue'} | ${''}
+ ${'REQUIREMENTS'} | ${''} | ${'issue-type-requirements'} | ${'Requirements'}
+ ${'INCIDENT'} | ${''} | ${'issue-type-incident'} | ${'Incident'}
+ ${'TEST_CASE'} | ${''} | ${'issue-type-test-case'} | ${'Test case'}
+ ${'random-issue-type'} | ${''} | ${'issue-type-issue'} | ${''}
+ `(
+ 'with workItemType set to "$workItemType" and workItemIconName set to "$workItemIconName"',
+ ({ workItemType, workItemIconName, iconName, text }) => {
+ beforeEach(() => {
+ createComponent({
+ workItemType,
+ workItemIconName,
+ });
+ });
+
+ it(`renders icon with name '${iconName}'`, () => {
+ expect(findIcon().props('name')).toBe(iconName);
+ });
+
+ it(`renders correct text`, () => {
+ expect(wrapper.text()).toBe(text);
+ });
+ },
+ );
+});
diff --git a/spec/frontend/work_items/components/work_item_weight_spec.js b/spec/frontend/work_items/components/work_item_weight_spec.js
index c3bbea26cda..94bdb336deb 100644
--- a/spec/frontend/work_items/components/work_item_weight_spec.js
+++ b/spec/frontend/work_items/components/work_item_weight_spec.js
@@ -1,16 +1,21 @@
import { GlForm, GlFormInput } from '@gitlab/ui';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
import { mockTracking } from 'helpers/tracking_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import { __ } from '~/locale';
import WorkItemWeight from '~/work_items/components/work_item_weight.vue';
-import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
-import localUpdateWorkItemMutation from '~/work_items/graphql/local_update_work_item.mutation.graphql';
+import { i18n, TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
+import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+import { updateWorkItemMutationResponse } from 'jest/work_items/mock_data';
describe('WorkItemWeight component', () => {
+ Vue.use(VueApollo);
+
let wrapper;
- const mutateSpy = jest.fn();
const workItemId = 'gid://gitlab/WorkItem/1';
const workItemType = 'Task';
@@ -22,8 +27,10 @@ describe('WorkItemWeight component', () => {
hasIssueWeightsFeature = true,
isEditing = false,
weight,
+ mutationHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse),
} = {}) => {
wrapper = mountExtended(WorkItemWeight, {
+ apolloProvider: createMockApollo([[updateWorkItemMutation, mutationHandler]]),
propsData: {
canUpdate,
weight,
@@ -33,11 +40,6 @@ describe('WorkItemWeight component', () => {
provide: {
hasIssueWeightsFeature,
},
- mocks: {
- $apollo: {
- mutate: mutateSpy,
- },
- },
});
if (isEditing) {
@@ -131,26 +133,73 @@ describe('WorkItemWeight component', () => {
});
describe('when blurred', () => {
- it('calls a mutation to update the weight', () => {
- const weight = 0;
- createComponent({ isEditing: true, weight });
+ it('calls a mutation to update the weight when the input value is different', () => {
+ const mutationSpy = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
+ createComponent({
+ isEditing: true,
+ weight: 0,
+ mutationHandler: mutationSpy,
+ canUpdate: true,
+ });
+
+ findInput().vm.$emit('blur', { target: { value: 1 } });
+
+ expect(mutationSpy).toHaveBeenCalledWith({
+ input: {
+ id: workItemId,
+ weightWidget: {
+ weight: 1,
+ },
+ },
+ });
+ });
+
+ it('does not call a mutation to update the weight when the input value is the same', () => {
+ const mutationSpy = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
+ createComponent({ isEditing: true, mutationHandler: mutationSpy, canUpdate: true });
findInput().trigger('blur');
- expect(mutateSpy).toHaveBeenCalledWith({
- mutation: localUpdateWorkItemMutation,
- variables: {
- input: {
- id: workItemId,
- weight,
+ expect(mutationSpy).not.toHaveBeenCalledWith();
+ });
+
+ it('emits an error when there is a GraphQL error', async () => {
+ const response = {
+ data: {
+ workItemUpdate: {
+ errors: ['Error!'],
+ workItem: {},
},
},
+ };
+ createComponent({
+ isEditing: true,
+ mutationHandler: jest.fn().mockResolvedValue(response),
+ canUpdate: true,
+ });
+
+ findInput().trigger('blur');
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[i18n.updateError]]);
+ });
+
+ it('emits an error when there is a network error', async () => {
+ createComponent({
+ isEditing: true,
+ mutationHandler: jest.fn().mockRejectedValue(new Error()),
+ canUpdate: true,
});
+
+ findInput().trigger('blur');
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[i18n.updateError]]);
});
it('tracks updating the weight', () => {
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- createComponent();
+ createComponent({ canUpdate: true });
findInput().trigger('blur');