diff options
Diffstat (limited to 'spec/frontend/work_items/components/work_item_ancestors')
4 files changed, 466 insertions, 0 deletions
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.', + }); + }); +}); |