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.js9
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_actions_spec.js5
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_awards_list_spec.js82
-rw-r--r--spec/frontend/work_items/components/notes/work_item_notes_activity_header_spec.js18
-rw-r--r--spec/frontend/work_items/components/work_item_ancestors/disclosure_hierarchy_item_spec.js53
-rw-r--r--spec/frontend/work_items/components/work_item_ancestors/disclosure_hierarchy_spec.js99
-rw-r--r--spec/frontend/work_items/components/work_item_ancestors/mock_data.js197
-rw-r--r--spec/frontend/work_items/components/work_item_ancestors/work_item_ancestors_spec.js117
-rw-r--r--spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js73
-rw-r--r--spec/frontend/work_items/components/work_item_detail_modal_spec.js1
-rw-r--r--spec/frontend/work_items/components/work_item_detail_spec.js219
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_actions_split_button_spec.js (renamed from spec/frontend/work_items/components/work_item_links/okr_actions_split_button_spec.js)34
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js44
-rw-r--r--spec/frontend/work_items/components/work_item_notes_spec.js27
-rw-r--r--spec/frontend/work_items/components/work_item_parent_inline_spec.js (renamed from spec/frontend/work_items/components/work_item_parent_spec.js)6
-rw-r--r--spec/frontend/work_items/components/work_item_parent_with_edit_spec.js409
-rw-r--r--spec/frontend/work_items/components/work_item_state_toggle_spec.js (renamed from spec/frontend/work_items/components/work_item_state_toggle_button_spec.js)0
-rw-r--r--spec/frontend/work_items/components/work_item_sticky_header_spec.js59
-rw-r--r--spec/frontend/work_items/components/work_item_title_spec.js34
-rw-r--r--spec/frontend/work_items/mock_data.js51
-rw-r--r--spec/frontend/work_items/notes/award_utils_spec.js18
-rw-r--r--spec/frontend/work_items/pages/create_work_item_spec.js2
-rw-r--r--spec/frontend/work_items/pages/work_item_root_spec.js1
23 files changed, 1287 insertions, 271 deletions
diff --git a/spec/frontend/work_items/components/item_title_spec.js b/spec/frontend/work_items/components/item_title_spec.js
index 3a84ba4bd5e..660ff671a80 100644
--- a/spec/frontend/work_items/components/item_title_spec.js
+++ b/spec/frontend/work_items/components/item_title_spec.js
@@ -2,11 +2,12 @@ import { shallowMount } from '@vue/test-utils';
import { escape } from 'lodash';
import ItemTitle from '~/work_items/components/item_title.vue';
-const createComponent = ({ title = 'Sample title', disabled = false } = {}) =>
+const createComponent = ({ title = 'Sample title', disabled = false, useH1 = false } = {}) =>
shallowMount(ItemTitle, {
propsData: {
title,
disabled,
+ useH1,
},
});
@@ -27,6 +28,12 @@ describe('ItemTitle', () => {
expect(findInputEl().text()).toBe('Sample title');
});
+ it('renders H1 if useH1 is true, otherwise renders H2', () => {
+ expect(wrapper.element.tagName).toBe('H2');
+ wrapper = createComponent({ useH1: true });
+ expect(wrapper.element.tagName).toBe('H1');
+ });
+
it('renders title contents with editing disabled', () => {
wrapper = createComponent({
disabled: true,
diff --git a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
index 596283a9590..97aed1d548e 100644
--- a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
@@ -1,4 +1,4 @@
-import { GlButton, GlDisclosureDropdown } from '@gitlab/ui';
+import { GlDisclosureDropdown } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -17,7 +17,7 @@ describe('Work Item Note Actions', () => {
const showSpy = jest.fn();
const findReplyButton = () => wrapper.findComponent(ReplyButton);
- const findEditButton = () => wrapper.findComponent(GlButton);
+ const findEditButton = () => wrapper.findByTestId('note-actions-edit');
const findEmojiButton = () => wrapper.findByTestId('note-emoji-button');
const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
const findDeleteNoteButton = () => wrapper.findByTestId('delete-note-action');
@@ -64,6 +64,7 @@ describe('Work Item Note Actions', () => {
projectName,
},
provide: {
+ isGroup: false,
glFeatures: {
workItemsMvc2: true,
},
diff --git a/spec/frontend/work_items/components/notes/work_item_note_awards_list_spec.js b/spec/frontend/work_items/components/notes/work_item_note_awards_list_spec.js
index ce915635946..6ce4c09329f 100644
--- a/spec/frontend/work_items/components/notes/work_item_note_awards_list_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_note_awards_list_spec.js
@@ -9,6 +9,7 @@ import AwardsList from '~/vue_shared/components/awards_list.vue';
import WorkItemNoteAwardsList from '~/work_items/components/notes/work_item_note_awards_list.vue';
import addAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_add_award_emoji.mutation.graphql';
import removeAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_remove_award_emoji.mutation.graphql';
+import groupWorkItemNotesByIidQuery from '~/work_items/graphql/notes/group_work_item_notes_by_iid.query.graphql';
import workItemNotesByIidQuery from '~/work_items/graphql/notes/work_item_notes_by_iid.query.graphql';
import {
mockWorkItemNotesResponseWithComments,
@@ -45,7 +46,9 @@ describe('Work Item Note Awards List', () => {
const findAwardsList = () => wrapper.findComponent(AwardsList);
const createComponent = ({
+ isGroup = false,
note = firstNote,
+ query = workItemNotesByIidQuery,
addAwardEmojiMutationHandler = addAwardEmojiMutationSuccessHandler,
removeAwardEmojiMutationHandler = removeAwardEmojiMutationSuccessHandler,
} = {}) => {
@@ -55,12 +58,15 @@ describe('Work Item Note Awards List', () => {
]);
apolloProvider.clients.defaultClient.writeQuery({
- query: workItemNotesByIidQuery,
+ query,
variables: { fullPath, iid: workItemIid },
...mockWorkItemNotesResponseWithComments,
});
wrapper = shallowMount(WorkItemNoteAwardsList, {
+ provide: {
+ isGroup,
+ },
propsData: {
fullPath,
workItemIid,
@@ -89,54 +95,58 @@ describe('Work Item Note Awards List', () => {
expect(findAwardsList().props('canAwardEmoji')).toBe(hasAwardEmojiPermission);
});
- it('adds award if not already awarded', async () => {
- createComponent();
- await waitForPromises();
-
- findAwardsList().vm.$emit('award', EMOJI_THUMBSUP);
-
- expect(addAwardEmojiMutationSuccessHandler).toHaveBeenCalledWith({
- awardableId: firstNote.id,
- name: EMOJI_THUMBSUP,
- });
- });
+ it.each`
+ isGroup | query
+ ${true} | ${groupWorkItemNotesByIidQuery}
+ ${false} | ${workItemNotesByIidQuery}
+ `(
+ 'adds award if not already awarded in both group and project contexts',
+ async ({ isGroup, query }) => {
+ createComponent({ isGroup, query });
+ await waitForPromises();
+
+ findAwardsList().vm.$emit('award', EMOJI_THUMBSUP);
+
+ expect(addAwardEmojiMutationSuccessHandler).toHaveBeenCalledWith({
+ awardableId: firstNote.id,
+ name: EMOJI_THUMBSUP,
+ });
+ },
+ );
it('emits error if awarding emoji fails', async () => {
- createComponent({
- addAwardEmojiMutationHandler: jest.fn().mockRejectedValue('oh no'),
- });
- await waitForPromises();
+ createComponent({ addAwardEmojiMutationHandler: jest.fn().mockRejectedValue('oh no') });
findAwardsList().vm.$emit('award', EMOJI_THUMBSUP);
-
await waitForPromises();
expect(wrapper.emitted('error')).toEqual([[__('Failed to add emoji. Please try again')]]);
});
- it('removes award if already awarded', async () => {
- const removeAwardEmojiMutationHandler = removeAwardEmojiMutationSuccessHandler;
-
- createComponent({ removeAwardEmojiMutationHandler });
-
- findAwardsList().vm.$emit('award', EMOJI_THUMBSDOWN);
-
- await waitForPromises();
-
- expect(removeAwardEmojiMutationHandler).toHaveBeenCalledWith({
- awardableId: firstNote.id,
- name: EMOJI_THUMBSDOWN,
- });
- });
+ it.each`
+ isGroup | query
+ ${true} | ${groupWorkItemNotesByIidQuery}
+ ${false} | ${workItemNotesByIidQuery}
+ `(
+ 'removes award if already awarded in both group and project contexts',
+ async ({ isGroup, query }) => {
+ const removeAwardEmojiMutationHandler = removeAwardEmojiMutationSuccessHandler;
+ createComponent({ isGroup, query, removeAwardEmojiMutationHandler });
+
+ findAwardsList().vm.$emit('award', EMOJI_THUMBSDOWN);
+ await waitForPromises();
+
+ expect(removeAwardEmojiMutationHandler).toHaveBeenCalledWith({
+ awardableId: firstNote.id,
+ name: EMOJI_THUMBSDOWN,
+ });
+ },
+ );
it('restores award if remove fails', async () => {
- createComponent({
- removeAwardEmojiMutationHandler: jest.fn().mockRejectedValue('oh no'),
- });
- await waitForPromises();
+ createComponent({ removeAwardEmojiMutationHandler: jest.fn().mockRejectedValue('oh no') });
findAwardsList().vm.$emit('award', EMOJI_THUMBSDOWN);
-
await waitForPromises();
expect(wrapper.emitted('error')).toEqual([[__('Failed to remove emoji. Please try again')]]);
diff --git a/spec/frontend/work_items/components/notes/work_item_notes_activity_header_spec.js b/spec/frontend/work_items/components/notes/work_item_notes_activity_header_spec.js
index daf74f7a93b..dff54fef9fe 100644
--- a/spec/frontend/work_items/components/notes/work_item_notes_activity_header_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_notes_activity_header_spec.js
@@ -9,7 +9,8 @@ import {
describe('Work Item Note Activity Header', () => {
let wrapper;
- const findActivityLabelHeading = () => wrapper.find('h3');
+ const findActivityLabelH2Heading = () => wrapper.find('h2');
+ const findActivityLabelH3Heading = () => wrapper.find('h3');
const findActivityFilterDropdown = () => wrapper.findByTestId('work-item-filter');
const findActivitySortDropdown = () => wrapper.findByTestId('work-item-sort');
@@ -18,6 +19,7 @@ describe('Work Item Note Activity Header', () => {
sortOrder = ASC,
workItemType = 'Task',
discussionFilter = WORK_ITEM_NOTES_FILTER_ALL_NOTES,
+ useH2 = false,
} = {}) => {
wrapper = shallowMountExtended(WorkItemNotesActivityHeader, {
propsData: {
@@ -25,6 +27,7 @@ describe('Work Item Note Activity Header', () => {
sortOrder,
workItemType,
discussionFilter,
+ useH2,
},
});
};
@@ -34,7 +37,18 @@ describe('Work Item Note Activity Header', () => {
});
it('Should have the Activity label', () => {
- expect(findActivityLabelHeading().text()).toBe(WorkItemNotesActivityHeader.i18n.activityLabel);
+ expect(findActivityLabelH3Heading().text()).toBe(
+ WorkItemNotesActivityHeader.i18n.activityLabel,
+ );
+ });
+
+ it('Should render an H2 instead of an H3 if useH2 is true', () => {
+ createComponent();
+ expect(findActivityLabelH3Heading().exists()).toBe(true);
+ expect(findActivityLabelH2Heading().exists()).toBe(false);
+ createComponent({ useH2: true });
+ expect(findActivityLabelH2Heading().exists()).toBe(true);
+ expect(findActivityLabelH3Heading().exists()).toBe(false);
});
it('Should have Activity filtering dropdown', () => {
diff --git a/spec/frontend/work_items/components/work_item_ancestors/disclosure_hierarchy_item_spec.js b/spec/frontend/work_items/components/work_item_ancestors/disclosure_hierarchy_item_spec.js
new file mode 100644
index 00000000000..2cfe61654ad
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_ancestors/disclosure_hierarchy_item_spec.js
@@ -0,0 +1,53 @@
+import { GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+
+import DisclosureHierarchyItem from '~/work_items/components/work_item_ancestors/disclosure_hierarchy_item.vue';
+import { mockDisclosureHierarchyItems } from './mock_data';
+
+describe('DisclosurePathItem', () => {
+ let wrapper;
+
+ const findIcon = () => wrapper.findComponent(GlIcon);
+
+ const createComponent = (props = {}, options = {}) => {
+ return shallowMount(DisclosureHierarchyItem, {
+ propsData: {
+ item: mockDisclosureHierarchyItems[0],
+ ...props,
+ },
+ ...options,
+ });
+ };
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ describe('renders the item', () => {
+ it('renders the inline icon', () => {
+ expect(findIcon().exists()).toBe(true);
+ expect(findIcon().props('name')).toBe(mockDisclosureHierarchyItems[0].icon);
+ });
+ });
+
+ describe('item slot', () => {
+ beforeEach(() => {
+ wrapper = createComponent(null, {
+ scopedSlots: {
+ default: `
+ <div
+ data-testid="item-slot-content">
+ {{ props.item.title }}
+ </div>
+ `,
+ },
+ });
+ });
+
+ it('contains all elements passed into the additional slot', () => {
+ const item = wrapper.find('[data-testid="item-slot-content"]');
+
+ expect(item.text()).toBe(mockDisclosureHierarchyItems[0].title);
+ });
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_ancestors/disclosure_hierarchy_spec.js b/spec/frontend/work_items/components/work_item_ancestors/disclosure_hierarchy_spec.js
new file mode 100644
index 00000000000..b808c13c3e7
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_ancestors/disclosure_hierarchy_spec.js
@@ -0,0 +1,99 @@
+import { shallowMount } from '@vue/test-utils';
+
+import { GlDisclosureDropdown, GlTooltip } from '@gitlab/ui';
+import DisclosureHierarchy from '~/work_items/components/work_item_ancestors//disclosure_hierarchy.vue';
+import DisclosureHierarchyItem from '~/work_items/components/work_item_ancestors/disclosure_hierarchy_item.vue';
+import { mockDisclosureHierarchyItems } from './mock_data';
+
+describe('DisclosurePath', () => {
+ let wrapper;
+
+ const createComponent = (props = {}, options = {}) => {
+ return shallowMount(DisclosureHierarchy, {
+ propsData: {
+ items: mockDisclosureHierarchyItems,
+ ...props,
+ },
+ ...options,
+ });
+ };
+
+ const listItems = () => wrapper.findAllComponents(DisclosureHierarchyItem);
+ const itemAt = (index) => listItems().at(index);
+ const itemTextAt = (index) => itemAt(index).props('item').title;
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ describe('renders the list of items', () => {
+ it('renders the correct number of items', () => {
+ expect(listItems().length).toBe(mockDisclosureHierarchyItems.length);
+ });
+
+ it('renders the items in the correct order', () => {
+ expect(itemTextAt(0)).toContain(mockDisclosureHierarchyItems[0].title);
+ expect(itemTextAt(4)).toContain(mockDisclosureHierarchyItems[4].title);
+ expect(itemTextAt(9)).toContain(mockDisclosureHierarchyItems[9].title);
+ });
+ });
+
+ describe('slots', () => {
+ beforeEach(() => {
+ wrapper = createComponent(null, {
+ scopedSlots: {
+ default: `
+ <div
+ :data-itemid="props.itemId"
+ data-testid="item-slot-content">
+ {{ props.item.title }}
+ </div>
+ `,
+ },
+ });
+ });
+
+ it('contains all elements passed into the default slot', () => {
+ mockDisclosureHierarchyItems.forEach((item, index) => {
+ const disclosureItem = wrapper.findAll('[data-testid="item-slot-content"]').at(index);
+
+ expect(disclosureItem.text()).toBe(item.title);
+ expect(disclosureItem.attributes('data-itemid')).toContain('disclosure-');
+ });
+ });
+ });
+
+ describe('with ellipsis', () => {
+ const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
+ const findTooltip = () => wrapper.findComponent(GlTooltip);
+ const findTooltipText = () => findTooltip().text();
+ const tooltipText = 'Display more items';
+
+ beforeEach(() => {
+ wrapper = createComponent({ withEllipsis: true, ellipsisTooltipLabel: tooltipText });
+ });
+
+ describe('renders items and dropdown', () => {
+ it('renders 2 items', () => {
+ expect(listItems().length).toBe(2);
+ });
+
+ it('renders first and last items', () => {
+ expect(itemTextAt(0)).toContain(mockDisclosureHierarchyItems[0].title);
+ expect(itemTextAt(1)).toContain(
+ mockDisclosureHierarchyItems[mockDisclosureHierarchyItems.length - 1].title,
+ );
+ });
+
+ it('renders dropdown with the rest of the items passed down', () => {
+ expect(findDropdown().exists()).toBe(true);
+ expect(findDropdown().props('items').length).toBe(mockDisclosureHierarchyItems.length - 2);
+ });
+
+ it('renders tooltip with text passed as prop', () => {
+ expect(findTooltip().exists()).toBe(true);
+ expect(findTooltipText()).toBe(tooltipText);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_ancestors/mock_data.js b/spec/frontend/work_items/components/work_item_ancestors/mock_data.js
new file mode 100644
index 00000000000..8e7f99658de
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_ancestors/mock_data.js
@@ -0,0 +1,197 @@
+export const mockDisclosureHierarchyItems = [
+ {
+ title: 'First',
+ icon: 'epic',
+ href: '#',
+ },
+ {
+ title: 'Second',
+ icon: 'epic',
+ href: '#',
+ },
+ {
+ title: 'Third',
+ icon: 'epic',
+ href: '#',
+ },
+ {
+ title: 'Fourth',
+ icon: 'epic',
+ href: '#',
+ },
+ {
+ title: 'Fifth',
+ icon: 'issues',
+ href: '#',
+ },
+ {
+ title: 'Sixth',
+ icon: 'issues',
+ href: '#',
+ },
+ {
+ title: 'Seventh',
+ icon: 'issues',
+ href: '#',
+ },
+ {
+ title: 'Eighth',
+ icon: 'issue-type-task',
+ href: '#',
+ disabled: true,
+ },
+ {
+ title: 'Ninth',
+ icon: 'issue-type-task',
+ href: '#',
+ },
+ {
+ title: 'Tenth',
+ icon: 'issue-type-task',
+ href: '#',
+ },
+];
+
+export const workItemAncestorsQueryResponse = {
+ data: {
+ workItem: {
+ __typename: 'WorkItem',
+ id: 'gid://gitlab/WorkItem/1',
+ title: 'Test',
+ widgets: [
+ {
+ __typename: 'WorkItemWidgetHierarchy',
+ type: 'HIERARCHY',
+ parent: {
+ id: 'gid://gitlab/Issue/1',
+ },
+ ancestors: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WorkItem/444',
+ iid: '4',
+ reference: '#40',
+ createdAt: '2022-08-03T12:41:54Z',
+ closedAt: null,
+ confidential: false,
+ title: '123',
+ state: 'OPEN',
+ webUrl: '/gitlab-org/gitlab-test/-/work_items/4',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/2',
+ name: 'Issue',
+ iconName: 'issue-type-issue',
+ },
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+};
+
+export const workItemThreeAncestorsQueryResponse = {
+ data: {
+ workItem: {
+ __typename: 'WorkItem',
+ id: 'gid://gitlab/WorkItem/1',
+ title: 'Test',
+ workItemType: {
+ __typename: 'WorkItemType',
+ id: 'gid://gitlab/WorkItems::Type/5',
+ name: 'Task',
+ iconName: 'issue-type-task',
+ },
+ widgets: [
+ {
+ __typename: 'WorkItemWidgetHierarchy',
+ type: 'HIERARCHY',
+ parent: {
+ id: 'gid://gitlab/Issue/1',
+ },
+ ancestors: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WorkItem/444',
+ iid: '4',
+ reference: '#40',
+ createdAt: '2022-08-03T12:41:54Z',
+ closedAt: null,
+ confidential: false,
+ title: '123',
+ state: 'OPEN',
+ webUrl: '/gitlab-org/gitlab-test/-/work_items/4',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/2',
+ name: 'Issue',
+ iconName: 'issue-type-issue',
+ },
+ },
+ {
+ id: 'gid://gitlab/WorkItem/445',
+ iid: '5',
+ reference: '#41',
+ createdAt: '2022-08-03T12:41:54Z',
+ closedAt: null,
+ confidential: false,
+ title: '1234',
+ state: 'OPEN',
+ webUrl: '/gitlab-org/gitlab-test/-/work_items/5',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/2',
+ name: 'Issue',
+ iconName: 'issue-type-issue',
+ },
+ },
+ {
+ id: 'gid://gitlab/WorkItem/446',
+ iid: '6',
+ reference: '#42',
+ createdAt: '2022-08-03T12:41:54Z',
+ closedAt: null,
+ confidential: false,
+ title: '12345',
+ state: 'OPEN',
+ webUrl: '/gitlab-org/gitlab-test/-/work_items/6',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/2',
+ name: 'Issue',
+ iconName: 'issue-type-issue',
+ },
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+};
+
+export const workItemEmptyAncestorsQueryResponse = {
+ data: {
+ workItem: {
+ __typename: 'WorkItem',
+ id: 'gid://gitlab/WorkItem/1',
+ title: 'Test',
+ workItemType: {
+ __typename: 'WorkItemType',
+ id: 'gid://gitlab/WorkItems::Type/5',
+ name: 'Task',
+ iconName: 'issue-type-task',
+ },
+ widgets: [
+ {
+ __typename: 'WorkItemWidgetHierarchy',
+ type: 'HIERARCHY',
+ parent: {
+ id: null,
+ },
+ ancestors: {
+ nodes: [],
+ },
+ },
+ ],
+ },
+ },
+};
diff --git a/spec/frontend/work_items/components/work_item_ancestors/work_item_ancestors_spec.js b/spec/frontend/work_items/components/work_item_ancestors/work_item_ancestors_spec.js
new file mode 100644
index 00000000000..a9f66b20f06
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_ancestors/work_item_ancestors_spec.js
@@ -0,0 +1,117 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlPopover } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+
+import { createAlert } from '~/alert';
+import DisclosureHierarchy from '~/work_items/components/work_item_ancestors/disclosure_hierarchy.vue';
+import WorkItemAncestors from '~/work_items/components/work_item_ancestors/work_item_ancestors.vue';
+import workItemAncestorsQuery from '~/work_items/graphql/work_item_ancestors.query.graphql';
+import { formatAncestors } from '~/work_items/utils';
+
+import { workItemTask } from '../../mock_data';
+import {
+ workItemAncestorsQueryResponse,
+ workItemEmptyAncestorsQueryResponse,
+ workItemThreeAncestorsQueryResponse,
+} from './mock_data';
+
+Vue.use(VueApollo);
+jest.mock('~/alert');
+
+describe('WorkItemAncestors', () => {
+ let wrapper;
+ let mockApollo;
+
+ const workItemAncestorsQueryHandler = jest.fn().mockResolvedValue(workItemAncestorsQueryResponse);
+ const workItemEmptyAncestorsQueryHandler = jest
+ .fn()
+ .mockResolvedValue(workItemEmptyAncestorsQueryResponse);
+ const workItemThreeAncestorsQueryHandler = jest
+ .fn()
+ .mockResolvedValue(workItemThreeAncestorsQueryResponse);
+ const workItemAncestorsFailureHandler = jest.fn().mockRejectedValue(new Error());
+
+ const findDisclosureHierarchy = () => wrapper.findComponent(DisclosureHierarchy);
+ const findPopover = () => wrapper.findComponent(GlPopover);
+
+ const createComponent = ({
+ props = {},
+ options = {},
+ ancestorsQueryHandler = workItemAncestorsQueryHandler,
+ } = {}) => {
+ mockApollo = createMockApollo([[workItemAncestorsQuery, ancestorsQueryHandler]]);
+ return mountExtended(WorkItemAncestors, {
+ apolloProvider: mockApollo,
+ propsData: {
+ workItem: workItemTask,
+ ...props,
+ },
+ ...options,
+ });
+ };
+
+ beforeEach(async () => {
+ createAlert.mockClear();
+ wrapper = createComponent();
+ await waitForPromises();
+ });
+
+ it('fetches work item ancestors', () => {
+ expect(workItemAncestorsQueryHandler).toHaveBeenCalled();
+ });
+
+ it('displays DisclosureHierarchy component with ancestors when work item has at least one ancestor', () => {
+ expect(findDisclosureHierarchy().exists()).toBe(true);
+ expect(findDisclosureHierarchy().props('items')).toEqual(
+ expect.objectContaining(formatAncestors(workItemAncestorsQueryResponse.data.workItem)),
+ );
+ });
+
+ it('does not display DisclosureHierarchy component when work item has no ancestor', async () => {
+ wrapper = createComponent({ ancestorsQueryHandler: workItemEmptyAncestorsQueryHandler });
+ await waitForPromises();
+
+ expect(findDisclosureHierarchy().exists()).toBe(false);
+ });
+
+ it('displays work item info in popover on hover and focus', () => {
+ expect(findPopover().exists()).toBe(true);
+ expect(findPopover().props('triggers')).toBe('hover focus');
+
+ const ancestor = findDisclosureHierarchy().props('items')[0];
+
+ expect(findPopover().text()).toContain(ancestor.title);
+ expect(findPopover().text()).toContain(ancestor.reference);
+ });
+
+ describe('when work item has less than 3 ancestors', () => {
+ it('does not activate ellipsis option for DisclosureHierarchy component', () => {
+ expect(findDisclosureHierarchy().props('withEllipsis')).toBe(false);
+ });
+ });
+
+ describe('when work item has at least 3 ancestors', () => {
+ beforeEach(async () => {
+ wrapper = createComponent({ ancestorsQueryHandler: workItemThreeAncestorsQueryHandler });
+ await waitForPromises();
+ });
+
+ it('activates ellipsis option for DisclosureHierarchy component', () => {
+ expect(findDisclosureHierarchy().props('withEllipsis')).toBe(true);
+ });
+ });
+
+ it('creates alert when the query fails', async () => {
+ createComponent({ ancestorsQueryHandler: workItemAncestorsFailureHandler });
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ captureError: true,
+ error: expect.any(Object),
+ message: 'Something went wrong while fetching ancestors.',
+ });
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js b/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js
index 123cf647674..48ec84ceb85 100644
--- a/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js
+++ b/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js
@@ -1,11 +1,20 @@
+import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue';
import WorkItemDueDate from '~/work_items/components/work_item_due_date.vue';
import WorkItemLabels from '~/work_items/components/work_item_labels.vue';
import WorkItemMilestone from '~/work_items/components/work_item_milestone.vue';
-
+import WorkItemParentInline from '~/work_items/components/work_item_parent_inline.vue';
+import WorkItemParent from '~/work_items/components/work_item_parent_with_edit.vue';
+import waitForPromises from 'helpers/wait_for_promises';
import WorkItemAttributesWrapper from '~/work_items/components/work_item_attributes_wrapper.vue';
-import { workItemResponseFactory } from '../mock_data';
+import {
+ workItemResponseFactory,
+ taskType,
+ issueType,
+ objectiveType,
+ keyResultType,
+} from '../mock_data';
describe('WorkItemAttributesWrapper component', () => {
let wrapper;
@@ -16,8 +25,13 @@ describe('WorkItemAttributesWrapper component', () => {
const findWorkItemAssignees = () => wrapper.findComponent(WorkItemAssignees);
const findWorkItemLabels = () => wrapper.findComponent(WorkItemLabels);
const findWorkItemMilestone = () => wrapper.findComponent(WorkItemMilestone);
+ const findWorkItemParentInline = () => wrapper.findComponent(WorkItemParentInline);
+ const findWorkItemParent = () => wrapper.findComponent(WorkItemParent);
- const createComponent = ({ workItem = workItemQueryResponse.data.workItem } = {}) => {
+ const createComponent = ({
+ workItem = workItemQueryResponse.data.workItem,
+ workItemsMvc2 = true,
+ } = {}) => {
wrapper = shallowMount(WorkItemAttributesWrapper, {
propsData: {
fullPath: 'group/project',
@@ -29,6 +43,9 @@ describe('WorkItemAttributesWrapper component', () => {
hasOkrsFeature: true,
hasIssuableHealthStatusFeature: true,
projectNamespace: 'namespace',
+ glFeatures: {
+ workItemsMvc2,
+ },
},
stubs: {
WorkItemWeight: true,
@@ -94,4 +111,54 @@ describe('WorkItemAttributesWrapper component', () => {
expect(findWorkItemMilestone().exists()).toBe(exists);
});
});
+
+ describe('parent widget', () => {
+ describe.each`
+ description | workItemType | exists
+ ${'when work item type is task'} | ${taskType} | ${true}
+ ${'when work item type is objective'} | ${objectiveType} | ${true}
+ ${'when work item type is keyresult'} | ${keyResultType} | ${true}
+ ${'when work item type is issue'} | ${issueType} | ${false}
+ `('$description', ({ workItemType, exists }) => {
+ it(`${exists ? 'renders' : 'does not render'} parent component`, async () => {
+ const response = workItemResponseFactory({ workItemType });
+ createComponent({ workItem: response.data.workItem });
+
+ await waitForPromises();
+
+ expect(findWorkItemParent().exists()).toBe(exists);
+ });
+ });
+
+ it('renders WorkItemParent when workItemsMvc2 enabled', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findWorkItemParent().exists()).toBe(true);
+ expect(findWorkItemParentInline().exists()).toBe(false);
+ });
+
+ it('renders WorkItemParentInline when workItemsMvc2 disabled', async () => {
+ createComponent({ workItemsMvc2: false });
+
+ await waitForPromises();
+
+ expect(findWorkItemParent().exists()).toBe(false);
+ expect(findWorkItemParentInline().exists()).toBe(true);
+ });
+
+ it('emits an error event to the wrapper', async () => {
+ const response = workItemResponseFactory({ parentWidgetPresent: true });
+ createComponent({ workItem: response.data.workItem });
+ const updateError = 'Failed to update';
+
+ await waitForPromises();
+
+ findWorkItemParent().vm.$emit('error', updateError);
+ await nextTick();
+
+ expect(wrapper.emitted('error')).toEqual([[updateError]]);
+ });
+ });
});
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 6fa3a70c3eb..f77d6c89035 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
@@ -61,7 +61,6 @@ describe('WorkItemDetailModal component', () => {
expect(findWorkItemDetail().props()).toEqual({
isModal: true,
workItemIid: '1',
- workItemParentId: null,
});
});
diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js
index acfe4571cd2..d63bb94c3f0 100644
--- a/spec/frontend/work_items/components/work_item_detail_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_spec.js
@@ -1,10 +1,4 @@
-import {
- GlAlert,
- GlSkeletonLoader,
- GlButton,
- GlEmptyState,
- GlIntersectionObserver,
-} from '@gitlab/ui';
+import { GlAlert, GlSkeletonLoader, GlEmptyState } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -15,6 +9,7 @@ import setWindowLocation from 'helpers/set_window_location_helper';
import { stubComponent } from 'helpers/stub_component';
import WorkItemDetail from '~/work_items/components/work_item_detail.vue';
import WorkItemActions from '~/work_items/components/work_item_actions.vue';
+import WorkItemAncestors from '~/work_items/components/work_item_ancestors/work_item_ancestors.vue';
import WorkItemDescription from '~/work_items/components/work_item_description.vue';
import WorkItemCreatedUpdated from '~/work_items/components/work_item_created_updated.vue';
import WorkItemAttributesWrapper from '~/work_items/components/work_item_attributes_wrapper.vue';
@@ -23,13 +18,13 @@ import WorkItemTree from '~/work_items/components/work_item_links/work_item_tree
import WorkItemRelationships from '~/work_items/components/work_item_relationships/work_item_relationships.vue';
import WorkItemNotes from '~/work_items/components/work_item_notes.vue';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
+import WorkItemStickyHeader from '~/work_items/components/work_item_sticky_header.vue';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
import WorkItemTodos from '~/work_items/components/work_item_todos.vue';
import { i18n } from '~/work_items/constants';
import groupWorkItemByIidQuery from '~/work_items/graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.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 workItemUpdatedSubscription from '~/work_items/graphql/work_item_updated.subscription.graphql';
import {
@@ -74,8 +69,7 @@ describe('WorkItemDetail component', () => {
const findCreatedUpdated = () => wrapper.findComponent(WorkItemCreatedUpdated);
const findWorkItemDescription = () => wrapper.findComponent(WorkItemDescription);
const findWorkItemAttributesWrapper = () => wrapper.findComponent(WorkItemAttributesWrapper);
- const findParent = () => wrapper.findByTestId('work-item-parent');
- const findParentButton = () => findParent().findComponent(GlButton);
+ const findAncestors = () => wrapper.findComponent(WorkItemAncestors);
const findCloseButton = () => wrapper.findByTestId('work-item-close');
const findWorkItemType = () => wrapper.findByTestId('work-item-type');
const findHierarchyTree = () => wrapper.findComponent(WorkItemTree);
@@ -84,11 +78,9 @@ describe('WorkItemDetail component', () => {
const findModal = () => wrapper.findComponent(WorkItemDetailModal);
const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
const findWorkItemTodos = () => wrapper.findComponent(WorkItemTodos);
- const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
- const findStickyHeader = () => wrapper.findByTestId('work-item-sticky-header');
+ const findStickyHeader = () => wrapper.findComponent(WorkItemStickyHeader);
const findWorkItemTwoColumnViewContainer = () => wrapper.findByTestId('work-item-overview');
const findRightSidebar = () => wrapper.findByTestId('work-item-overview-right-sidebar');
- const triggerPageScroll = () => findIntersectionObserver().vm.$emit('disappear');
const createComponent = ({
isGroup = false,
@@ -96,7 +88,7 @@ describe('WorkItemDetail component', () => {
updateInProgress = false,
workItemIid = '1',
handler = successHandler,
- confidentialityMock = [updateWorkItemMutation, jest.fn()],
+ mutationHandler,
error = undefined,
workItemsMvc2Enabled = false,
linkedWorkItemsEnabled = false,
@@ -105,8 +97,8 @@ describe('WorkItemDetail component', () => {
apolloProvider: createMockApollo([
[workItemByIidQuery, handler],
[groupWorkItemByIidQuery, groupSuccessHandler],
+ [updateWorkItemMutation, mutationHandler],
[workItemUpdatedSubscription, workItemUpdatedSubscriptionHandler],
- confidentialityMock,
]),
isLoggedIn: isLoggedIn(),
propsData: {
@@ -134,6 +126,7 @@ describe('WorkItemDetail component', () => {
reportAbusePath: '/report/abuse/path',
},
stubs: {
+ WorkItemAncestors: true,
WorkItemWeight: true,
WorkItemIteration: true,
WorkItemHealthStatus: true,
@@ -236,119 +229,52 @@ describe('WorkItemDetail component', () => {
describe('confidentiality', () => {
const errorMessage = 'Mutation failed';
- const confidentialWorkItem = workItemByIidResponseFactory({
- confidential: true,
- });
- const workItem = confidentialWorkItem.data.workspace.workItems.nodes[0];
-
- // Mocks for work item without parent
- const withoutParentExpectedInputVars = { id, confidential: true };
- const toggleConfidentialityWithoutParentHandler = jest.fn().mockResolvedValue({
- data: {
- workItemUpdate: {
- workItem,
- errors: [],
- },
- },
- });
- const withoutParentHandlerMock = jest
- .fn()
- .mockResolvedValue(workItemQueryResponseWithoutParent);
- const confidentialityWithoutParentMock = [
- updateWorkItemMutation,
- toggleConfidentialityWithoutParentHandler,
- ];
- const confidentialityWithoutParentFailureMock = [
- updateWorkItemMutation,
- jest.fn().mockRejectedValue(new Error(errorMessage)),
- ];
-
- // Mocks for work item with parent
- const withParentExpectedInputVars = {
- id: mockParent.parent.id,
- taskData: { id, confidential: true },
- };
- const toggleConfidentialityWithParentHandler = jest.fn().mockResolvedValue({
+ const confidentialWorkItem = workItemByIidResponseFactory({ confidential: true });
+ const mutationHandler = jest.fn().mockResolvedValue({
data: {
workItemUpdate: {
- workItem: {
- id: workItem.id,
- descriptionHtml: workItem.description,
- },
- task: {
- workItem,
- confidential: true,
- },
+ workItem: confidentialWorkItem.data.workspace.workItems.nodes[0],
errors: [],
},
},
});
- const confidentialityWithParentMock = [
- updateWorkItemTaskMutation,
- toggleConfidentialityWithParentHandler,
- ];
- const confidentialityWithParentFailureMock = [
- updateWorkItemTaskMutation,
- jest.fn().mockRejectedValue(new Error(errorMessage)),
- ];
-
- describe.each`
- context | handlerMock | confidentialityMock | confidentialityFailureMock | inputVariables
- ${'no parent'} | ${withoutParentHandlerMock} | ${confidentialityWithoutParentMock} | ${confidentialityWithoutParentFailureMock} | ${withoutParentExpectedInputVars}
- ${'parent'} | ${successHandler} | ${confidentialityWithParentMock} | ${confidentialityWithParentFailureMock} | ${withParentExpectedInputVars}
- `(
- 'when work item has $context',
- ({ handlerMock, confidentialityMock, confidentialityFailureMock, inputVariables }) => {
- it('sends updateInProgress props to child component', async () => {
- createComponent({
- handler: handlerMock,
- confidentialityMock,
- });
-
- await waitForPromises();
-
- findWorkItemActions().vm.$emit('toggleWorkItemConfidentiality', true);
- await nextTick();
-
- expect(findCreatedUpdated().props('updateInProgress')).toBe(true);
- });
+ it('sends updateInProgress props to child component', async () => {
+ createComponent({ mutationHandler });
+ await waitForPromises();
- it('emits workItemUpdated when mutation is successful', async () => {
- createComponent({
- handler: handlerMock,
- confidentialityMock,
- });
+ findWorkItemActions().vm.$emit('toggleWorkItemConfidentiality', true);
+ await nextTick();
- await waitForPromises();
+ expect(findCreatedUpdated().props('updateInProgress')).toBe(true);
+ });
- findWorkItemActions().vm.$emit('toggleWorkItemConfidentiality', true);
- await waitForPromises();
+ it('emits workItemUpdated when mutation is successful', async () => {
+ createComponent({ mutationHandler });
+ await waitForPromises();
- expect(wrapper.emitted('workItemUpdated')).toEqual([[{ confidential: true }]]);
- expect(confidentialityMock[1]).toHaveBeenCalledWith({
- input: inputVariables,
- });
- });
+ findWorkItemActions().vm.$emit('toggleWorkItemConfidentiality', true);
+ await waitForPromises();
- it('shows an alert when mutation fails', async () => {
- createComponent({
- handler: handlerMock,
- confidentialityMock: confidentialityFailureMock,
- });
+ expect(wrapper.emitted('workItemUpdated')).toEqual([[{ confidential: true }]]);
+ expect(mutationHandler).toHaveBeenCalledWith({
+ input: {
+ id: 'gid://gitlab/WorkItem/1',
+ confidential: true,
+ },
+ });
+ });
- await waitForPromises();
- findWorkItemActions().vm.$emit('toggleWorkItemConfidentiality', true);
- await waitForPromises();
- expect(wrapper.emitted('workItemUpdated')).toBeUndefined();
+ it('shows an alert when mutation fails', async () => {
+ createComponent({ mutationHandler: jest.fn().mockRejectedValue(new Error(errorMessage)) });
+ await waitForPromises();
- await nextTick();
+ findWorkItemActions().vm.$emit('toggleWorkItemConfidentiality', true);
+ await waitForPromises();
- expect(findAlert().exists()).toBe(true);
- expect(findAlert().text()).toBe(errorMessage);
- });
- },
- );
+ expect(wrapper.emitted('workItemUpdated')).toBeUndefined();
+ expect(findAlert().text()).toBe(errorMessage);
+ });
});
describe('description', () => {
@@ -366,19 +292,19 @@ describe('WorkItemDetail component', () => {
});
});
- describe('secondary breadcrumbs', () => {
- it('does not show secondary breadcrumbs by default', () => {
+ describe('ancestors widget', () => {
+ it('does not show ancestors widget by default', () => {
createComponent();
- expect(findParent().exists()).toBe(false);
+ expect(findAncestors().exists()).toBe(false);
});
- it('does not show secondary breadcrumbs if there is not a parent', async () => {
+ it('does not show ancestors widget if there is not a parent', async () => {
createComponent({ handler: jest.fn().mockResolvedValue(workItemQueryResponseWithoutParent) });
await waitForPromises();
- expect(findParent().exists()).toBe(false);
+ expect(findAncestors().exists()).toBe(false);
});
it('shows title in the header when there is no parent', async () => {
@@ -396,45 +322,8 @@ describe('WorkItemDetail component', () => {
return waitForPromises();
});
- it('shows secondary breadcrumbs if there is a parent', () => {
- expect(findParent().exists()).toBe(true);
- });
-
- it('shows parent breadcrumb icon', () => {
- expect(findParentButton().props('icon')).toBe(mockParent.parent.workItemType.iconName);
- });
-
- it('shows parent title and iid', () => {
- expect(findParentButton().text()).toBe(
- `${mockParent.parent.title} #${mockParent.parent.iid}`,
- );
- });
-
- it('sets the parent breadcrumb URL pointing to issue page when parent type is `Issue`', () => {
- expect(findParentButton().attributes().href).toBe('../../-/issues/5');
- });
-
- it('sets the parent breadcrumb URL based on parent webUrl when parent type is not `Issue`', async () => {
- const mockParentObjective = {
- parent: {
- ...mockParent.parent,
- workItemType: {
- id: mockParent.parent.workItemType.id,
- name: 'Objective',
- iconName: 'issue-type-objective',
- },
- },
- };
- const parentResponse = workItemByIidResponseFactory(mockParentObjective);
- createComponent({ handler: jest.fn().mockResolvedValue(parentResponse) });
- await waitForPromises();
-
- expect(findParentButton().attributes().href).toBe(mockParentObjective.parent.webUrl);
- });
-
- it('shows work item type and iid', () => {
- const { iid } = workItemQueryResponse.data.workspace.workItems.nodes[0];
- expect(findParent().text()).toContain(`#${iid}`);
+ it('shows ancestors widget if there is a parent', () => {
+ expect(findAncestors().exists()).toBe(true);
});
it('does not show title in the header when parent exists', () => {
@@ -769,8 +658,7 @@ describe('WorkItemDetail component', () => {
expect(findWorkItemTwoColumnViewContainer().classes()).not.toContain('work-item-overview');
});
- it('does not have sticky header', () => {
- expect(findIntersectionObserver().exists()).toBe(false);
+ it('does not have sticky header component', () => {
expect(findStickyHeader().exists()).toBe(false);
});
@@ -789,18 +677,7 @@ describe('WorkItemDetail component', () => {
expect(findWorkItemTwoColumnViewContainer().classes()).toContain('work-item-overview');
});
- it('does not show sticky header by default', () => {
- expect(findStickyHeader().exists()).toBe(false);
- });
-
- it('has the sticky header when the page is scrolled', async () => {
- expect(findIntersectionObserver().exists()).toBe(true);
-
- global.pageYOffset = 100;
- triggerPageScroll();
-
- await nextTick();
-
+ it('renders the work item sticky header component', () => {
expect(findStickyHeader().exists()).toBe(true);
});
diff --git a/spec/frontend/work_items/components/work_item_links/okr_actions_split_button_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_actions_split_button_spec.js
index 55d5b34ae70..630ffa1a699 100644
--- a/spec/frontend/work_items/components/work_item_links/okr_actions_split_button_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_actions_split_button_spec.js
@@ -1,12 +1,40 @@
import { GlDisclosureDropdown, GlDisclosureDropdownGroup } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import OkrActionsSplitButton from '~/work_items/components/work_item_links/okr_actions_split_button.vue';
+import WorkItemActionsSplitButton from '~/work_items/components/work_item_links/work_item_actions_split_button.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+const okrActions = [
+ {
+ name: 'Objective',
+ items: [
+ {
+ text: 'New objective',
+ },
+ {
+ text: 'Existing objective',
+ },
+ ],
+ },
+ {
+ name: 'Key result',
+ items: [
+ {
+ text: 'New key result',
+ },
+ {
+ text: 'Existing key result',
+ },
+ ],
+ },
+];
+
const createComponent = () => {
return extendedWrapper(
- shallowMount(OkrActionsSplitButton, {
+ shallowMount(WorkItemActionsSplitButton, {
+ propsData: {
+ actions: okrActions,
+ },
stubs: {
GlDisclosureDropdown,
},
@@ -21,7 +49,7 @@ describe('RelatedItemsTree', () => {
wrapper = createComponent();
});
- describe('OkrActionsSplitButton', () => {
+ describe('WorkItemActionsSplitButton', () => {
describe('template', () => {
it('renders objective and key results sections', () => {
expect(wrapper.findAllComponents(GlDisclosureDropdownGroup).at(0).props('group').name).toBe(
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js
index 6c1d1035c3d..49a674e73c8 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js
@@ -1,28 +1,36 @@
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
import { GlToggle } from '@gitlab/ui';
+import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import WidgetWrapper from '~/work_items/components/widget_wrapper.vue';
import WorkItemTree from '~/work_items/components/work_item_links/work_item_tree.vue';
import WorkItemChildrenWrapper from '~/work_items/components/work_item_links/work_item_children_wrapper.vue';
import WorkItemLinksForm from '~/work_items/components/work_item_links/work_item_links_form.vue';
-import OkrActionsSplitButton from '~/work_items/components/work_item_links/okr_actions_split_button.vue';
+import WorkItemActionsSplitButton from '~/work_items/components/work_item_links/work_item_actions_split_button.vue';
+import getAllowedWorkItemChildTypes from '~/work_items//graphql/work_item_allowed_children.query.graphql';
import {
FORM_TYPES,
WORK_ITEM_TYPE_ENUM_OBJECTIVE,
WORK_ITEM_TYPE_ENUM_KEY_RESULT,
} from '~/work_items/constants';
-import { childrenWorkItems } from '../../mock_data';
+import { childrenWorkItems, allowedChildrenTypesResponse } from '../../mock_data';
+
+Vue.use(VueApollo);
describe('WorkItemTree', () => {
let wrapper;
const findEmptyState = () => wrapper.findByTestId('tree-empty');
- const findToggleFormSplitButton = () => wrapper.findComponent(OkrActionsSplitButton);
+ const findToggleFormSplitButton = () => wrapper.findComponent(WorkItemActionsSplitButton);
const findForm = () => wrapper.findComponent(WorkItemLinksForm);
const findWidgetWrapper = () => wrapper.findComponent(WidgetWrapper);
const findWorkItemLinkChildrenWrapper = () => wrapper.findComponent(WorkItemChildrenWrapper);
const findShowLabelsToggle = () => wrapper.findComponent(GlToggle);
+ const allowedChildrenTypesHandler = jest.fn().mockResolvedValue(allowedChildrenTypesResponse);
+
const createComponent = ({
workItemType = 'Objective',
parentWorkItemType = 'Objective',
@@ -31,6 +39,9 @@ describe('WorkItemTree', () => {
canUpdate = true,
} = {}) => {
wrapper = shallowMountExtended(WorkItemTree, {
+ apolloProvider: createMockApollo([
+ [getAllowedWorkItemChildTypes, allowedChildrenTypesHandler],
+ ]),
propsData: {
fullPath: 'test/project',
workItemType,
@@ -79,18 +90,25 @@ describe('WorkItemTree', () => {
expect(findWidgetWrapper().props('error')).toBe(errorMessage);
});
+ it('fetches allowed children types for current work item', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(allowedChildrenTypesHandler).toHaveBeenCalled();
+ });
+
it.each`
- option | event | formType | childType
- ${'New objective'} | ${'showCreateObjectiveForm'} | ${FORM_TYPES.create} | ${WORK_ITEM_TYPE_ENUM_OBJECTIVE}
- ${'Existing objective'} | ${'showAddObjectiveForm'} | ${FORM_TYPES.add} | ${WORK_ITEM_TYPE_ENUM_OBJECTIVE}
- ${'New key result'} | ${'showCreateKeyResultForm'} | ${FORM_TYPES.create} | ${WORK_ITEM_TYPE_ENUM_KEY_RESULT}
- ${'Existing key result'} | ${'showAddKeyResultForm'} | ${FORM_TYPES.add} | ${WORK_ITEM_TYPE_ENUM_KEY_RESULT}
+ option | formType | childType
+ ${'New objective'} | ${FORM_TYPES.create} | ${WORK_ITEM_TYPE_ENUM_OBJECTIVE}
+ ${'Existing objective'} | ${FORM_TYPES.add} | ${WORK_ITEM_TYPE_ENUM_OBJECTIVE}
+ ${'New key result'} | ${FORM_TYPES.create} | ${WORK_ITEM_TYPE_ENUM_KEY_RESULT}
+ ${'Existing key result'} | ${FORM_TYPES.add} | ${WORK_ITEM_TYPE_ENUM_KEY_RESULT}
`(
- 'when selecting $option from split button, renders the form passing $formType and $childType',
- async ({ event, formType, childType }) => {
+ 'when triggering action $option, renders the form passing $formType and $childType',
+ async ({ formType, childType }) => {
createComponent();
- findToggleFormSplitButton().vm.$emit(event);
+ wrapper.vm.showAddForm(formType, childType);
await nextTick();
expect(findForm().exists()).toBe(true);
@@ -122,7 +140,7 @@ describe('WorkItemTree', () => {
it('emits `addChild` event when form emits `addChild` event', async () => {
createComponent();
- findToggleFormSplitButton().vm.$emit('showCreateObjectiveForm');
+ wrapper.vm.showAddForm(FORM_TYPES.create, WORK_ITEM_TYPE_ENUM_OBJECTIVE);
await nextTick();
findForm().vm.$emit('addChild');
diff --git a/spec/frontend/work_items/components/work_item_notes_spec.js b/spec/frontend/work_items/components/work_item_notes_spec.js
index 9e02e0708d4..2620242000e 100644
--- a/spec/frontend/work_items/components/work_item_notes_spec.js
+++ b/spec/frontend/work_items/components/work_item_notes_spec.js
@@ -10,6 +10,7 @@ import WorkItemNotes from '~/work_items/components/work_item_notes.vue';
import WorkItemDiscussion from '~/work_items/components/notes/work_item_discussion.vue';
import WorkItemAddNote from '~/work_items/components/notes/work_item_add_note.vue';
import WorkItemNotesActivityHeader from '~/work_items/components/notes/work_item_notes_activity_header.vue';
+import groupWorkItemNotesByIidQuery from '~/work_items/graphql/notes/group_work_item_notes_by_iid.query.graphql';
import workItemNotesByIidQuery from '~/work_items/graphql/notes/work_item_notes_by_iid.query.graphql';
import deleteWorkItemNoteMutation from '~/work_items/graphql/notes/delete_work_item_notes.mutation.graphql';
import workItemNoteCreatedSubscription from '~/work_items/graphql/notes/work_item_note_created.subscription.graphql';
@@ -63,6 +64,9 @@ describe('WorkItemNotes component', () => {
const findWorkItemCommentNoteAtIndex = (index) => findAllWorkItemCommentNotes().at(index);
const findDeleteNoteModal = () => wrapper.findComponent(GlModal);
+ const groupWorkItemNotesQueryHandler = jest
+ .fn()
+ .mockResolvedValue(mockWorkItemNotesByIidResponse);
const workItemNotesQueryHandler = jest.fn().mockResolvedValue(mockWorkItemNotesByIidResponse);
const workItemMoreNotesQueryHandler = jest.fn().mockResolvedValue(mockMoreWorkItemNotesResponse);
const workItemNotesWithCommentsQueryHandler = jest
@@ -87,17 +91,22 @@ describe('WorkItemNotes component', () => {
workItemIid = mockWorkItemIid,
defaultWorkItemNotesQueryHandler = workItemNotesQueryHandler,
deleteWINoteMutationHandler = deleteWorkItemNoteMutationSuccessHandler,
+ isGroup = false,
isModal = false,
isWorkItemConfidential = false,
} = {}) => {
wrapper = shallowMount(WorkItemNotes, {
apolloProvider: createMockApollo([
[workItemNotesByIidQuery, defaultWorkItemNotesQueryHandler],
+ [groupWorkItemNotesByIidQuery, groupWorkItemNotesQueryHandler],
[deleteWorkItemNoteMutation, deleteWINoteMutationHandler],
[workItemNoteCreatedSubscription, notesCreateSubscriptionHandler],
[workItemNoteUpdatedSubscription, notesUpdateSubscriptionHandler],
[workItemNoteDeletedSubscription, notesDeleteSubscriptionHandler],
]),
+ provide: {
+ isGroup,
+ },
propsData: {
fullPath: 'test-path',
workItemId,
@@ -354,4 +363,22 @@ describe('WorkItemNotes component', () => {
expect(findWorkItemCommentNoteAtIndex(0).props('isWorkItemConfidential')).toBe(true);
});
+
+ describe('when project context', () => {
+ it('calls the project work item query', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(workItemNotesQueryHandler).toHaveBeenCalled();
+ });
+ });
+
+ describe('when group context', () => {
+ it('calls the group work item query', async () => {
+ createComponent({ isGroup: true });
+ await waitForPromises();
+
+ expect(groupWorkItemNotesQueryHandler).toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_parent_spec.js b/spec/frontend/work_items/components/work_item_parent_inline_spec.js
index 11fe6dffbfa..3e4f99d5935 100644
--- a/spec/frontend/work_items/components/work_item_parent_spec.js
+++ b/spec/frontend/work_items/components/work_item_parent_inline_spec.js
@@ -6,7 +6,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
-import WorkItemParent from '~/work_items/components/work_item_parent.vue';
+import WorkItemParentInline from '~/work_items/components/work_item_parent_inline.vue';
import { removeHierarchyChild } from '~/work_items/graphql/cache_utils';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import groupWorkItemsQuery from '~/work_items/graphql/group_work_items.query.graphql';
@@ -26,7 +26,7 @@ jest.mock('~/work_items/graphql/cache_utils', () => ({
removeHierarchyChild: jest.fn(),
}));
-describe('WorkItemParent component', () => {
+describe('WorkItemParentInline component', () => {
Vue.use(VueApollo);
let wrapper;
@@ -50,7 +50,7 @@ describe('WorkItemParent component', () => {
mutationHandler = successUpdateWorkItemMutationHandler,
isGroup = false,
} = {}) => {
- wrapper = shallowMountExtended(WorkItemParent, {
+ wrapper = shallowMountExtended(WorkItemParentInline, {
apolloProvider: createMockApollo([
[projectWorkItemsQuery, searchQueryHandler],
[groupWorkItemsQuery, groupWorkItemsSuccessHandler],
diff --git a/spec/frontend/work_items/components/work_item_parent_with_edit_spec.js b/spec/frontend/work_items/components/work_item_parent_with_edit_spec.js
new file mode 100644
index 00000000000..61e43456479
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_parent_with_edit_spec.js
@@ -0,0 +1,409 @@
+import { GlForm, GlCollapsibleListbox } from '@gitlab/ui';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { __ } from '~/locale';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
+import WorkItemParent from '~/work_items/components/work_item_parent_with_edit.vue';
+import { removeHierarchyChild } from '~/work_items/graphql/cache_utils';
+import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+import groupWorkItemsQuery from '~/work_items/graphql/group_work_items.query.graphql';
+import projectWorkItemsQuery from '~/work_items/graphql/project_work_items.query.graphql';
+import { WORK_ITEM_TYPE_ENUM_OBJECTIVE } from '~/work_items/constants';
+
+import {
+ availableObjectivesResponse,
+ mockParentWidgetResponse,
+ updateWorkItemMutationResponseFactory,
+ searchedObjectiveResponse,
+ updateWorkItemMutationErrorResponse,
+} from '../mock_data';
+
+jest.mock('~/sentry/sentry_browser_wrapper');
+jest.mock('~/work_items/graphql/cache_utils', () => ({
+ removeHierarchyChild: jest.fn(),
+}));
+
+describe('WorkItemParent component', () => {
+ Vue.use(VueApollo);
+
+ let wrapper;
+
+ const workItemId = 'gid://gitlab/WorkItem/1';
+ const workItemType = 'Objective';
+ const mockFullPath = 'full-path';
+
+ const groupWorkItemsSuccessHandler = jest.fn().mockResolvedValue(availableObjectivesResponse);
+ const availableWorkItemsSuccessHandler = jest.fn().mockResolvedValue(availableObjectivesResponse);
+ const availableWorkItemsFailureHandler = jest.fn().mockRejectedValue(new Error());
+
+ const findHeader = () => wrapper.find('h3');
+ const findEditButton = () => wrapper.find('[data-testid="edit-parent"]');
+ const findApplyButton = () => wrapper.find('[data-testid="apply-parent"]');
+
+ const findLoadingIcon = () => wrapper.find('[data-testid="loading-icon-parent"]');
+ const findLabel = () => wrapper.find('label');
+ const findForm = () => wrapper.findComponent(GlForm);
+ const findCollapsibleListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+
+ const successUpdateWorkItemMutationHandler = jest
+ .fn()
+ .mockResolvedValue(updateWorkItemMutationResponseFactory({ parent: mockParentWidgetResponse }));
+
+ const createComponent = ({
+ canUpdate = true,
+ parent = null,
+ searchQueryHandler = availableWorkItemsSuccessHandler,
+ mutationHandler = successUpdateWorkItemMutationHandler,
+ isEditing = false,
+ isGroup = false,
+ } = {}) => {
+ wrapper = mountExtended(WorkItemParent, {
+ apolloProvider: createMockApollo([
+ [projectWorkItemsQuery, searchQueryHandler],
+ [groupWorkItemsQuery, groupWorkItemsSuccessHandler],
+ [updateWorkItemMutation, mutationHandler],
+ ]),
+ provide: {
+ fullPath: mockFullPath,
+ isGroup,
+ },
+ propsData: {
+ canUpdate,
+ parent,
+ workItemId,
+ workItemType,
+ },
+ });
+
+ if (isEditing) {
+ findEditButton().trigger('click');
+ }
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('label', () => {
+ it('shows header when not editing', () => {
+ createComponent();
+
+ expect(findHeader().exists()).toBe(true);
+ expect(findHeader().classes('gl-sr-only')).toBe(false);
+ expect(findLabel().exists()).toBe(false);
+ });
+
+ it('shows label and hides header while editing', async () => {
+ createComponent({ isEditing: true });
+
+ await nextTick();
+
+ expect(findLabel().exists()).toBe(true);
+ expect(findHeader().classes('gl-sr-only')).toBe(true);
+ });
+ });
+
+ describe('edit button', () => {
+ it('is not shown if user cannot edit', () => {
+ createComponent({ canUpdate: false });
+
+ expect(findEditButton().exists()).toBe(false);
+ });
+
+ it('is shown if user can edit', () => {
+ createComponent({ canUpdate: true });
+
+ expect(findEditButton().exists()).toBe(true);
+ });
+
+ it('triggers edit mode on click', async () => {
+ createComponent();
+
+ findEditButton().trigger('click');
+
+ await nextTick();
+
+ expect(findLabel().exists()).toBe(true);
+ expect(findForm().exists()).toBe(true);
+ });
+
+ it('is replaced by Apply button while editing', async () => {
+ createComponent();
+
+ findEditButton().trigger('click');
+
+ await nextTick();
+
+ expect(findEditButton().exists()).toBe(false);
+ expect(findApplyButton().exists()).toBe(true);
+ });
+ });
+
+ describe('loading icon', () => {
+ const selectWorkItem = async (workItem) => {
+ await findCollapsibleListbox().vm.$emit('select', workItem);
+ };
+
+ it('shows loading icon while update is in progress', async () => {
+ createComponent();
+ findEditButton().trigger('click');
+
+ await nextTick();
+
+ selectWorkItem('gid://gitlab/WorkItem/716');
+
+ await nextTick();
+ expect(findLoadingIcon().exists()).toBe(true);
+ await waitForPromises();
+
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+
+ it('shows loading icon when unassign is clicked', async () => {
+ createComponent({ parent: mockParentWidgetResponse });
+ findEditButton().trigger('click');
+
+ await nextTick();
+
+ findCollapsibleListbox().vm.$emit('reset');
+
+ await nextTick();
+ expect(findLoadingIcon().exists()).toBe(true);
+ await waitForPromises();
+
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+ });
+
+ describe('value', () => {
+ it('shows None when no parent is set', () => {
+ createComponent();
+
+ expect(wrapper.text()).toContain(__('None'));
+ });
+
+ it('shows parent when parent is set', () => {
+ createComponent({ parent: mockParentWidgetResponse });
+
+ expect(wrapper.text()).not.toContain(__('None'));
+ expect(wrapper.text()).toContain(mockParentWidgetResponse.title);
+ });
+ });
+
+ describe('form', () => {
+ it('is not shown while not editing', async () => {
+ await createComponent();
+
+ expect(findForm().exists()).toBe(false);
+ });
+
+ it('is shown while editing', async () => {
+ await createComponent({ isEditing: true });
+
+ expect(findForm().exists()).toBe(true);
+ });
+ });
+
+ describe('Parent Input', () => {
+ it('is not shown while not editing', async () => {
+ await createComponent();
+
+ expect(findCollapsibleListbox().exists()).toBe(false);
+ });
+
+ it('renders the collapsible listbox with required props', async () => {
+ await createComponent({ isEditing: true });
+
+ expect(findCollapsibleListbox().exists()).toBe(true);
+ expect(findCollapsibleListbox().props()).toMatchObject({
+ items: [],
+ headerText: 'Assign parent',
+ category: 'primary',
+ loading: false,
+ isCheckCentered: true,
+ searchable: true,
+ searching: false,
+ infiniteScroll: false,
+ noResultsText: 'No matching results',
+ toggleText: 'None',
+ searchPlaceholder: 'Search',
+ resetButtonLabel: 'Unassign',
+ });
+ });
+ it('shows loading while searching', async () => {
+ await createComponent({ isEditing: true });
+
+ await findCollapsibleListbox().vm.$emit('shown');
+ expect(findCollapsibleListbox().props('searching')).toBe(true);
+ });
+ });
+
+ describe('work items query', () => {
+ it('loads work items in the listbox', async () => {
+ await createComponent({ isEditing: true });
+ await findCollapsibleListbox().vm.$emit('shown');
+
+ await waitForPromises();
+
+ expect(findCollapsibleListbox().props('searching')).toBe(false);
+ expect(findCollapsibleListbox().props('items')).toStrictEqual([
+ { text: 'Objective 101', value: 'gid://gitlab/WorkItem/716' },
+ { text: 'Objective 103', value: 'gid://gitlab/WorkItem/712' },
+ { text: 'Objective 102', value: 'gid://gitlab/WorkItem/711' },
+ ]);
+ expect(availableWorkItemsSuccessHandler).toHaveBeenCalled();
+ });
+
+ it('emits error when the query fails', async () => {
+ await createComponent({
+ searchQueryHandler: availableWorkItemsFailureHandler,
+ isEditing: true,
+ });
+
+ await findCollapsibleListbox().vm.$emit('shown');
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([
+ ['Something went wrong while fetching items. Please try again.'],
+ ]);
+ });
+
+ it('searches item when input data is entered', async () => {
+ const searchedItemQueryHandler = jest.fn().mockResolvedValue(searchedObjectiveResponse);
+ await createComponent({
+ searchQueryHandler: searchedItemQueryHandler,
+ isEditing: true,
+ });
+
+ await findCollapsibleListbox().vm.$emit('shown');
+
+ await waitForPromises();
+
+ expect(searchedItemQueryHandler).toHaveBeenCalledWith({
+ fullPath: 'full-path',
+ searchTerm: '',
+ types: [WORK_ITEM_TYPE_ENUM_OBJECTIVE],
+ in: undefined,
+ iid: null,
+ isNumber: false,
+ });
+
+ await findCollapsibleListbox().vm.$emit('search', 'Objective 101');
+
+ expect(searchedItemQueryHandler).toHaveBeenCalledWith({
+ fullPath: 'full-path',
+ searchTerm: 'Objective 101',
+ types: [WORK_ITEM_TYPE_ENUM_OBJECTIVE],
+ in: 'TITLE',
+ iid: null,
+ isNumber: false,
+ });
+
+ await nextTick();
+
+ expect(findCollapsibleListbox().props('items')).toStrictEqual([
+ { text: 'Objective 101', value: 'gid://gitlab/WorkItem/716' },
+ ]);
+ });
+ });
+
+ describe('listbox', () => {
+ const selectWorkItem = async (workItem) => {
+ await findCollapsibleListbox().vm.$emit('select', workItem);
+ };
+
+ it('calls mutation when item is selected', async () => {
+ await createComponent({ isEditing: true });
+ selectWorkItem('gid://gitlab/WorkItem/716');
+
+ await waitForPromises();
+
+ expect(successUpdateWorkItemMutationHandler).toHaveBeenCalledWith({
+ input: {
+ id: 'gid://gitlab/WorkItem/1',
+ hierarchyWidget: {
+ parentId: 'gid://gitlab/WorkItem/716',
+ },
+ },
+ });
+
+ expect(removeHierarchyChild).toHaveBeenCalledWith({
+ cache: expect.anything(Object),
+ fullPath: mockFullPath,
+ iid: undefined,
+ isGroup: false,
+ workItem: { id: 'gid://gitlab/WorkItem/1' },
+ });
+ });
+
+ it('calls mutation when item is unassigned', async () => {
+ const unAssignParentWorkItemMutationHandler = jest
+ .fn()
+ .mockResolvedValue(updateWorkItemMutationResponseFactory({ parent: null }));
+ await createComponent({
+ parent: {
+ iid: '1',
+ },
+ mutationHandler: unAssignParentWorkItemMutationHandler,
+ });
+
+ findEditButton().trigger('click');
+
+ await nextTick();
+
+ findCollapsibleListbox().vm.$emit('reset');
+
+ await waitForPromises();
+
+ expect(unAssignParentWorkItemMutationHandler).toHaveBeenCalledWith({
+ input: {
+ id: 'gid://gitlab/WorkItem/1',
+ hierarchyWidget: {
+ parentId: null,
+ },
+ },
+ });
+ expect(removeHierarchyChild).toHaveBeenCalledWith({
+ cache: expect.anything(Object),
+ fullPath: mockFullPath,
+ iid: '1',
+ isGroup: false,
+ workItem: { id: 'gid://gitlab/WorkItem/1' },
+ });
+ });
+
+ it('emits error when mutation fails', async () => {
+ await createComponent({
+ mutationHandler: jest.fn().mockResolvedValue(updateWorkItemMutationErrorResponse),
+ isEditing: true,
+ });
+
+ selectWorkItem('gid://gitlab/WorkItem/716');
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([['Error!']]);
+ });
+
+ it('emits error and captures exception in sentry when network request fails', async () => {
+ const error = new Error('error');
+ await createComponent({
+ mutationHandler: jest.fn().mockRejectedValue(error),
+ isEditing: true,
+ });
+
+ selectWorkItem('gid://gitlab/WorkItem/716');
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([
+ ['Something went wrong while updating the objective. Please try again.'],
+ ]);
+ expect(Sentry.captureException).toHaveBeenCalledWith(error);
+ });
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_state_toggle_button_spec.js b/spec/frontend/work_items/components/work_item_state_toggle_spec.js
index a210bd50422..a210bd50422 100644
--- a/spec/frontend/work_items/components/work_item_state_toggle_button_spec.js
+++ b/spec/frontend/work_items/components/work_item_state_toggle_spec.js
diff --git a/spec/frontend/work_items/components/work_item_sticky_header_spec.js b/spec/frontend/work_items/components/work_item_sticky_header_spec.js
new file mode 100644
index 00000000000..4b7818044b1
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_sticky_header_spec.js
@@ -0,0 +1,59 @@
+import { GlIntersectionObserver } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { STATE_OPEN } from '~/work_items/constants';
+import { workItemResponseFactory } from 'jest/work_items/mock_data';
+import WorkItemStickyHeader from '~/work_items/components/work_item_sticky_header.vue';
+import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
+import WorkItemActions from '~/work_items/components/work_item_actions.vue';
+import WorkItemTodos from '~/work_items/components/work_item_todos.vue';
+
+describe('WorkItemStickyHeader', () => {
+ let wrapper;
+
+ const workItemResponse = workItemResponseFactory({ canUpdate: true, confidential: true }).data
+ .workItem;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(WorkItemStickyHeader, {
+ propsData: {
+ workItem: workItemResponse,
+ fullPath: '/test',
+ isStickyHeaderShowing: true,
+ workItemNotificationsSubscribed: true,
+ updateInProgress: false,
+ parentWorkItemConfidentiality: false,
+ showWorkItemCurrentUserTodos: true,
+ isModal: false,
+ currentUserTodos: [],
+ workItemState: STATE_OPEN,
+ },
+ });
+ };
+ const findStickyHeader = () => wrapper.findByTestId('work-item-sticky-header');
+ const findConfidentialityBadge = () => wrapper.findComponent(ConfidentialityBadge);
+ const findWorkItemActions = () => wrapper.findComponent(WorkItemActions);
+ const findWorkItemTodos = () => wrapper.findComponent(WorkItemTodos);
+ const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
+ const triggerPageScroll = () => findIntersectionObserver().vm.$emit('disappear');
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('has the sticky header when the page is scrolled', async () => {
+ global.pageYOffset = 100;
+ triggerPageScroll();
+
+ await nextTick();
+
+ expect(findStickyHeader().exists()).toBe(true);
+ });
+
+ it('has the components of confidentiality, actions, todos and title', () => {
+ expect(findConfidentialityBadge().exists()).toBe(true);
+ expect(findWorkItemActions().exists()).toBe(true);
+ expect(findWorkItemTodos().exists()).toBe(true);
+ expect(wrapper.findByText(workItemResponse.title).exists()).toBe(true);
+ });
+});
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 0f466bcf691..de740e5fbc5 100644
--- a/spec/frontend/work_items/components/work_item_title_spec.js
+++ b/spec/frontend/work_items/components/work_item_title_spec.js
@@ -8,7 +8,6 @@ import ItemTitle from '~/work_items/components/item_title.vue';
import WorkItemTitle from '~/work_items/components/work_item_title.vue';
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';
describe('WorkItemTitle component', () => {
@@ -20,22 +19,14 @@ describe('WorkItemTitle component', () => {
const findItemTitle = () => wrapper.findComponent(ItemTitle);
- const createComponent = ({
- workItemParentId,
- mutationHandler = mutationSuccessHandler,
- canUpdate = true,
- } = {}) => {
+ const createComponent = ({ mutationHandler = mutationSuccessHandler, canUpdate = true } = {}) => {
const { id, title, workItemType } = workItemQueryResponse.data.workItem;
wrapper = shallowMount(WorkItemTitle, {
- apolloProvider: createMockApollo([
- [updateWorkItemMutation, mutationHandler],
- [updateWorkItemTaskMutation, mutationHandler],
- ]),
+ apolloProvider: createMockApollo([[updateWorkItemMutation, mutationHandler]]),
propsData: {
workItemId: id,
workItemTitle: title,
workItemType: workItemType.name,
- workItemParentId,
canUpdate,
},
});
@@ -77,27 +68,6 @@ describe('WorkItemTitle component', () => {
});
});
- it('calls WorkItemTaskUpdate if passed workItemParentId prop', () => {
- const title = 'new title!';
- const workItemParentId = '1234';
-
- createComponent({
- workItemParentId,
- });
-
- 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', () => {
createComponent();
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index 8df46403b90..9d4606eb95a 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -445,7 +445,7 @@ export const descriptionHtmlWithCheckboxes = `
</ul>
`;
-const taskType = {
+export const taskType = {
__typename: 'WorkItemType',
id: 'gid://gitlab/WorkItems::Type/5',
name: 'Task',
@@ -459,6 +459,20 @@ export const objectiveType = {
iconName: 'issue-type-objective',
};
+export const keyResultType = {
+ __typename: 'WorkItemType',
+ id: 'gid://gitlab/WorkItems::Type/2411',
+ name: 'Key Result',
+ iconName: 'issue-type-keyresult',
+};
+
+export const issueType = {
+ __typename: 'WorkItemType',
+ id: 'gid://gitlab/WorkItems::Type/2411',
+ name: 'Issue',
+ iconName: 'issue-type-issue',
+};
+
export const mockEmptyLinkedItems = {
type: WIDGET_TYPE_LINKED_ITEMS,
blocked: false,
@@ -3703,5 +3717,40 @@ export const updateWorkItemNotificationsMutationResponse = (subscribed) => ({
},
});
+export const allowedChildrenTypesResponse = {
+ data: {
+ workItem: {
+ id: 'gid://gitlab/WorkItem/634',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/6',
+ name: 'Objective',
+ widgetDefinitions: [
+ {
+ type: 'HIERARCHY',
+ allowedChildTypes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WorkItems::Type/7',
+ name: 'Key Result',
+ __typename: 'WorkItemType',
+ },
+ {
+ id: 'gid://gitlab/WorkItems::Type/6',
+ name: 'Objective',
+ __typename: 'WorkItemType',
+ },
+ ],
+ __typename: 'WorkItemTypeConnection',
+ },
+ __typename: 'WorkItemWidgetDefinitionHierarchy',
+ },
+ ],
+ __typename: 'WorkItemType',
+ },
+ __typename: 'WorkItem',
+ },
+ },
+};
+
export const generateWorkItemsListWithId = (count) =>
Array.from({ length: count }, (_, i) => ({ id: `gid://gitlab/WorkItem/${i + 1}` }));
diff --git a/spec/frontend/work_items/notes/award_utils_spec.js b/spec/frontend/work_items/notes/award_utils_spec.js
index 8ae32ce5f40..43eceb13b67 100644
--- a/spec/frontend/work_items/notes/award_utils_spec.js
+++ b/spec/frontend/work_items/notes/award_utils_spec.js
@@ -2,6 +2,7 @@ import { getMutation, optimisticAwardUpdate } from '~/work_items/notes/award_uti
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import mockApollo from 'helpers/mock_apollo_helper';
import { __ } from '~/locale';
+import groupWorkItemNotesByIidQuery from '~/work_items/graphql/notes/group_work_item_notes_by_iid.query.graphql';
import workItemNotesByIidQuery from '~/work_items/graphql/notes/work_item_notes_by_iid.query.graphql';
import addAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_add_award_emoji.mutation.graphql';
import removeAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_remove_award_emoji.mutation.graphql';
@@ -105,5 +106,22 @@ describe('Work item note award utils', () => {
expect(updatedNote.awardEmoji.nodes).toEqual([]);
});
+
+ it.each`
+ description | isGroup | query
+ ${'calls project query when in project context'} | ${false} | ${workItemNotesByIidQuery}
+ ${'calls group query when in group context'} | ${true} | ${groupWorkItemNotesByIidQuery}
+ `('$description', ({ isGroup, query }) => {
+ const note = firstNote;
+ const { name } = mockAwardEmojiThumbsUp;
+ const cacheSpy = { updateQuery: jest.fn() };
+
+ optimisticAwardUpdate({ note, name, fullPath, isGroup, workItemIid })(cacheSpy);
+
+ expect(cacheSpy.updateQuery).toHaveBeenCalledWith(
+ { query, variables: { fullPath, iid: workItemIid } },
+ expect.any(Function),
+ );
+ });
});
});
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 527f5890338..2c898f97ee9 100644
--- a/spec/frontend/work_items/pages/create_work_item_spec.js
+++ b/spec/frontend/work_items/pages/create_work_item_spec.js
@@ -8,7 +8,6 @@ import CreateWorkItem from '~/work_items/pages/create_work_item.vue';
import ItemTitle from '~/work_items/components/item_title.vue';
import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql';
-import createWorkItemFromTaskMutation from '~/work_items/graphql/create_work_item_from_task.mutation.graphql';
import { projectWorkItemTypesQueryResponse, createWorkItemMutationResponse } from '../mock_data';
jest.mock('~/lib/utils/uuids', () => ({ uuids: () => ['testuuid'] }));
@@ -42,7 +41,6 @@ describe('Create work item component', () => {
[
[projectWorkItemTypesQuery, queryHandler],
[createWorkItemMutation, mutationHandler],
- [createWorkItemFromTaskMutation, mutationHandler],
],
{},
{ typePolicies: { Project: { merge: true } } },
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 84b10f30418..4854b5bfb77 100644
--- a/spec/frontend/work_items/pages/work_item_root_spec.js
+++ b/spec/frontend/work_items/pages/work_item_root_spec.js
@@ -49,7 +49,6 @@ describe('Work items root component', () => {
expect(findWorkItemDetail().props()).toEqual({
isModal: false,
- workItemParentId: null,
workItemIid: '1',
});
});