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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/work_items')
-rw-r--r--spec/frontend/work_items/components/item_title_spec.js2
-rw-r--r--spec/frontend/work_items/components/work_item_assignees_spec.js93
-rw-r--r--spec/frontend/work_items/components/work_item_description_spec.js222
-rw-r--r--spec/frontend/work_items/components/work_item_detail_modal_spec.js12
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_spec.js88
-rw-r--r--spec/frontend/work_items/components/work_item_state_spec.js14
-rw-r--r--spec/frontend/work_items/components/work_item_title_spec.js37
-rw-r--r--spec/frontend/work_items/components/work_item_weight_spec.js47
-rw-r--r--spec/frontend/work_items/mock_data.js151
-rw-r--r--spec/frontend/work_items/pages/work_item_detail_spec.js105
-rw-r--r--spec/frontend/work_items/pages/work_item_root_spec.js2
11 files changed, 731 insertions, 42 deletions
diff --git a/spec/frontend/work_items/components/item_title_spec.js b/spec/frontend/work_items/components/item_title_spec.js
index 0d85df25b4f..2c3f6ef8634 100644
--- a/spec/frontend/work_items/components/item_title_spec.js
+++ b/spec/frontend/work_items/components/item_title_spec.js
@@ -15,7 +15,7 @@ const createComponent = ({ title = 'Sample title', disabled = false } = {}) =>
describe('ItemTitle', () => {
let wrapper;
const mockUpdatedTitle = 'Updated title';
- const findInputEl = () => wrapper.find('span#item-title');
+ const findInputEl = () => wrapper.find('[aria-label="Title"]');
beforeEach(() => {
wrapper = createComponent();
diff --git a/spec/frontend/work_items/components/work_item_assignees_spec.js b/spec/frontend/work_items/components/work_item_assignees_spec.js
new file mode 100644
index 00000000000..0552fe5050e
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_assignees_spec.js
@@ -0,0 +1,93 @@
+import { GlLink, GlTokenSelector } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue';
+import localUpdateWorkItemMutation from '~/work_items/graphql/local_update_work_item.mutation.graphql';
+
+const mockAssignees = [
+ {
+ __typename: 'UserCore',
+ id: 'gid://gitlab/User/1',
+ avatarUrl: '',
+ webUrl: '',
+ name: 'John Doe',
+ username: 'doe_I',
+ },
+ {
+ __typename: 'UserCore',
+ id: 'gid://gitlab/User/2',
+ avatarUrl: '',
+ webUrl: '',
+ name: 'Marcus Rutherford',
+ username: 'ruthfull',
+ },
+];
+
+const workItemId = 'gid://gitlab/WorkItem/1';
+
+const mutate = jest.fn();
+
+describe('WorkItemAssignees component', () => {
+ let wrapper;
+
+ const findAssigneeLinks = () => wrapper.findAllComponents(GlLink);
+ const findTokenSelector = () => wrapper.findComponent(GlTokenSelector);
+
+ const findEmptyState = () => wrapper.findByTestId('empty-state');
+
+ const createComponent = ({ assignees = mockAssignees } = {}) => {
+ wrapper = mountExtended(WorkItemAssignees, {
+ propsData: {
+ assignees,
+ workItemId,
+ },
+ mocks: {
+ $apollo: {
+ mutate,
+ },
+ },
+ attachTo: document.body,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('should pass the correct data-user-id attribute', () => {
+ createComponent();
+
+ expect(findAssigneeLinks().at(0).attributes('data-user-id')).toBe('1');
+ });
+
+ describe('when there are assignees', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should focus token selector on token removal', async () => {
+ findTokenSelector().vm.$emit('token-remove', mockAssignees[0].id);
+ await nextTick();
+
+ expect(findEmptyState().exists()).toBe(false);
+ expect(findTokenSelector().element.contains(document.activeElement)).toBe(true);
+ });
+
+ it('should call a mutation on clicking outside the token selector', async () => {
+ findTokenSelector().vm.$emit('input', [mockAssignees[0]]);
+ findTokenSelector().vm.$emit('token-remove');
+ await nextTick();
+ expect(mutate).not.toHaveBeenCalled();
+
+ findTokenSelector().vm.$emit('blur', new FocusEvent({ relatedTarget: null }));
+ await nextTick();
+
+ expect(mutate).toHaveBeenCalledWith({
+ mutation: localUpdateWorkItemMutation,
+ variables: {
+ input: { id: workItemId, assigneeIds: [mockAssignees[0].id] },
+ },
+ });
+ });
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_description_spec.js b/spec/frontend/work_items/components/work_item_description_spec.js
new file mode 100644
index 00000000000..8017c46dea8
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_description_spec.js
@@ -0,0 +1,222 @@
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { mockTracking } from 'helpers/tracking_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { updateDraft } from '~/lib/utils/autosave';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
+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 {
+ updateWorkItemWidgetsResponse,
+ workItemResponseFactory,
+ workItemQueryResponse,
+} from '../mock_data';
+
+jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal', () => {
+ return {
+ confirmAction: jest.fn(),
+ };
+});
+jest.mock('~/lib/utils/autosave');
+
+const workItemId = workItemQueryResponse.data.workItem.id;
+
+describe('WorkItemDescription', () => {
+ let wrapper;
+
+ Vue.use(VueApollo);
+
+ const mutationSuccessHandler = jest.fn().mockResolvedValue(updateWorkItemWidgetsResponse);
+
+ const findEditButton = () => wrapper.find('[data-testid="edit-description"]');
+ const findMarkdownField = () => wrapper.findComponent(MarkdownField);
+
+ const editDescription = (newText) => wrapper.find('textarea').setValue(newText);
+
+ const clickCancel = () => wrapper.find('[data-testid="cancel"]').vm.$emit('click');
+ const clickSave = () => wrapper.find('[data-testid="save-description"]').vm.$emit('click', {});
+
+ const createComponent = async ({
+ mutationHandler = mutationSuccessHandler,
+ canUpdate = true,
+ isEditing = false,
+ } = {}) => {
+ const workItemResponse = workItemResponseFactory({ canUpdate });
+ const workItemResponseHandler = jest.fn().mockResolvedValue(workItemResponse);
+
+ const { id } = workItemQueryResponse.data.workItem;
+ wrapper = shallowMount(WorkItemDescription, {
+ apolloProvider: createMockApollo([
+ [workItemQuery, workItemResponseHandler],
+ [updateWorkItemWidgetsMutation, mutationHandler],
+ ]),
+ propsData: {
+ workItemId: id,
+ },
+ provide: {
+ fullPath: '/group/project',
+ },
+ stubs: {
+ MarkdownField,
+ },
+ });
+
+ await waitForPromises();
+
+ if (isEditing) {
+ findEditButton().vm.$emit('click');
+
+ await nextTick();
+ }
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('Edit button', () => {
+ it('is not visible when canUpdate = false', async () => {
+ await createComponent({
+ canUpdate: false,
+ });
+
+ expect(findEditButton().exists()).toBe(false);
+ });
+
+ it('toggles edit mode', async () => {
+ await createComponent({
+ canUpdate: true,
+ });
+
+ findEditButton().vm.$emit('click');
+
+ await nextTick();
+
+ expect(findMarkdownField().exists()).toBe(true);
+ });
+ });
+
+ describe('editing description', () => {
+ it('cancels when clicking cancel', async () => {
+ await createComponent({
+ isEditing: true,
+ });
+
+ clickCancel();
+
+ await nextTick();
+
+ expect(confirmAction).not.toHaveBeenCalled();
+ expect(findMarkdownField().exists()).toBe(false);
+ });
+
+ it('prompts for confirmation when clicking cancel after changes', async () => {
+ await createComponent({
+ isEditing: true,
+ });
+
+ editDescription('updated desc');
+
+ clickCancel();
+
+ await nextTick();
+
+ expect(confirmAction).toHaveBeenCalled();
+ });
+
+ it('calls update widgets mutation', async () => {
+ await createComponent({
+ isEditing: true,
+ });
+
+ editDescription('updated desc');
+
+ clickSave();
+
+ await waitForPromises();
+
+ expect(mutationSuccessHandler).toHaveBeenCalledWith({
+ input: {
+ id: workItemId,
+ descriptionWidget: {
+ description: 'updated desc',
+ },
+ },
+ });
+ });
+
+ it('tracks editing description', async () => {
+ await createComponent({
+ isEditing: true,
+ markdownPreviewPath: '/preview',
+ });
+ const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+
+ clickSave();
+
+ await waitForPromises();
+
+ expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'updated_description', {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'item_description',
+ property: 'type_Task',
+ });
+ });
+
+ it('emits error when mutation returns error', async () => {
+ const error = 'eror';
+
+ await createComponent({
+ isEditing: true,
+ mutationHandler: jest.fn().mockResolvedValue({
+ data: {
+ workItemUpdateWidgets: {
+ workItem: {},
+ errors: [error],
+ },
+ },
+ }),
+ });
+
+ editDescription('updated desc');
+
+ clickSave();
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[error]]);
+ });
+
+ it('emits error when mutation fails', async () => {
+ const error = 'eror';
+
+ await createComponent({
+ isEditing: true,
+ mutationHandler: jest.fn().mockRejectedValue(new Error(error)),
+ });
+
+ editDescription('updated desc');
+
+ clickSave();
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[error]]);
+ });
+
+ it('autosaves description', async () => {
+ await createComponent({
+ isEditing: true,
+ });
+
+ editDescription('updated desc');
+
+ expect(updateDraft).toHaveBeenCalled();
+ });
+ });
+});
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 aaabdbc82d9..d55ba318e46 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
@@ -29,7 +29,7 @@ describe('WorkItemDetailModal component', () => {
const findAlert = () => wrapper.findComponent(GlAlert);
const findWorkItemDetail = () => wrapper.findComponent(WorkItemDetail);
- const createComponent = ({ workItemId = '1', error = false } = {}) => {
+ const createComponent = ({ workItemId = '1', issueGid = '2', error = false } = {}) => {
const apolloProvider = createMockApollo([
[
deleteWorkItemFromTaskMutation,
@@ -46,7 +46,7 @@ describe('WorkItemDetailModal component', () => {
wrapper = shallowMount(WorkItemDetailModal, {
apolloProvider,
- propsData: { workItemId },
+ propsData: { workItemId, issueGid },
data() {
return {
error,
@@ -67,6 +67,7 @@ describe('WorkItemDetailModal component', () => {
expect(findWorkItemDetail().props()).toEqual({
workItemId: '1',
+ workItemParentId: '2',
});
});
@@ -97,13 +98,6 @@ describe('WorkItemDetailModal component', () => {
expect(wrapper.emitted('close')).toBeTruthy();
});
- it('emits `workItemUpdated` event on updating work item', () => {
- createComponent();
- findWorkItemDetail().vm.$emit('workItemUpdated');
-
- expect(wrapper.emitted('workItemUpdated')).toBeTruthy();
- });
-
describe('delete work item', () => {
it('emits workItemDeleted and closes modal', async () => {
createComponent();
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
new file mode 100644
index 00000000000..774e9198992
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
@@ -0,0 +1,88 @@
+import Vue, { nextTick } from 'vue';
+import { GlBadge } 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 WorkItemLinks from '~/work_items/components/work_item_links/work_item_links.vue';
+import getWorkItemLinksQuery from '~/work_items/graphql/work_item_links.query.graphql';
+import { workItemHierarchyResponse, workItemHierarchyEmptyResponse } from '../../mock_data';
+
+Vue.use(VueApollo);
+
+describe('WorkItemLinks', () => {
+ let wrapper;
+
+ const createComponent = async ({ response = workItemHierarchyResponse } = {}) => {
+ wrapper = shallowMountExtended(WorkItemLinks, {
+ apolloProvider: createMockApollo([
+ [getWorkItemLinksQuery, jest.fn().mockResolvedValue(response)],
+ ]),
+ propsData: { issuableId: 1 },
+ });
+
+ await waitForPromises();
+ };
+
+ 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');
+
+ beforeEach(async () => {
+ await createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('is expanded by default', () => {
+ expect(findToggleButton().props('icon')).toBe('chevron-lg-up');
+ expect(findLinksBody().exists()).toBe(true);
+ });
+
+ it('expands on click toggle button', async () => {
+ findToggleButton().vm.$emit('click');
+ await nextTick();
+
+ expect(findToggleButton().props('icon')).toBe('chevron-lg-down');
+ expect(findLinksBody().exists()).toBe(false);
+ });
+
+ describe('when no child links', () => {
+ beforeEach(async () => {
+ await createComponent({ response: workItemHierarchyEmptyResponse });
+ });
+
+ it('displays empty state if there are no children', () => {
+ expect(findEmptyState().exists()).toBe(true);
+ });
+
+ describe('add link form', () => {
+ it('displays form on click add button and hides form on cancel', async () => {
+ expect(findEmptyState().exists()).toBe(true);
+
+ findToggleAddFormButton().vm.$emit('click');
+ await nextTick();
+
+ expect(findAddLinksForm().exists()).toBe(true);
+
+ findAddLinksForm().vm.$emit('cancel');
+ await nextTick();
+
+ expect(findAddLinksForm().exists()).toBe(false);
+ });
+ });
+ });
+
+ it('renders all hierarchy widget children', () => {
+ expect(findLinksBody().exists()).toBe(true);
+
+ const children = wrapper.findAll('[data-testid="links-child"]');
+
+ expect(children).toHaveLength(4);
+ expect(children.at(0).findComponent(GlBadge).text()).toBe('Open');
+ });
+});
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 9e48f56d9e9..b379d1fc846 100644
--- a/spec/frontend/work_items/components/work_item_state_spec.js
+++ b/spec/frontend/work_items/components/work_item_state_spec.js
@@ -12,6 +12,7 @@ import {
STATE_CLOSED,
STATE_EVENT_CLOSE,
STATE_EVENT_REOPEN,
+ TRACKING_CATEGORY_SHOW,
} from '~/work_items/constants';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import { updateWorkItemMutationResponse, workItemQueryResponse } from '../mock_data';
@@ -81,15 +82,6 @@ describe('WorkItemState component', () => {
});
});
- it('emits updated event', async () => {
- createComponent();
-
- findItemState().vm.$emit('changed', STATE_CLOSED);
- await waitForPromises();
-
- expect(wrapper.emitted('updated')).toEqual([[]]);
- });
-
it('emits an error message when the mutation was unsuccessful', async () => {
createComponent({ mutationHandler: jest.fn().mockRejectedValue('Error!') });
@@ -107,8 +99,8 @@ describe('WorkItemState component', () => {
findItemState().vm.$emit('changed', STATE_CLOSED);
await waitForPromises();
- expect(trackingSpy).toHaveBeenCalledWith('workItems:show', 'updated_state', {
- category: 'workItems:show',
+ expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'updated_state', {
+ category: TRACKING_CATEGORY_SHOW,
label: 'item_state',
property: 'type_Task',
});
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 19b56362ac0..a48449bb636 100644
--- a/spec/frontend/work_items/components/work_item_title_spec.js
+++ b/spec/frontend/work_items/components/work_item_title_spec.js
@@ -6,8 +6,9 @@ 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 } from '~/work_items/constants';
+import { i18n, 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';
describe('WorkItemTitle component', () => {
@@ -19,14 +20,18 @@ describe('WorkItemTitle component', () => {
const findItemTitle = () => wrapper.findComponent(ItemTitle);
- const createComponent = ({ mutationHandler = mutationSuccessHandler } = {}) => {
+ const createComponent = ({ workItemParentId, mutationHandler = mutationSuccessHandler } = {}) => {
const { id, title, workItemType } = workItemQueryResponse.data.workItem;
wrapper = shallowMount(WorkItemTitle, {
- apolloProvider: createMockApollo([[updateWorkItemMutation, mutationHandler]]),
+ apolloProvider: createMockApollo([
+ [updateWorkItemMutation, mutationHandler],
+ [updateWorkItemTaskMutation, mutationHandler],
+ ]),
propsData: {
workItemId: id,
workItemTitle: title,
workItemType: workItemType.name,
+ workItemParentId,
},
});
};
@@ -57,13 +62,25 @@ describe('WorkItemTitle component', () => {
});
});
- it('emits updated event', async () => {
- createComponent();
+ it('calls WorkItemTaskUpdate if passed workItemParentId prop', () => {
+ const title = 'new title!';
+ const workItemParentId = '1234';
- findItemTitle().vm.$emit('title-changed', 'new title');
- await waitForPromises();
+ createComponent({
+ workItemParentId,
+ });
- expect(wrapper.emitted('updated')).toEqual([[]]);
+ findItemTitle().vm.$emit('title-changed', title);
+
+ expect(mutationSuccessHandler).toHaveBeenCalledWith({
+ input: {
+ id: workItemParentId,
+ taskData: {
+ id: workItemQueryResponse.data.workItem.id,
+ title,
+ },
+ },
+ });
});
it('does not call a mutation when the title has not changed', () => {
@@ -91,8 +108,8 @@ describe('WorkItemTitle component', () => {
findItemTitle().vm.$emit('title-changed', 'new title');
await waitForPromises();
- expect(trackingSpy).toHaveBeenCalledWith('workItems:show', 'updated_title', {
- category: 'workItems:show',
+ expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'updated_title', {
+ category: TRACKING_CATEGORY_SHOW,
label: 'item_title',
property: 'type_Task',
});
diff --git a/spec/frontend/work_items/components/work_item_weight_spec.js b/spec/frontend/work_items/components/work_item_weight_spec.js
new file mode 100644
index 00000000000..80a1d032ad7
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_weight_spec.js
@@ -0,0 +1,47 @@
+import { shallowMount } from '@vue/test-utils';
+import WorkItemWeight from '~/work_items/components/work_item_weight.vue';
+
+describe('WorkItemAssignees component', () => {
+ let wrapper;
+
+ const createComponent = ({ weight, hasIssueWeightsFeature = true } = {}) => {
+ wrapper = shallowMount(WorkItemWeight, {
+ propsData: {
+ weight,
+ },
+ provide: {
+ hasIssueWeightsFeature,
+ },
+ });
+ };
+
+ describe('weight 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(wrapper.find('div').exists()).toBe(exists);
+ });
+ });
+ });
+
+ describe('weight text', () => {
+ describe.each`
+ description | weight | text
+ ${'renders 1'} | ${1} | ${'1'}
+ ${'renders 0'} | ${0} | ${'0'}
+ ${'renders None'} | ${null} | ${'None'}
+ ${'renders None'} | ${undefined} | ${'None'}
+ `('when weight is $weight', ({ description, weight, text }) => {
+ it(description, () => {
+ createComponent({ weight });
+
+ expect(wrapper.text()).toContain(text);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index f3483550013..bf3f4e1364d 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -15,6 +15,15 @@ export const workItemQueryResponse = {
deleteWorkItem: false,
updateWorkItem: false,
},
+ widgets: [
+ {
+ __typename: 'WorkItemWidgetDescription',
+ type: 'DESCRIPTION',
+ description: 'some **great** text',
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:19" dir="auto">some <strong>great</strong> text</p>',
+ },
+ ],
},
},
};
@@ -38,11 +47,53 @@ export const updateWorkItemMutationResponse = {
deleteWorkItem: false,
updateWorkItem: false,
},
+ widgets: [],
},
},
},
};
+export const workItemResponseFactory = ({ canUpdate } = {}) => ({
+ data: {
+ workItem: {
+ __typename: 'WorkItem',
+ id: 'gid://gitlab/WorkItem/1',
+ title: 'Updated title',
+ state: 'OPEN',
+ description: 'description',
+ workItemType: {
+ __typename: 'WorkItemType',
+ id: 'gid://gitlab/WorkItems::Type/5',
+ name: 'Task',
+ },
+ userPermissions: {
+ deleteWorkItem: false,
+ updateWorkItem: canUpdate,
+ },
+ widgets: [
+ {
+ __typename: 'WorkItemWidgetDescription',
+ type: 'DESCRIPTION',
+ description: 'some **great** text',
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:19" dir="auto">some <strong>great</strong> text</p>',
+ },
+ ],
+ },
+ },
+});
+
+export const updateWorkItemWidgetsResponse = {
+ data: {
+ workItemUpdateWidgets: {
+ workItem: {
+ id: 1234,
+ },
+ errors: [],
+ },
+ },
+};
+
export const projectWorkItemTypesQueryResponse = {
data: {
workspace: {
@@ -77,6 +128,7 @@ export const createWorkItemMutationResponse = {
deleteWorkItem: false,
updateWorkItem: false,
},
+ widgets: [],
},
},
},
@@ -124,3 +176,102 @@ export const workItemTitleSubscriptionResponse = {
},
},
};
+
+export const workItemHierarchyEmptyResponse = {
+ data: {
+ workItem: {
+ id: 'gid://gitlab/WorkItem/1',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/6',
+ __typename: 'WorkItemType',
+ },
+ title: 'New title',
+ widgets: [
+ {
+ type: 'DESCRIPTION',
+ __typename: 'WorkItemWidgetDescription',
+ },
+ {
+ type: 'HIERARCHY',
+ parent: null,
+ children: {
+ nodes: [],
+ __typename: 'WorkItemConnection',
+ },
+ __typename: 'WorkItemWidgetHierarchy',
+ },
+ ],
+ __typename: 'WorkItem',
+ },
+ },
+};
+
+export const workItemHierarchyResponse = {
+ data: {
+ workItem: {
+ id: 'gid://gitlab/WorkItem/1',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/6',
+ __typename: 'WorkItemType',
+ },
+ title: 'New title',
+ widgets: [
+ {
+ type: 'DESCRIPTION',
+ __typename: 'WorkItemWidgetDescription',
+ },
+ {
+ type: 'HIERARCHY',
+ parent: null,
+ children: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WorkItem/2',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/5',
+ __typename: 'WorkItemType',
+ },
+ title: 'xyz',
+ state: 'OPEN',
+ __typename: 'WorkItem',
+ },
+ {
+ id: 'gid://gitlab/WorkItem/3',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/5',
+ __typename: 'WorkItemType',
+ },
+ title: 'abc',
+ state: 'CLOSED',
+ __typename: 'WorkItem',
+ },
+ {
+ id: 'gid://gitlab/WorkItem/4',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/5',
+ __typename: 'WorkItemType',
+ },
+ title: 'bar',
+ state: 'OPEN',
+ __typename: 'WorkItem',
+ },
+ {
+ id: 'gid://gitlab/WorkItem/5',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/5',
+ __typename: 'WorkItemType',
+ },
+ title: 'foobar',
+ state: 'OPEN',
+ __typename: 'WorkItem',
+ },
+ ],
+ __typename: 'WorkItemConnection',
+ },
+ __typename: 'WorkItemWidgetHierarchy',
+ },
+ ],
+ __typename: 'WorkItem',
+ },
+ },
+};
diff --git a/spec/frontend/work_items/pages/work_item_detail_spec.js b/spec/frontend/work_items/pages/work_item_detail_spec.js
index 9f87655175c..b9724034cb4 100644
--- a/spec/frontend/work_items/pages/work_item_detail_spec.js
+++ b/spec/frontend/work_items/pages/work_item_detail_spec.js
@@ -5,11 +5,15 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import WorkItemDetail from '~/work_items/components/work_item_detail.vue';
+import WorkItemDescription from '~/work_items/components/work_item_description.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 WorkItemWeight from '~/work_items/components/work_item_weight.vue';
import { i18n } from '~/work_items/constants';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subscription.graphql';
+import { temporaryConfig } from '~/work_items/graphql/provider';
import { workItemTitleSubscriptionResponse, workItemQueryResponse } from '../mock_data';
describe('WorkItemDetail component', () => {
@@ -24,18 +28,34 @@ describe('WorkItemDetail component', () => {
const findSkeleton = () => wrapper.findComponent(GlSkeletonLoader);
const findWorkItemTitle = () => wrapper.findComponent(WorkItemTitle);
const findWorkItemState = () => wrapper.findComponent(WorkItemState);
+ const findWorkItemDescription = () => wrapper.findComponent(WorkItemDescription);
+ const findWorkItemAssignees = () => wrapper.findComponent(WorkItemAssignees);
+ const findWorkItemWeight = () => wrapper.findComponent(WorkItemWeight);
const createComponent = ({
workItemId = workItemQueryResponse.data.workItem.id,
handler = successHandler,
subscriptionHandler = initialSubscriptionHandler,
+ workItemsMvc2Enabled = false,
+ includeWidgets = false,
} = {}) => {
wrapper = shallowMount(WorkItemDetail, {
- apolloProvider: createMockApollo([
- [workItemQuery, handler],
- [workItemTitleSubscription, subscriptionHandler],
- ]),
+ apolloProvider: createMockApollo(
+ [
+ [workItemQuery, handler],
+ [workItemTitleSubscription, subscriptionHandler],
+ ],
+ {},
+ {
+ typePolicies: includeWidgets ? temporaryConfig.cacheConfig.typePolicies : {},
+ },
+ ),
propsData: { workItemId },
+ provide: {
+ glFeatures: {
+ workItemsMvc2: workItemsMvc2Enabled,
+ },
+ },
});
};
@@ -78,6 +98,22 @@ describe('WorkItemDetail component', () => {
});
});
+ describe('description', () => {
+ it('does not show description widget if loading description fails', () => {
+ createComponent();
+
+ expect(findWorkItemDescription().exists()).toBe(false);
+ });
+
+ it('shows description widget if description loads', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findWorkItemDescription().exists()).toBe(true);
+ });
+ });
+
it('shows an error message when the work item query was unsuccessful', async () => {
const errorHandler = jest.fn().mockRejectedValue('Oops');
createComponent({ handler: errorHandler });
@@ -105,17 +141,64 @@ describe('WorkItemDetail component', () => {
});
});
- it('emits workItemUpdated event when fields updated', async () => {
- createComponent();
+ describe('when work_items_mvc_2 feature flag is enabled', () => {
+ it('renders assignees component when assignees widget is returned from the API', async () => {
+ createComponent({
+ workItemsMvc2Enabled: true,
+ includeWidgets: true,
+ });
+ await waitForPromises();
- await waitForPromises();
+ expect(findWorkItemAssignees().exists()).toBe(true);
+ });
- findWorkItemState().vm.$emit('updated');
+ it('does not render assignees component when assignees widget is not returned from the API', async () => {
+ createComponent({
+ workItemsMvc2Enabled: true,
+ includeWidgets: false,
+ });
+ await waitForPromises();
- expect(wrapper.emitted('workItemUpdated')).toEqual([[]]);
+ expect(findWorkItemAssignees().exists()).toBe(false);
+ });
+ });
- findWorkItemTitle().vm.$emit('updated');
+ it('does not render assignees component when assignees feature flag is disabled', async () => {
+ createComponent();
+ await waitForPromises();
- expect(wrapper.emitted('workItemUpdated')).toEqual([[], []]);
+ expect(findWorkItemAssignees().exists()).toBe(false);
+ });
+
+ describe('weight widget', () => {
+ describe('when work_items_mvc_2 feature flag is enabled', () => {
+ describe.each`
+ description | includeWidgets | exists
+ ${'when widget is returned from API'} | ${true} | ${true}
+ ${'when widget is not returned from API'} | ${false} | ${false}
+ `('$description', ({ includeWidgets, exists }) => {
+ it(`${includeWidgets ? 'renders' : 'does not render'} weight component`, async () => {
+ createComponent({ includeWidgets, workItemsMvc2Enabled: true });
+ await waitForPromises();
+
+ expect(findWorkItemWeight().exists()).toBe(exists);
+ });
+ });
+ });
+
+ describe('when work_items_mvc_2 feature flag is disabled', () => {
+ describe.each`
+ description | includeWidgets | exists
+ ${'when widget is returned from API'} | ${true} | ${false}
+ ${'when widget is not returned from API'} | ${false} | ${false}
+ `('$description', ({ includeWidgets, exists }) => {
+ it(`${includeWidgets ? 'renders' : 'does not render'} weight component`, async () => {
+ createComponent({ includeWidgets, workItemsMvc2Enabled: false });
+ await waitForPromises();
+
+ expect(findWorkItemWeight().exists()).toBe(exists);
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/work_items/pages/work_item_root_spec.js b/spec/frontend/work_items/pages/work_item_root_spec.js
index 85096392e84..3c5da94114e 100644
--- a/spec/frontend/work_items/pages/work_item_root_spec.js
+++ b/spec/frontend/work_items/pages/work_item_root_spec.js
@@ -11,6 +11,7 @@ import deleteWorkItem from '~/work_items/graphql/delete_work_item.mutation.graph
import { deleteWorkItemResponse, deleteWorkItemFailureResponse } from '../mock_data';
jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
visitUrl: jest.fn(),
}));
@@ -52,6 +53,7 @@ describe('Work items root component', () => {
expect(findWorkItemDetail().props()).toEqual({
workItemId: 'gid://gitlab/WorkItem/1',
+ workItemParentId: null,
});
});