summaryrefslogtreecommitdiff
path: root/spec/frontend/work_items
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-09-19 23:18:09 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-09-19 23:18:09 +0000
commit6ed4ec3e0b1340f96b7c043ef51d1b33bbe85fde (patch)
treedc4d20fe6064752c0bd323187252c77e0a89144b /spec/frontend/work_items
parent9868dae7fc0655bd7ce4a6887d4e6d487690eeed (diff)
downloadgitlab-ce-6ed4ec3e0b1340f96b7c043ef51d1b33bbe85fde.tar.gz
Add latest changes from gitlab-org/gitlab@15-4-stable-eev15.4.0-rc42
Diffstat (limited to 'spec/frontend/work_items')
-rw-r--r--spec/frontend/work_items/components/item_title_spec.js2
-rw-r--r--spec/frontend/work_items/components/work_item_actions_spec.js48
-rw-r--r--spec/frontend/work_items/components/work_item_assignees_spec.js79
-rw-r--r--spec/frontend/work_items/components/work_item_description_spec.js14
-rw-r--r--spec/frontend/work_items/components/work_item_detail_modal_spec.js2
-rw-r--r--spec/frontend/work_items/components/work_item_detail_spec.js (renamed from spec/frontend/work_items/pages/work_item_detail_spec.js)120
-rw-r--r--spec/frontend/work_items/components/work_item_due_date_spec.js346
-rw-r--r--spec/frontend/work_items/components/work_item_information_spec.js9
-rw-r--r--spec/frontend/work_items/components/work_item_labels_spec.js6
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js122
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_spec.js98
-rw-r--r--spec/frontend/work_items/components/work_item_state_spec.js5
-rw-r--r--spec/frontend/work_items/components/work_item_title_spec.js6
-rw-r--r--spec/frontend/work_items/components/work_item_type_icon_spec.js39
-rw-r--r--spec/frontend/work_items/components/work_item_weight_spec.js214
-rw-r--r--spec/frontend/work_items/mock_data.js258
-rw-r--r--spec/frontend/work_items/pages/create_work_item_spec.js4
-rw-r--r--spec/frontend/work_items/router_spec.js39
18 files changed, 1002 insertions, 409 deletions
diff --git a/spec/frontend/work_items/components/item_title_spec.js b/spec/frontend/work_items/components/item_title_spec.js
index de20369eb1b..13e04ef6671 100644
--- a/spec/frontend/work_items/components/item_title_spec.js
+++ b/spec/frontend/work_items/components/item_title_spec.js
@@ -49,6 +49,6 @@ describe('ItemTitle', () => {
findInputEl().element.innerText = mockUpdatedTitle;
await findInputEl().trigger(sourceEvent);
- expect(wrapper.emitted(eventName)).toBeTruthy();
+ expect(wrapper.emitted(eventName)).toBeDefined();
});
});
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 a1f1d47ab90..3c312fb4552 100644
--- a/spec/frontend/work_items/components/work_item_actions_spec.js
+++ b/spec/frontend/work_items/components/work_item_actions_spec.js
@@ -1,15 +1,30 @@
-import { GlModal } from '@gitlab/ui';
+import { GlDropdownDivider, GlModal } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import WorkItemActions from '~/work_items/components/work_item_actions.vue';
+const TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION = 'confidentiality-toggle-action';
+const TEST_ID_DELETE_ACTION = 'delete-action';
+
describe('WorkItemActions component', () => {
let wrapper;
let glModalDirective;
const findModal = () => wrapper.findComponent(GlModal);
const findConfidentialityToggleButton = () =>
- wrapper.findByTestId('confidentiality-toggle-action');
- const findDeleteButton = () => wrapper.findByTestId('delete-action');
+ wrapper.findByTestId(TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION);
+ const findDeleteButton = () => wrapper.findByTestId(TEST_ID_DELETE_ACTION);
+ const findDropdownItems = () => wrapper.findAll('[data-testid="work-item-actions-dropdown"] > *');
+ const findDropdownItemsActual = () =>
+ findDropdownItems().wrappers.map((x) => {
+ if (x.is(GlDropdownDivider)) {
+ return { divider: true };
+ }
+
+ return {
+ testId: x.attributes('data-testid'),
+ text: x.text(),
+ };
+ });
const createComponent = ({
canUpdate = true,
@@ -19,7 +34,14 @@ describe('WorkItemActions component', () => {
} = {}) => {
glModalDirective = jest.fn();
wrapper = shallowMountExtended(WorkItemActions, {
- propsData: { workItemId: '123', canUpdate, canDelete, isConfidential, isParentConfidential },
+ propsData: {
+ workItemId: '123',
+ canUpdate,
+ canDelete,
+ isConfidential,
+ isParentConfidential,
+ workItemType: 'Task',
+ },
directives: {
glModal: {
bind(_, { value }) {
@@ -44,8 +66,19 @@ describe('WorkItemActions component', () => {
it('renders dropdown actions', () => {
createComponent();
- expect(findConfidentialityToggleButton().exists()).toBe(true);
- expect(findDeleteButton().exists()).toBe(true);
+ expect(findDropdownItemsActual()).toEqual([
+ {
+ testId: TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION,
+ text: 'Turn on confidentiality',
+ },
+ {
+ divider: true,
+ },
+ {
+ testId: TEST_ID_DELETE_ACTION,
+ text: 'Delete task',
+ },
+ ]);
});
describe('toggle confidentiality action', () => {
@@ -103,7 +136,8 @@ describe('WorkItemActions component', () => {
canDelete: false,
});
- expect(wrapper.findByTestId('delete-action').exists()).toBe(false);
+ expect(findDeleteButton().exists()).toBe(false);
+ expect(wrapper.findComponent(GlDropdownDivider).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 f0ef8aee7a9..28231fad108 100644
--- a/spec/frontend/work_items/components/work_item_assignees_spec.js
+++ b/spec/frontend/work_items/components/work_item_assignees_spec.js
@@ -1,4 +1,4 @@
-import { GlLink, GlTokenSelector, GlSkeletonLoader } from '@gitlab/ui';
+import { GlLink, GlTokenSelector, GlSkeletonLoader, GlIntersectionObserver } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -8,12 +8,17 @@ import { mockTracking } from 'helpers/tracking_helper';
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 { temporaryConfig } from '~/graphql_shared/issuable_client';
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 } from '~/work_items/graphql/provider';
+import {
+ i18n,
+ TASK_TYPE_NAME,
+ TRACKING_CATEGORY_SHOW,
+ DEFAULT_PAGE_SIZE_ASSIGNEES,
+} from '~/work_items/constants';
import {
projectMembersResponseWithCurrentUser,
mockAssignees,
@@ -22,6 +27,8 @@ import {
currentUserNullResponse,
projectMembersResponseWithoutCurrentUser,
updateWorkItemMutationResponse,
+ projectMembersResponseWithCurrentUserWithNextPage,
+ projectMembersResponseWithNoMatchingUsers,
} from '../mock_data';
Vue.use(VueApollo);
@@ -40,15 +47,25 @@ describe('WorkItemAssignees component', () => {
const findEmptyState = () => wrapper.findByTestId('empty-state');
const findAssignSelfButton = () => wrapper.findByTestId('assign-self');
const findAssigneesTitle = () => wrapper.findByTestId('assignees-title');
+ const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
+
+ const triggerInfiniteScroll = () =>
+ wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
const successSearchQueryHandler = jest
.fn()
.mockResolvedValue(projectMembersResponseWithCurrentUser);
+ const successSearchQueryHandlerWithMoreAssignees = jest
+ .fn()
+ .mockResolvedValue(projectMembersResponseWithCurrentUserWithNextPage);
const successCurrentUserQueryHandler = jest.fn().mockResolvedValue(currentUserResponse);
const noCurrentUserQueryHandler = jest.fn().mockResolvedValue(currentUserNullResponse);
const successUpdateWorkItemMutationHandler = jest
.fn()
.mockResolvedValue(updateWorkItemMutationResponse);
+ const successSearchWithNoMatchingUsers = jest
+ .fn()
+ .mockResolvedValue(projectMembersResponseWithNoMatchingUsers);
const errorHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
@@ -82,9 +99,6 @@ describe('WorkItemAssignees component', () => {
});
wrapper = mountExtended(WorkItemAssignees, {
- provide: {
- fullPath: 'test-project-path',
- },
propsData: {
assignees,
workItemId,
@@ -92,6 +106,7 @@ describe('WorkItemAssignees component', () => {
workItemType: TASK_TYPE_NAME,
canUpdate,
canInviteMembers,
+ fullPath: 'test-project-path',
},
attachTo: document.body,
apolloProvider,
@@ -459,4 +474,56 @@ describe('WorkItemAssignees component', () => {
expect(findInviteMembersTrigger().exists()).toBe(true);
});
});
+
+ describe('load more assignees', () => {
+ it('does not have intersection observer when no matching users', async () => {
+ createComponent({ searchQueryHandler: successSearchWithNoMatchingUsers });
+ findTokenSelector().vm.$emit('focus');
+ await nextTick();
+
+ expect(findSkeletonLoader().exists()).toBe(true);
+
+ await waitForPromises();
+
+ expect(findSkeletonLoader().exists()).toBe(false);
+ expect(findIntersectionObserver().exists()).toBe(false);
+ });
+
+ it('does not trigger load more when does not have next page', async () => {
+ createComponent();
+ findTokenSelector().vm.$emit('focus');
+ await nextTick();
+
+ expect(findSkeletonLoader().exists()).toBe(true);
+
+ await waitForPromises();
+
+ expect(findSkeletonLoader().exists()).toBe(false);
+
+ expect(findIntersectionObserver().exists()).toBe(false);
+ });
+
+ it('triggers load more when there are more users', async () => {
+ createComponent({ searchQueryHandler: successSearchQueryHandlerWithMoreAssignees });
+ findTokenSelector().vm.$emit('focus');
+ await nextTick();
+
+ expect(findSkeletonLoader().exists()).toBe(true);
+
+ await waitForPromises();
+
+ expect(findSkeletonLoader().exists()).toBe(false);
+ expect(findIntersectionObserver().exists()).toBe(true);
+
+ triggerInfiniteScroll();
+
+ expect(successSearchQueryHandlerWithMoreAssignees).toHaveBeenCalledWith({
+ first: DEFAULT_PAGE_SIZE_ASSIGNEES,
+ after:
+ projectMembersResponseWithCurrentUserWithNextPage.data.workspace.users.pageInfo.endCursor,
+ search: '',
+ fullPath: 'test-project-path',
+ });
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_description_spec.js b/spec/frontend/work_items/components/work_item_description_spec.js
index 8017c46dea8..d3165d8dc26 100644
--- a/spec/frontend/work_items/components/work_item_description_spec.js
+++ b/spec/frontend/work_items/components/work_item_description_spec.js
@@ -10,9 +10,9 @@ import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import WorkItemDescription from '~/work_items/components/work_item_description.vue';
import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
-import updateWorkItemWidgetsMutation from '~/work_items/graphql/update_work_item_widgets.mutation.graphql';
+import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import {
- updateWorkItemWidgetsResponse,
+ updateWorkItemMutationResponse,
workItemResponseFactory,
workItemQueryResponse,
} from '../mock_data';
@@ -31,7 +31,7 @@ describe('WorkItemDescription', () => {
Vue.use(VueApollo);
- const mutationSuccessHandler = jest.fn().mockResolvedValue(updateWorkItemWidgetsResponse);
+ const mutationSuccessHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
const findEditButton = () => wrapper.find('[data-testid="edit-description"]');
const findMarkdownField = () => wrapper.findComponent(MarkdownField);
@@ -53,13 +53,11 @@ describe('WorkItemDescription', () => {
wrapper = shallowMount(WorkItemDescription, {
apolloProvider: createMockApollo([
[workItemQuery, workItemResponseHandler],
- [updateWorkItemWidgetsMutation, mutationHandler],
+ [updateWorkItemMutation, mutationHandler],
]),
propsData: {
workItemId: id,
- },
- provide: {
- fullPath: '/group/project',
+ fullPath: 'test-project-path',
},
stubs: {
MarkdownField,
@@ -175,7 +173,7 @@ describe('WorkItemDescription', () => {
isEditing: true,
mutationHandler: jest.fn().mockResolvedValue({
data: {
- workItemUpdateWidgets: {
+ workItemUpdate: {
workItem: {},
errors: [error],
},
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 01891012f99..6b1ef8971d3 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
@@ -113,7 +113,7 @@ describe('WorkItemDetailModal component', () => {
createComponent();
findModal().vm.$emit('hide');
- expect(wrapper.emitted('close')).toBeTruthy();
+ expect(wrapper.emitted('close')).toHaveLength(1);
});
it('hides the modal when WorkItemDetail emits `close` event', () => {
diff --git a/spec/frontend/work_items/pages/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js
index 823981df880..b047e0dc8d7 100644
--- a/spec/frontend/work_items/pages/work_item_detail_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_spec.js
@@ -2,29 +2,33 @@ import { GlAlert, GlBadge, GlLoadingIcon, GlSkeletonLoader, GlButton } from '@gi
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
+import workItemWeightSubscription from 'ee_component/work_items/graphql/work_item_weight.subscription.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import WorkItemDetail from '~/work_items/components/work_item_detail.vue';
import WorkItemActions from '~/work_items/components/work_item_actions.vue';
import WorkItemDescription from '~/work_items/components/work_item_description.vue';
+import WorkItemDueDate from '~/work_items/components/work_item_due_date.vue';
import WorkItemState from '~/work_items/components/work_item_state.vue';
import WorkItemTitle from '~/work_items/components/work_item_title.vue';
import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue';
import WorkItemLabels from '~/work_items/components/work_item_labels.vue';
-import WorkItemWeight from '~/work_items/components/work_item_weight.vue';
import WorkItemInformation from '~/work_items/components/work_item_information.vue';
import { i18n } from '~/work_items/constants';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
+import workItemDatesSubscription from '~/work_items/graphql/work_item_dates.subscription.graphql';
import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subscription.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import updateWorkItemTaskMutation from '~/work_items/graphql/update_work_item_task.mutation.graphql';
-import { temporaryConfig } from '~/work_items/graphql/provider';
+import { temporaryConfig } from '~/graphql_shared/issuable_client';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import {
- workItemTitleSubscriptionResponse,
- workItemResponseFactory,
mockParent,
+ workItemDatesSubscriptionResponse,
+ workItemResponseFactory,
+ workItemTitleSubscriptionResponse,
+ workItemWeightSubscriptionResponse,
} from '../mock_data';
describe('WorkItemDetail component', () => {
@@ -40,7 +44,9 @@ describe('WorkItemDetail component', () => {
canDelete: true,
});
const successHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
- const initialSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse);
+ const datesSubscriptionHandler = jest.fn().mockResolvedValue(workItemDatesSubscriptionResponse);
+ const titleSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse);
+ const weightSubscriptionHandler = jest.fn().mockResolvedValue(workItemWeightSubscriptionResponse);
const findAlert = () => wrapper.findComponent(GlAlert);
const findSkeleton = () => wrapper.findComponent(GlSkeletonLoader);
@@ -49,9 +55,9 @@ describe('WorkItemDetail component', () => {
const findWorkItemTitle = () => wrapper.findComponent(WorkItemTitle);
const findWorkItemState = () => wrapper.findComponent(WorkItemState);
const findWorkItemDescription = () => wrapper.findComponent(WorkItemDescription);
+ const findWorkItemDueDate = () => wrapper.findComponent(WorkItemDueDate);
const findWorkItemAssignees = () => wrapper.findComponent(WorkItemAssignees);
const findWorkItemLabels = () => wrapper.findComponent(WorkItemLabels);
- const findWorkItemWeight = () => wrapper.findComponent(WorkItemWeight);
const findParent = () => wrapper.find('[data-testid="work-item-parent"]');
const findParentButton = () => findParent().findComponent(GlButton);
const findCloseButton = () => wrapper.find('[data-testid="work-item-close"]');
@@ -64,19 +70,26 @@ describe('WorkItemDetail component', () => {
updateInProgress = false,
workItemId = workItemQueryResponse.data.workItem.id,
handler = successHandler,
- subscriptionHandler = initialSubscriptionHandler,
+ subscriptionHandler = titleSubscriptionHandler,
confidentialityMock = [updateWorkItemMutation, jest.fn()],
workItemsMvc2Enabled = false,
includeWidgets = false,
error = undefined,
} = {}) => {
+ const handlers = [
+ [workItemQuery, handler],
+ [workItemTitleSubscription, subscriptionHandler],
+ [workItemDatesSubscription, datesSubscriptionHandler],
+ confidentialityMock,
+ ];
+
+ if (IS_EE) {
+ handlers.push([workItemWeightSubscription, weightSubscriptionHandler]);
+ }
+
wrapper = shallowMount(WorkItemDetail, {
apolloProvider: createMockApollo(
- [
- [workItemQuery, handler],
- [workItemTitleSubscription, subscriptionHandler],
- confidentialityMock,
- ],
+ handlers,
{},
{
typePolicies: includeWidgets ? temporaryConfig.cacheConfig.typePolicies : {},
@@ -93,6 +106,7 @@ describe('WorkItemDetail component', () => {
glFeatures: {
workItemsMvc2: workItemsMvc2Enabled,
},
+ hasIssueWeightsFeature: true,
},
});
};
@@ -134,6 +148,10 @@ describe('WorkItemDetail component', () => {
expect(findWorkItemState().exists()).toBe(true);
expect(findWorkItemTitle().exists()).toBe(true);
});
+
+ it('updates the document title', () => {
+ expect(document.title).toEqual('Updated title · Task · test-project-path');
+ });
});
describe('close button', () => {
@@ -295,8 +313,7 @@ describe('WorkItemDetail component', () => {
await waitForPromises();
findWorkItemActions().vm.$emit('toggleWorkItemConfidentiality', true);
await waitForPromises();
-
- expect(wrapper.emitted('workItemUpdated')).toBeFalsy();
+ expect(wrapper.emitted('workItemUpdated')).toBeUndefined();
await nextTick();
@@ -379,23 +396,50 @@ describe('WorkItemDetail component', () => {
it('shows an error message when WorkItemTitle emits an `error` event', async () => {
createComponent();
await waitForPromises();
+ const updateError = 'Failed to update';
- findWorkItemTitle().vm.$emit('error', i18n.updateError);
+ findWorkItemTitle().vm.$emit('error', updateError);
await waitForPromises();
- expect(findAlert().text()).toBe(i18n.updateError);
+ expect(findAlert().text()).toBe(updateError);
});
- it('calls the subscription', () => {
- createComponent();
+ describe('subscriptions', () => {
+ it('calls the title subscription', () => {
+ createComponent();
+
+ expect(titleSubscriptionHandler).toHaveBeenCalledWith({
+ issuableId: workItemQueryResponse.data.workItem.id,
+ });
+ });
- expect(initialSubscriptionHandler).toHaveBeenCalledWith({
- issuableId: workItemQueryResponse.data.workItem.id,
+ describe('dates subscription', () => {
+ describe('when the due date widget exists', () => {
+ it('calls the dates subscription', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(datesSubscriptionHandler).toHaveBeenCalledWith({
+ issuableId: workItemQueryResponse.data.workItem.id,
+ });
+ });
+ });
+
+ describe('when the due date widget does not exist', () => {
+ it('does not call the dates subscription', async () => {
+ const response = workItemResponseFactory({ datesWidgetPresent: false });
+ const handler = jest.fn().mockResolvedValue(response);
+ createComponent({ handler, workItemsMvc2Enabled: true });
+ await waitForPromises();
+
+ expect(datesSubscriptionHandler).not.toHaveBeenCalled();
+ });
+ });
});
});
- describe('when work_items_mvc_2 feature flag is enabled', () => {
- it('renders assignees component when assignees widget is returned from the API', async () => {
+ describe('assignees widget', () => {
+ it('renders assignees component when widget is returned from the API', async () => {
createComponent({
workItemsMvc2Enabled: true,
});
@@ -404,7 +448,7 @@ describe('WorkItemDetail component', () => {
expect(findWorkItemAssignees().exists()).toBe(true);
});
- it('does not render assignees component when assignees widget is not returned from the API', async () => {
+ it('does not render assignees component when widget is not returned from the API', async () => {
createComponent({
workItemsMvc2Enabled: true,
handler: jest
@@ -417,13 +461,6 @@ describe('WorkItemDetail component', () => {
});
});
- it('does not render assignees component when assignees feature flag is disabled', async () => {
- createComponent();
- await waitForPromises();
-
- expect(findWorkItemAssignees().exists()).toBe(false);
- });
-
describe('labels widget', () => {
it.each`
description | includeWidgets | exists
@@ -437,30 +474,31 @@ describe('WorkItemDetail component', () => {
});
});
- describe('weight widget', () => {
+ describe('dates widget', () => {
describe.each`
- description | weightWidgetPresent | exists
- ${'when widget is returned from API'} | ${true} | ${true}
- ${'when widget is not returned from API'} | ${false} | ${false}
- `('$description', ({ weightWidgetPresent, exists }) => {
- it(`${weightWidgetPresent ? 'renders' : 'does not render'} weight component`, async () => {
- const response = workItemResponseFactory({ weightWidgetPresent });
+ description | datesWidgetPresent | exists
+ ${'when widget is returned from API'} | ${true} | ${true}
+ ${'when widget is not returned from API'} | ${false} | ${false}
+ `('$description', ({ datesWidgetPresent, exists }) => {
+ it(`${datesWidgetPresent ? 'renders' : 'does not render'} due date component`, async () => {
+ const response = workItemResponseFactory({ datesWidgetPresent });
const handler = jest.fn().mockResolvedValue(response);
- createComponent({ handler });
+ createComponent({ handler, workItemsMvc2Enabled: true });
await waitForPromises();
- expect(findWorkItemWeight().exists()).toBe(exists);
+ expect(findWorkItemDueDate().exists()).toBe(exists);
});
});
it('shows an error message when it emits an `error` event', async () => {
createComponent({ workItemsMvc2Enabled: true });
await waitForPromises();
+ const updateError = 'Failed to update';
- findWorkItemWeight().vm.$emit('error', i18n.updateError);
+ findWorkItemDueDate().vm.$emit('error', updateError);
await waitForPromises();
- expect(findAlert().text()).toBe(i18n.updateError);
+ expect(findAlert().text()).toBe(updateError);
});
});
diff --git a/spec/frontend/work_items/components/work_item_due_date_spec.js b/spec/frontend/work_items/components/work_item_due_date_spec.js
new file mode 100644
index 00000000000..1d76154a1f0
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_due_date_spec.js
@@ -0,0 +1,346 @@
+import { GlFormGroup, GlDatepicker } from '@gitlab/ui';
+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 WorkItemDueDate from '~/work_items/components/work_item_due_date.vue';
+import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
+import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+import { updateWorkItemMutationResponse, updateWorkItemMutationErrorResponse } from '../mock_data';
+
+describe('WorkItemDueDate component', () => {
+ let wrapper;
+
+ Vue.use(VueApollo);
+
+ const workItemId = 'gid://gitlab/WorkItem/1';
+ const updateWorkItemMutationHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
+
+ const findStartDateButton = () =>
+ wrapper.findByRole('button', { name: WorkItemDueDate.i18n.addStartDate });
+ const findStartDateInput = () => wrapper.findByLabelText(WorkItemDueDate.i18n.startDate);
+ const findStartDatePicker = () => wrapper.findComponent(GlDatepicker);
+ const findDueDateButton = () =>
+ wrapper.findByRole('button', { name: WorkItemDueDate.i18n.addDueDate });
+ const findDueDateInput = () => wrapper.findByLabelText(WorkItemDueDate.i18n.dueDate);
+ const findDueDatePicker = () => wrapper.findAllComponents(GlDatepicker).at(1);
+ const findGlFormGroup = () => wrapper.findComponent(GlFormGroup);
+
+ const createComponent = ({
+ canUpdate = false,
+ dueDate = null,
+ startDate = null,
+ mutationHandler = updateWorkItemMutationHandler,
+ } = {}) => {
+ wrapper = mountExtended(WorkItemDueDate, {
+ apolloProvider: createMockApollo([[updateWorkItemMutation, mutationHandler]]),
+ propsData: {
+ canUpdate,
+ dueDate,
+ startDate,
+ workItemId,
+ workItemType: 'Task',
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when can update', () => {
+ describe('start date', () => {
+ describe('`Add start date` button', () => {
+ describe.each`
+ description | startDate | exists
+ ${'when there is no start date'} | ${null} | ${true}
+ ${'when there is a start date'} | ${'2022-01-01'} | ${false}
+ `('$description', ({ startDate, exists }) => {
+ beforeEach(() => {
+ createComponent({ canUpdate: true, startDate });
+ });
+
+ it(exists ? 'renders' : 'does not render', () => {
+ expect(findStartDateButton().exists()).toBe(exists);
+ });
+ });
+
+ describe('when it emits `click` event', () => {
+ beforeEach(() => {
+ createComponent({ canUpdate: true, startDate: null });
+ findStartDateButton().vm.$emit('click');
+ });
+
+ it('renders start date picker', () => {
+ expect(findStartDateInput().exists()).toBe(true);
+ });
+
+ it('hides itself', () => {
+ expect(findStartDateButton().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('date picker', () => {
+ describe('when it emits a `clear` event', () => {
+ beforeEach(() => {
+ createComponent({ canUpdate: true, dueDate: '2022-01-01', startDate: '2022-01-01' });
+ findStartDatePicker().vm.$emit('clear');
+ });
+
+ it('hides the date picker', () => {
+ expect(findStartDateInput().exists()).toBe(false);
+ });
+
+ it('shows the `Add start date` button', () => {
+ expect(findStartDateButton().exists()).toBe(true);
+ });
+
+ it('calls a mutation to update the dates', () => {
+ expect(updateWorkItemMutationHandler).toHaveBeenCalledWith({
+ input: {
+ id: workItemId,
+ startAndDueDateWidget: {
+ dueDate: new Date('2022-01-01T00:00:00.000Z'),
+ startDate: null,
+ },
+ },
+ });
+ });
+ });
+
+ describe('when it emits a `close` event', () => {
+ describe('when the start date is earlier than the due date', () => {
+ const startDate = new Date('2022-01-01T00:00:00.000Z');
+
+ beforeEach(() => {
+ createComponent({ canUpdate: true, dueDate: '2022-12-31', startDate: '2022-12-31' });
+ findStartDatePicker().vm.$emit('input', startDate);
+ findStartDatePicker().vm.$emit('close');
+ });
+
+ it('calls a mutation to update the dates', () => {
+ expect(updateWorkItemMutationHandler).toHaveBeenCalledWith({
+ input: {
+ id: workItemId,
+ startAndDueDateWidget: {
+ dueDate: new Date('2022-12-31T00:00:00.000Z'),
+ startDate,
+ },
+ },
+ });
+ });
+ });
+
+ describe('when the start date is later than the due date', () => {
+ const startDate = new Date('2030-01-01T00:00:00.000Z');
+ let datePickerOpenSpy;
+
+ beforeEach(() => {
+ createComponent({ canUpdate: true, dueDate: '2022-12-31', startDate: '2022-12-31' });
+ datePickerOpenSpy = jest.spyOn(wrapper.vm.$refs.dueDatePicker.calendar, 'show');
+ findStartDatePicker().vm.$emit('input', startDate);
+ findStartDatePicker().vm.$emit('close');
+ });
+
+ it('does not call a mutation to update the dates', () => {
+ expect(updateWorkItemMutationHandler).not.toHaveBeenCalled();
+ });
+
+ it('updates the due date picker to the same date', () => {
+ expect(findDueDatePicker().props('value')).toEqual(startDate);
+ });
+
+ it('opens the due date picker', () => {
+ expect(datePickerOpenSpy).toHaveBeenCalled();
+ });
+ });
+ });
+ });
+ });
+
+ describe('due date', () => {
+ describe('`Add due date` button', () => {
+ describe.each`
+ description | dueDate | exists
+ ${'when there is no due date'} | ${null} | ${true}
+ ${'when there is a due date'} | ${'2022-01-01'} | ${false}
+ `('$description', ({ dueDate, exists }) => {
+ beforeEach(() => {
+ createComponent({ canUpdate: true, dueDate });
+ });
+
+ it(exists ? 'renders' : 'does not render', () => {
+ expect(findDueDateButton().exists()).toBe(exists);
+ });
+ });
+
+ describe('when it emits `click` event', () => {
+ beforeEach(() => {
+ createComponent({ canUpdate: true, dueDate: null });
+ findDueDateButton().vm.$emit('click');
+ });
+
+ it('renders due date picker', () => {
+ expect(findDueDateInput().exists()).toBe(true);
+ });
+
+ it('hides itself', () => {
+ expect(findDueDateButton().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('date picker', () => {
+ describe('when it emits a `clear` event', () => {
+ beforeEach(() => {
+ createComponent({ canUpdate: true, dueDate: '2022-01-01', startDate: '2022-01-01' });
+ findDueDatePicker().vm.$emit('clear');
+ });
+
+ it('hides the date picker', () => {
+ expect(findDueDateInput().exists()).toBe(false);
+ });
+
+ it('shows the `Add due date` button', () => {
+ expect(findDueDateButton().exists()).toBe(true);
+ });
+
+ it('calls a mutation to update the dates', () => {
+ expect(updateWorkItemMutationHandler).toHaveBeenCalledWith({
+ input: {
+ id: workItemId,
+ startAndDueDateWidget: {
+ dueDate: null,
+ startDate: new Date('2022-01-01T00:00:00.000Z'),
+ },
+ },
+ });
+ });
+ });
+
+ describe('when it emits a `close` event', () => {
+ const dueDate = new Date('2022-12-31T00:00:00.000Z');
+
+ beforeEach(() => {
+ createComponent({ canUpdate: true, dueDate: '2022-01-01', startDate: '2022-01-01' });
+ findDueDatePicker().vm.$emit('input', dueDate);
+ findDueDatePicker().vm.$emit('close');
+ });
+
+ it('calls a mutation to update the dates', () => {
+ expect(updateWorkItemMutationHandler).toHaveBeenCalledWith({
+ input: {
+ id: workItemId,
+ startAndDueDateWidget: {
+ dueDate,
+ startDate: new Date('2022-01-01T00:00:00.000Z'),
+ },
+ },
+ });
+ });
+ });
+ });
+ });
+
+ describe('when updating date', () => {
+ describe('when dates are changed', () => {
+ let trackingSpy;
+
+ beforeEach(() => {
+ createComponent({ canUpdate: true, dueDate: '2022-12-31', startDate: '2022-12-31' });
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+
+ findStartDatePicker().vm.$emit('input', new Date('2022-01-01T00:00:00.000Z'));
+ findStartDatePicker().vm.$emit('close');
+ });
+
+ it('mutation is called to update dates', () => {
+ expect(updateWorkItemMutationHandler).toHaveBeenCalledWith({
+ input: {
+ id: workItemId,
+ startAndDueDateWidget: {
+ dueDate: new Date('2022-12-31T00:00:00.000Z'),
+ startDate: new Date('2022-01-01T00:00:00.000Z'),
+ },
+ },
+ });
+ });
+
+ it('start date input is disabled', () => {
+ expect(findStartDatePicker().props('disabled')).toBe(true);
+ });
+
+ it('due date input is disabled', () => {
+ expect(findDueDatePicker().props('disabled')).toBe(true);
+ });
+
+ it('tracks updating the dates', () => {
+ expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'updated_dates', {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'item_dates',
+ property: 'type_Task',
+ });
+ });
+ });
+
+ describe('when dates are unchanged', () => {
+ beforeEach(() => {
+ createComponent({ canUpdate: true, dueDate: '2022-12-31', startDate: '2022-12-31' });
+
+ findStartDatePicker().vm.$emit('input', new Date('2022-12-31T00:00:00.000Z'));
+ findStartDatePicker().vm.$emit('close');
+ });
+
+ it('mutation is not called to update dates', () => {
+ expect(updateWorkItemMutationHandler).not.toHaveBeenCalled();
+ });
+ });
+
+ describe.each`
+ description | mutationHandler
+ ${'when there is a GraphQL error'} | ${jest.fn().mockResolvedValue(updateWorkItemMutationErrorResponse)}
+ ${'when there is a network error'} | ${jest.fn().mockRejectedValue(new Error())}
+ `('$description', ({ mutationHandler }) => {
+ beforeEach(() => {
+ createComponent({
+ canUpdate: true,
+ dueDate: '2022-12-31',
+ startDate: '2022-12-31',
+ mutationHandler,
+ });
+
+ findStartDatePicker().vm.$emit('input', new Date('2022-01-01T00:00:00.000Z'));
+ findStartDatePicker().vm.$emit('close');
+ return waitForPromises();
+ });
+
+ it('emits an error', () => {
+ expect(wrapper.emitted('error')).toEqual([
+ ['Something went wrong while updating the task. Please try again.'],
+ ]);
+ });
+ });
+ });
+ });
+
+ describe('when cannot update', () => {
+ it('start and due date inputs are disabled', async () => {
+ createComponent({ canUpdate: false, dueDate: '2022-01-01', startDate: '2022-01-01' });
+ await nextTick();
+
+ expect(findStartDateInput().props('disabled')).toBe(true);
+ expect(findDueDateInput().props('disabled')).toBe(true);
+ });
+
+ describe('when there is no start and due date', () => {
+ it('shows None', () => {
+ createComponent({ canUpdate: false, dueDate: null, startDate: null });
+
+ expect(findGlFormGroup().text()).toContain(WorkItemDueDate.i18n.none);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_information_spec.js b/spec/frontend/work_items/components/work_item_information_spec.js
index d5f6921c2bc..887c5f615e9 100644
--- a/spec/frontend/work_items/components/work_item_information_spec.js
+++ b/spec/frontend/work_items/components/work_item_information_spec.js
@@ -8,7 +8,6 @@ const createComponent = () => mount(WorkItemInformation);
describe('Work item information alert', () => {
let wrapper;
const tasksHelpPath = helpPagePath('user/tasks');
- const workItemsHelpPath = helpPagePath('development/work_items');
const findAlert = () => wrapper.findComponent(GlAlert);
const findHelpLink = () => wrapper.findComponent(GlLink);
@@ -33,16 +32,12 @@ describe('Work item information alert', () => {
expect(findAlert().props('variant')).toBe('tip');
});
- it('should have the correct text for primary button and link', () => {
+ it('should have the correct text for title', () => {
expect(findAlert().props('title')).toBe(WorkItemInformation.i18n.tasksInformationTitle);
- expect(findAlert().props('primaryButtonText')).toBe(
- WorkItemInformation.i18n.learnTasksButtonText,
- );
- expect(findAlert().props('primaryButtonLink')).toBe(tasksHelpPath);
});
it('should have the correct link to work item link', () => {
expect(findHelpLink().exists()).toBe(true);
- expect(findHelpLink().attributes('href')).toBe(workItemsHelpPath);
+ expect(findHelpLink().attributes('href')).toBe(tasksHelpPath);
});
});
diff --git a/spec/frontend/work_items/components/work_item_labels_spec.js b/spec/frontend/work_items/components/work_item_labels_spec.js
index 1734b901d1a..1d976897c15 100644
--- a/spec/frontend/work_items/components/work_item_labels_spec.js
+++ b/spec/frontend/work_items/components/work_item_labels_spec.js
@@ -9,7 +9,7 @@ import labelSearchQuery from '~/vue_shared/components/sidebar/labels_select_widg
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import WorkItemLabels from '~/work_items/components/work_item_labels.vue';
import { i18n } from '~/work_items/constants';
-import { temporaryConfig, resolvers } from '~/work_items/graphql/provider';
+import { temporaryConfig, resolvers } from '~/graphql_shared/issuable_client';
import { projectLabelsResponse, mockLabels, workItemQueryResponse } from '../mock_data';
Vue.use(VueApollo);
@@ -45,13 +45,11 @@ describe('WorkItemLabels component', () => {
});
wrapper = mountExtended(WorkItemLabels, {
- provide: {
- fullPath: 'test-project-path',
- },
propsData: {
labels,
workItemId,
canUpdate,
+ fullPath: 'test-project-path',
},
attachTo: document.body,
apolloProvider,
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js
new file mode 100644
index 00000000000..1d5472a0473
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js
@@ -0,0 +1,122 @@
+import { GlButton, GlIcon } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue';
+
+import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue';
+import WorkItemLinksMenu from '~/work_items/components/work_item_links/work_item_links_menu.vue';
+
+import { workItemTask, confidentialWorkItemTask, closedWorkItemTask } from '../../mock_data';
+
+describe('WorkItemLinkChild', () => {
+ const WORK_ITEM_ID = 'gid://gitlab/WorkItem/2';
+ let wrapper;
+
+ const createComponent = ({
+ projectPath = 'gitlab-org/gitlab-test',
+ canUpdate = true,
+ issuableGid = WORK_ITEM_ID,
+ childItem = workItemTask,
+ } = {}) => {
+ wrapper = shallowMountExtended(WorkItemLinkChild, {
+ propsData: {
+ projectPath,
+ canUpdate,
+ issuableGid,
+ childItem,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it.each`
+ status | childItem | statusIconName | statusIconColorClass | rawTimestamp | tooltipContents
+ ${'open'} | ${workItemTask} | ${'issue-open-m'} | ${'gl-text-green-500'} | ${workItemTask.createdAt} | ${'Created'}
+ ${'closed'} | ${closedWorkItemTask} | ${'issue-close'} | ${'gl-text-blue-500'} | ${closedWorkItemTask.closedAt} | ${'Closed'}
+ `(
+ 'renders item status icon and tooltip when item status is `$status`',
+ ({ childItem, statusIconName, statusIconColorClass, rawTimestamp, tooltipContents }) => {
+ createComponent({ childItem });
+
+ const statusIcon = wrapper.findByTestId('item-status-icon').findComponent(GlIcon);
+ const statusTooltip = wrapper.findComponent(RichTimestampTooltip);
+
+ expect(statusIcon.props('name')).toBe(statusIconName);
+ expect(statusIcon.classes()).toContain(statusIconColorClass);
+ expect(statusTooltip.props('rawTimestamp')).toBe(rawTimestamp);
+ expect(statusTooltip.props('timestampTypeText')).toContain(tooltipContents);
+ },
+ );
+
+ it('renders confidential icon when item is confidential', () => {
+ createComponent({ childItem: confidentialWorkItemTask });
+
+ const confidentialIcon = wrapper.findByTestId('confidential-icon');
+
+ expect(confidentialIcon.props('name')).toBe('eye-slash');
+ expect(confidentialIcon.attributes('title')).toBe('Confidential');
+ });
+
+ describe('item title', () => {
+ let titleEl;
+
+ beforeEach(() => {
+ createComponent();
+
+ titleEl = wrapper.findComponent(GlButton);
+ });
+
+ it('renders item title', () => {
+ expect(titleEl.attributes('href')).toBe('/gitlab-org/gitlab-test/-/work_items/4');
+ expect(titleEl.text()).toBe(workItemTask.title);
+ });
+
+ it.each`
+ action | event | emittedEvent
+ ${'clicking'} | ${'click'} | ${'click'}
+ ${'doing mouseover on'} | ${'mouseover'} | ${'mouseover'}
+ ${'doing mouseout on'} | ${'mouseout'} | ${'mouseout'}
+ `('$action item title emit `$emittedEvent` event', ({ event, emittedEvent }) => {
+ const eventObj = {
+ preventDefault: jest.fn(),
+ };
+ titleEl.vm.$emit(event, eventObj);
+
+ expect(wrapper.emitted(emittedEvent)).toEqual([[workItemTask.id, eventObj]]);
+ });
+ });
+
+ describe('item menu', () => {
+ let itemMenuEl;
+
+ beforeEach(() => {
+ createComponent();
+
+ itemMenuEl = wrapper.findComponent(WorkItemLinksMenu);
+ });
+
+ it('renders work-item-links-menu', () => {
+ expect(itemMenuEl.exists()).toBe(true);
+
+ expect(itemMenuEl.attributes()).toMatchObject({
+ 'work-item-id': workItemTask.id,
+ 'parent-work-item-id': WORK_ITEM_ID,
+ });
+ });
+
+ it('does not render work-item-links-menu when canUpdate is false', () => {
+ createComponent({ canUpdate: false });
+
+ expect(wrapper.findComponent(WorkItemLinksMenu).exists()).toBe(false);
+ });
+
+ it('removeChild event on menu triggers `click-remove-child` event', () => {
+ itemMenuEl.vm.$emit('removeChild');
+
+ expect(wrapper.emitted('remove')).toEqual([[workItemTask.id]]);
+ });
+ });
+});
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 00f508f1548..876aedff08b 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,12 +1,13 @@
import Vue, { nextTick } from 'vue';
-import { GlButton, GlIcon, GlAlert } from '@gitlab/ui';
+import { 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 issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.graphql';
import WorkItemLinks from '~/work_items/components/work_item_links/work_item_links.vue';
+import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.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';
@@ -20,6 +21,20 @@ import {
Vue.use(VueApollo);
+const issueConfidentialityResponse = (confidential = false) => ({
+ data: {
+ workspace: {
+ id: '1',
+ __typename: 'Project',
+ issuable: {
+ __typename: 'Issue',
+ id: 'gid://gitlab/Issue/4',
+ confidential,
+ },
+ },
+ },
+});
+
describe('WorkItemLinks', () => {
let wrapper;
let mockApollo;
@@ -36,18 +51,18 @@ describe('WorkItemLinks', () => {
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,
+ confidentialQueryHandler = jest.fn().mockResolvedValue(issueConfidentialityResponse()),
} = {}) => {
mockApollo = createMockApollo(
[
[getWorkItemLinksQuery, fetchHandler],
[changeWorkItemParentMutation, mutationHandler],
[workItemQuery, childWorkItemQueryHandler],
+ [issueConfidentialQuery, confidentialQueryHandler],
],
{},
{ addTypename: true },
@@ -61,6 +76,7 @@ describe('WorkItemLinks', () => {
},
provide: {
projectPath: 'project/path',
+ iid: '1',
},
propsData: { issuableId: 1 },
apolloProvider: mockApollo,
@@ -77,8 +93,9 @@ describe('WorkItemLinks', () => {
const findLinksBody = () => wrapper.findByTestId('links-body');
const findEmptyState = () => wrapper.findByTestId('links-empty');
const findToggleAddFormButton = () => wrapper.findByTestId('toggle-add-form');
+ const findWorkItemLinkChildItems = () => wrapper.findAllComponents(WorkItemLinkChild);
+ const findFirstWorkItemLinkChild = () => findWorkItemLinkChildItems().at(0);
const findAddLinksForm = () => wrapper.findByTestId('add-links-form');
- const findFirstLinksMenu = () => wrapper.findByTestId('links-menu');
const findChildrenCount = () => wrapper.findByTestId('children-count');
beforeEach(async () => {
@@ -132,8 +149,7 @@ describe('WorkItemLinks', () => {
it('renders all hierarchy widget children', () => {
expect(findLinksBody().exists()).toBe(true);
- expect(findChildren()).toHaveLength(4);
- expect(findFirstLinksMenu().exists()).toBe(true);
+ expect(findWorkItemLinkChildItems()).toHaveLength(4);
});
it('shows alert when list loading fails', async () => {
@@ -148,40 +164,12 @@ describe('WorkItemLinks', () => {
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({
@@ -194,17 +182,21 @@ describe('WorkItemLinks', () => {
});
it('does not display link menu on children', () => {
- expect(findFirstLinksMenu().exists()).toBe(false);
+ expect(findWorkItemLinkChildItems().at(0).props('canUpdate')).toBe(false);
});
});
describe('remove child', () => {
+ let firstChild;
+
beforeEach(async () => {
await createComponent({ mutationHandler: mutationChangeParentHandler });
+
+ firstChild = findFirstWorkItemLinkChild();
});
it('calls correct mutation with correct variables', async () => {
- findFirstLinksMenu().vm.$emit('removeChild');
+ firstChild.vm.$emit('remove', firstChild.vm.childItem.id);
await waitForPromises();
@@ -219,7 +211,7 @@ describe('WorkItemLinks', () => {
});
it('shows toast when mutation succeeds', async () => {
- findFirstLinksMenu().vm.$emit('removeChild');
+ firstChild.vm.$emit('remove', firstChild.vm.childItem.id);
await waitForPromises();
@@ -229,28 +221,30 @@ describe('WorkItemLinks', () => {
});
it('renders correct number of children after removal', async () => {
- expect(findChildren()).toHaveLength(4);
+ expect(findWorkItemLinkChildItems()).toHaveLength(4);
- findFirstLinksMenu().vm.$emit('removeChild');
+ firstChild.vm.$emit('remove', firstChild.vm.childItem.id);
await waitForPromises();
- expect(findChildren()).toHaveLength(3);
+ expect(findWorkItemLinkChildItems()).toHaveLength(3);
});
});
describe('prefetching child items', () => {
+ let firstChild;
+
beforeEach(async () => {
await createComponent();
- });
- const findChildLink = () => findChildren().at(0).findComponent(GlButton);
+ firstChild = findFirstWorkItemLinkChild();
+ });
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');
+ firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id);
jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
await waitForPromises();
@@ -260,12 +254,24 @@ describe('WorkItemLinks', () => {
});
it('does not fetch the child work item if link is hovered for less than 250 ms', async () => {
- findChildLink().vm.$emit('mouseover');
+ firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id);
jest.advanceTimersByTime(200);
- findChildLink().vm.$emit('mouseout');
+ firstChild.vm.$emit('mouseout', firstChild.vm.childItem.id);
await waitForPromises();
expect(childWorkItemQueryHandler).not.toHaveBeenCalled();
});
});
+
+ describe('when parent item is confidential', () => {
+ it('passes correct confidentiality status to form', async () => {
+ await createComponent({
+ confidentialQueryHandler: jest.fn().mockResolvedValue(issueConfidentialityResponse(true)),
+ });
+ findToggleAddFormButton().vm.$emit('click');
+ await nextTick();
+
+ expect(findAddLinksForm().props('parentConfidential')).toBe(true);
+ });
+ });
});
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 6b23a6e4795..b24d940d56a 100644
--- a/spec/frontend/work_items/components/work_item_state_spec.js
+++ b/spec/frontend/work_items/components/work_item_state_spec.js
@@ -7,7 +7,6 @@ import waitForPromises from 'helpers/wait_for_promises';
import ItemState from '~/work_items/components/item_state.vue';
import WorkItemState from '~/work_items/components/work_item_state.vue';
import {
- i18n,
STATE_OPEN,
STATE_CLOSED,
STATE_EVENT_CLOSE,
@@ -104,7 +103,9 @@ describe('WorkItemState component', () => {
findItemState().vm.$emit('changed', STATE_CLOSED);
await waitForPromises();
- expect(wrapper.emitted('error')).toEqual([[i18n.updateError]]);
+ expect(wrapper.emitted('error')).toEqual([
+ ['Something went wrong while updating the task. Please try again.'],
+ ]);
});
it('tracks editing the state', async () => {
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 c0d966abab8..a549aad5cd8 100644
--- a/spec/frontend/work_items/components/work_item_title_spec.js
+++ b/spec/frontend/work_items/components/work_item_title_spec.js
@@ -6,7 +6,7 @@ import { mockTracking } from 'helpers/tracking_helper';
import waitForPromises from 'helpers/wait_for_promises';
import ItemTitle from '~/work_items/components/item_title.vue';
import WorkItemTitle from '~/work_items/components/work_item_title.vue';
-import { i18n, TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
+import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import updateWorkItemTaskMutation from '~/work_items/graphql/update_work_item_task.mutation.graphql';
import { updateWorkItemMutationResponse, workItemQueryResponse } from '../mock_data';
@@ -116,7 +116,9 @@ describe('WorkItemTitle component', () => {
findItemTitle().vm.$emit('title-changed', 'new title');
await waitForPromises();
- expect(wrapper.emitted('error')).toEqual([[i18n.updateError]]);
+ expect(wrapper.emitted('error')).toEqual([
+ ['Something went wrong while updating the task. Please try again.'],
+ ]);
});
it('tracks editing the title', async () => {
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
index 85466578e18..95ddfc3980e 100644
--- a/spec/frontend/work_items/components/work_item_type_icon_spec.js
+++ b/spec/frontend/work_items/components/work_item_type_icon_spec.js
@@ -1,11 +1,17 @@
import { GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
let wrapper;
function createComponent(propsData) {
- wrapper = shallowMount(WorkItemTypeIcon, { propsData });
+ wrapper = shallowMount(WorkItemTypeIcon, {
+ propsData,
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
+ });
}
describe('Work Item type component', () => {
@@ -16,22 +22,23 @@ describe('Work Item type component', () => {
});
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'} | ${''}
+ workItemType | workItemIconName | iconName | text | showTooltipOnHover
+ ${'TASK'} | ${''} | ${'issue-type-task'} | ${'Task'} | ${false}
+ ${''} | ${'issue-type-task'} | ${'issue-type-task'} | ${''} | ${true}
+ ${'ISSUE'} | ${''} | ${'issue-type-issue'} | ${'Issue'} | ${true}
+ ${''} | ${'issue-type-issue'} | ${'issue-type-issue'} | ${''} | ${true}
+ ${'REQUIREMENTS'} | ${''} | ${'issue-type-requirements'} | ${'Requirements'} | ${true}
+ ${'INCIDENT'} | ${''} | ${'issue-type-incident'} | ${'Incident'} | ${false}
+ ${'TEST_CASE'} | ${''} | ${'issue-type-test-case'} | ${'Test case'} | ${true}
+ ${'random-issue-type'} | ${''} | ${'issue-type-issue'} | ${''} | ${true}
`(
'with workItemType set to "$workItemType" and workItemIconName set to "$workItemIconName"',
- ({ workItemType, workItemIconName, iconName, text }) => {
+ ({ workItemType, workItemIconName, iconName, text, showTooltipOnHover }) => {
beforeEach(() => {
createComponent({
workItemType,
workItemIconName,
+ showTooltipOnHover,
});
});
@@ -42,6 +49,16 @@ describe('Work Item type component', () => {
it(`renders correct text`, () => {
expect(wrapper.text()).toBe(text);
});
+
+ it('renders the icon in gray color', () => {
+ expect(findIcon().classes()).toContain('gl-text-gray-500');
+ });
+
+ it('shows tooltip on hover when props passed', () => {
+ const tooltip = getBinding(findIcon().element, 'gl-tooltip');
+
+ expect(tooltip.value).toBe(showTooltipOnHover);
+ });
},
);
});
diff --git a/spec/frontend/work_items/components/work_item_weight_spec.js b/spec/frontend/work_items/components/work_item_weight_spec.js
deleted file mode 100644
index 94bdb336deb..00000000000
--- a/spec/frontend/work_items/components/work_item_weight_spec.js
+++ /dev/null
@@ -1,214 +0,0 @@
-import { GlForm, GlFormInput } from '@gitlab/ui';
-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 { 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 workItemId = 'gid://gitlab/WorkItem/1';
- const workItemType = 'Task';
-
- const findForm = () => wrapper.findComponent(GlForm);
- const findInput = () => wrapper.findComponent(GlFormInput);
-
- const createComponent = ({
- canUpdate = false,
- hasIssueWeightsFeature = true,
- isEditing = false,
- weight,
- mutationHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse),
- } = {}) => {
- wrapper = mountExtended(WorkItemWeight, {
- apolloProvider: createMockApollo([[updateWorkItemMutation, mutationHandler]]),
- propsData: {
- canUpdate,
- weight,
- workItemId,
- workItemType,
- },
- provide: {
- hasIssueWeightsFeature,
- },
- });
-
- if (isEditing) {
- findInput().vm.$emit('focus');
- }
- };
-
- describe('`issue_weights` licensed feature', () => {
- describe.each`
- description | hasIssueWeightsFeature | exists
- ${'when available'} | ${true} | ${true}
- ${'when not available'} | ${false} | ${false}
- `('$description', ({ hasIssueWeightsFeature, exists }) => {
- it(hasIssueWeightsFeature ? 'renders component' : 'does not render component', () => {
- createComponent({ hasIssueWeightsFeature });
-
- expect(findForm().exists()).toBe(exists);
- });
- });
- });
-
- describe('weight input', () => {
- it('has "Weight" label', () => {
- createComponent();
-
- expect(wrapper.findByLabelText(__('Weight')).exists()).toBe(true);
- });
-
- describe('placeholder attribute', () => {
- describe.each`
- description | isEditing | canUpdate | value
- ${'when not editing and cannot update'} | ${false} | ${false} | ${__('None')}
- ${'when editing and cannot update'} | ${true} | ${false} | ${__('None')}
- ${'when not editing and can update'} | ${false} | ${true} | ${__('None')}
- ${'when editing and can update'} | ${true} | ${true} | ${__('Enter a number')}
- `('$description', ({ isEditing, canUpdate, value }) => {
- it(`has a value of "${value}"`, async () => {
- createComponent({ canUpdate, isEditing });
- await nextTick();
-
- expect(findInput().attributes('placeholder')).toBe(value);
- });
- });
- });
-
- describe('readonly attribute', () => {
- describe.each`
- description | canUpdate | value
- ${'when cannot update'} | ${false} | ${'readonly'}
- ${'when can update'} | ${true} | ${undefined}
- `('$description', ({ canUpdate, value }) => {
- it(`renders readonly=${value}`, () => {
- createComponent({ canUpdate });
-
- expect(findInput().attributes('readonly')).toBe(value);
- });
- });
- });
-
- describe('type attribute', () => {
- describe.each`
- description | isEditing | canUpdate | type
- ${'when not editing and cannot update'} | ${false} | ${false} | ${'text'}
- ${'when editing and cannot update'} | ${true} | ${false} | ${'text'}
- ${'when not editing and can update'} | ${false} | ${true} | ${'text'}
- ${'when editing and can update'} | ${true} | ${true} | ${'number'}
- `('$description', ({ isEditing, canUpdate, type }) => {
- it(`has a value of "${type}"`, async () => {
- createComponent({ canUpdate, isEditing });
- await nextTick();
-
- expect(findInput().attributes('type')).toBe(type);
- });
- });
- });
-
- describe('value attribute', () => {
- describe.each`
- weight | value
- ${1} | ${'1'}
- ${0} | ${'0'}
- ${null} | ${''}
- ${undefined} | ${''}
- `('when `weight` prop is "$weight"', ({ weight, value }) => {
- it(`value is "${value}"`, () => {
- createComponent({ weight });
-
- expect(findInput().element.value).toBe(value);
- });
- });
- });
-
- describe('when blurred', () => {
- 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(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({ canUpdate: true });
-
- findInput().trigger('blur');
-
- expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'updated_weight', {
- category: TRACKING_CATEGORY_SHOW,
- label: 'item_weight',
- property: 'type_Task',
- });
- });
- });
- });
-});
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index d24ac2a9f93..e1bc8d2f6b7 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -28,6 +28,11 @@ export const workItemQueryResponse = {
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
+ project: {
+ __typename: 'Project',
+ id: '1',
+ fullPath: 'test-project-path',
+ },
workItemType: {
__typename: 'WorkItemType',
id: 'gid://gitlab/WorkItems::Type/5',
@@ -93,6 +98,11 @@ export const updateWorkItemMutationResponse = {
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
+ project: {
+ __typename: 'Project',
+ id: '1',
+ fullPath: 'test-project-path',
+ },
workItemType: {
__typename: 'WorkItemType',
id: 'gid://gitlab/WorkItems::Type/5',
@@ -128,6 +138,16 @@ export const updateWorkItemMutationResponse = {
},
};
+export const updateWorkItemMutationErrorResponse = {
+ data: {
+ workItemUpdate: {
+ __typename: 'WorkItemUpdatePayload',
+ errors: ['Error!'],
+ workItem: {},
+ },
+ },
+};
+
export const mockParent = {
parent: {
id: 'gid://gitlab/Issue/1',
@@ -142,6 +162,7 @@ export const workItemResponseFactory = ({
canDelete = false,
allowsMultipleAssignees = true,
assigneesWidgetPresent = true,
+ datesWidgetPresent = true,
weightWidgetPresent = true,
confidential = false,
canInviteMembers = false,
@@ -157,6 +178,11 @@ export const workItemResponseFactory = ({
confidential,
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
+ project: {
+ __typename: 'Project',
+ id: '1',
+ fullPath: 'test-project-path',
+ },
workItemType: {
__typename: 'WorkItemType',
id: 'gid://gitlab/WorkItems::Type/5',
@@ -186,6 +212,14 @@ export const workItemResponseFactory = ({
},
}
: { type: 'MOCK TYPE' },
+ datesWidgetPresent
+ ? {
+ __typename: 'WorkItemWidgetStartAndDueDate',
+ type: 'START_AND_DUE_DATE',
+ dueDate: '2022-12-31',
+ startDate: '2022-01-01',
+ }
+ : { type: 'MOCK TYPE' },
weightWidgetPresent
? {
__typename: 'WorkItemWidgetWeight',
@@ -212,17 +246,6 @@ export const workItemResponseFactory = ({
},
});
-export const updateWorkItemWidgetsResponse = {
- data: {
- workItemUpdateWidgets: {
- workItem: {
- id: 1234,
- },
- errors: [],
- },
- },
-};
-
export const projectWorkItemTypesQueryResponse = {
data: {
workspace: {
@@ -251,6 +274,11 @@ export const createWorkItemMutationResponse = {
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
+ project: {
+ __typename: 'Project',
+ id: '1',
+ fullPath: 'test-project-path',
+ },
workItemType: {
__typename: 'WorkItemType',
id: 'gid://gitlab/WorkItems::Type/5',
@@ -282,6 +310,11 @@ export const createWorkItemFromTaskMutationResponse = {
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
+ project: {
+ __typename: 'Project',
+ id: '1',
+ fullPath: 'test-project-path',
+ },
workItemType: {
__typename: 'WorkItemType',
id: 'gid://gitlab/WorkItems::Type/5',
@@ -310,6 +343,11 @@ export const createWorkItemFromTaskMutationResponse = {
closedAt: null,
description: '',
confidential: false,
+ project: {
+ __typename: 'Project',
+ id: '1',
+ fullPath: 'test-project-path',
+ },
workItemType: {
__typename: 'WorkItemType',
id: 'gid://gitlab/WorkItems::Type/5',
@@ -368,6 +406,21 @@ export const deleteWorkItemFromTaskMutationErrorResponse = {
},
};
+export const workItemDatesSubscriptionResponse = {
+ data: {
+ issuableDatesUpdated: {
+ id: 'gid://gitlab/WorkItem/1',
+ widgets: [
+ {
+ __typename: 'WorkItemWidgetStartAndDueDate',
+ dueDate: '2022-12-31',
+ startDate: '2022-01-01',
+ },
+ ],
+ },
+ },
+};
+
export const workItemTitleSubscriptionResponse = {
data: {
issuableTitleUpdated: {
@@ -377,6 +430,20 @@ export const workItemTitleSubscriptionResponse = {
},
};
+export const workItemWeightSubscriptionResponse = {
+ data: {
+ issuableWeightUpdated: {
+ id: 'gid://gitlab/WorkItem/1',
+ widgets: [
+ {
+ __typename: 'WorkItemWidgetWeight',
+ weight: 1,
+ },
+ ],
+ },
+ },
+};
+
export const workItemHierarchyEmptyResponse = {
data: {
workItem: {
@@ -388,6 +455,11 @@ export const workItemHierarchyEmptyResponse = {
title: 'New title',
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
+ project: {
+ __typename: 'Project',
+ id: '1',
+ fullPath: 'test-project-path',
+ },
userPermissions: {
deleteWorkItem: false,
updateWorkItem: false,
@@ -426,6 +498,11 @@ export const workItemHierarchyNoUpdatePermissionResponse = {
deleteWorkItem: false,
updateWorkItem: false,
},
+ project: {
+ __typename: 'Project',
+ id: '1',
+ fullPath: 'test-project-path',
+ },
confidential: false,
widgets: [
{
@@ -461,6 +538,48 @@ export const workItemHierarchyNoUpdatePermissionResponse = {
},
};
+export const workItemTask = {
+ id: 'gid://gitlab/WorkItem/4',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/5',
+ __typename: 'WorkItemType',
+ },
+ title: 'bar',
+ state: 'OPEN',
+ confidential: false,
+ createdAt: '2022-08-03T12:41:54Z',
+ closedAt: null,
+ __typename: 'WorkItem',
+};
+
+export const confidentialWorkItemTask = {
+ id: 'gid://gitlab/WorkItem/2',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/5',
+ __typename: 'WorkItemType',
+ },
+ title: 'xyz',
+ state: 'OPEN',
+ confidential: true,
+ createdAt: '2022-08-03T12:41:54Z',
+ closedAt: null,
+ __typename: 'WorkItem',
+};
+
+export const closedWorkItemTask = {
+ id: 'gid://gitlab/WorkItem/3',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/5',
+ __typename: 'WorkItemType',
+ },
+ title: 'abc',
+ state: 'CLOSED',
+ confidential: false,
+ createdAt: '2022-08-03T12:41:54Z',
+ closedAt: '2022-08-12T13:07:52Z',
+ __typename: 'WorkItem',
+};
+
export const workItemHierarchyResponse = {
data: {
workItem: {
@@ -475,6 +594,11 @@ export const workItemHierarchyResponse = {
updateWorkItem: true,
},
confidential: false,
+ project: {
+ __typename: 'Project',
+ id: '1',
+ fullPath: 'test-project-path',
+ },
widgets: [
{
type: 'DESCRIPTION',
@@ -485,45 +609,9 @@ export const workItemHierarchyResponse = {
parent: null,
children: {
nodes: [
- {
- id: 'gid://gitlab/WorkItem/2',
- workItemType: {
- id: 'gid://gitlab/WorkItems::Type/5',
- __typename: 'WorkItemType',
- },
- title: 'xyz',
- state: 'OPEN',
- confidential: true,
- createdAt: '2022-08-03T12:41:54Z',
- closedAt: null,
- __typename: 'WorkItem',
- },
- {
- id: 'gid://gitlab/WorkItem/3',
- workItemType: {
- id: 'gid://gitlab/WorkItems::Type/5',
- __typename: 'WorkItemType',
- },
- title: 'abc',
- state: 'CLOSED',
- confidential: false,
- createdAt: '2022-08-03T12:41:54Z',
- closedAt: '2022-08-12T13:07:52Z',
- __typename: 'WorkItem',
- },
- {
- id: 'gid://gitlab/WorkItem/4',
- workItemType: {
- id: 'gid://gitlab/WorkItems::Type/5',
- __typename: 'WorkItemType',
- },
- title: 'bar',
- state: 'OPEN',
- confidential: false,
- createdAt: '2022-08-03T12:41:54Z',
- closedAt: null,
- __typename: 'WorkItem',
- },
+ confidentialWorkItemTask,
+ closedWorkItemTask,
+ workItemTask,
{
id: 'gid://gitlab/WorkItem/5',
workItemType: {
@@ -570,6 +658,11 @@ export const changeWorkItemParentMutationResponse = {
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
+ project: {
+ __typename: 'Project',
+ id: '1',
+ fullPath: 'test-project-path',
+ },
widgets: [
{
__typename: 'WorkItemWidgetHierarchy',
@@ -649,6 +742,71 @@ export const projectMembersResponseWithCurrentUser = {
},
},
],
+ pageInfo: {
+ hasNextPage: false,
+ endCursor: null,
+ startCursor: null,
+ },
+ },
+ },
+ },
+};
+
+export const projectMembersResponseWithCurrentUserWithNextPage = {
+ data: {
+ workspace: {
+ id: '1',
+ __typename: 'Project',
+ users: {
+ nodes: [
+ {
+ id: 'user-2',
+ user: {
+ __typename: 'UserCore',
+ id: 'gid://gitlab/User/5',
+ avatarUrl: '/avatar2',
+ name: 'rookie',
+ username: 'rookie',
+ webUrl: 'rookie',
+ status: null,
+ },
+ },
+ {
+ id: 'user-1',
+ user: {
+ __typename: 'UserCore',
+ id: 'gid://gitlab/User/1',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: '/root',
+ status: null,
+ },
+ },
+ ],
+ pageInfo: {
+ hasNextPage: true,
+ endCursor: 'endCursor',
+ startCursor: 'startCursor',
+ },
+ },
+ },
+ },
+};
+
+export const projectMembersResponseWithNoMatchingUsers = {
+ data: {
+ workspace: {
+ id: '1',
+ __typename: 'Project',
+ users: {
+ nodes: [],
+ pageInfo: {
+ endCursor: null,
+ hasNextPage: false,
+ startCursor: null,
+ },
},
},
},
diff --git a/spec/frontend/work_items/pages/create_work_item_spec.js b/spec/frontend/work_items/pages/create_work_item_spec.js
index fed8be3783a..15dac25b7d9 100644
--- a/spec/frontend/work_items/pages/create_work_item_spec.js
+++ b/spec/frontend/work_items/pages/create_work_item_spec.js
@@ -193,6 +193,8 @@ describe('Create work item component', () => {
wrapper.find('form').trigger('submit');
await waitForPromises();
- expect(findAlert().text()).toBe(CreateWorkItem.createErrorText);
+ expect(findAlert().text()).toBe(
+ 'Something went wrong when creating work item. Please try again.',
+ );
});
});
diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js
index 99dcd886f7b..ab370e2ca8b 100644
--- a/spec/frontend/work_items/router_spec.js
+++ b/spec/frontend/work_items/router_spec.js
@@ -1,5 +1,18 @@
import { mount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import workItemWeightSubscription from 'ee_component/work_items/graphql/work_item_weight.subscription.graphql';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import {
+ workItemDatesSubscriptionResponse,
+ workItemResponseFactory,
+ workItemTitleSubscriptionResponse,
+ workItemWeightSubscriptionResponse,
+} from 'jest/work_items/mock_data';
import App from '~/work_items/components/app.vue';
+import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
+import workItemDatesSubscription from '~/work_items/graphql/work_item_dates.subscription.graphql';
+import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subscription.graphql';
import CreateWorkItem from '~/work_items/pages/create_work_item.vue';
import WorkItemsRoot from '~/work_items/pages/work_item_root.vue';
import { createRouter } from '~/work_items/router';
@@ -7,26 +20,36 @@ import { createRouter } from '~/work_items/router';
describe('Work items router', () => {
let wrapper;
+ Vue.use(VueApollo);
+
+ const workItemQueryHandler = jest.fn().mockResolvedValue(workItemResponseFactory());
+ const datesSubscriptionHandler = jest.fn().mockResolvedValue(workItemDatesSubscriptionResponse);
+ const titleSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse);
+ const weightSubscriptionHandler = jest.fn().mockResolvedValue(workItemWeightSubscriptionResponse);
+
const createComponent = async (routeArg) => {
const router = createRouter('/work_item');
if (routeArg !== undefined) {
await router.push(routeArg);
}
+ const handlers = [
+ [workItemQuery, workItemQueryHandler],
+ [workItemDatesSubscription, datesSubscriptionHandler],
+ [workItemTitleSubscription, titleSubscriptionHandler],
+ ];
+
+ if (IS_EE) {
+ handlers.push([workItemWeightSubscription, weightSubscriptionHandler]);
+ }
+
wrapper = mount(App, {
+ apolloProvider: createMockApollo(handlers),
router,
provide: {
fullPath: 'full-path',
issuesListPath: 'full-path/-/issues',
},
- mocks: {
- $apollo: {
- queries: {
- workItem: {},
- workItemTypes: {},
- },
- },
- },
});
};