diff options
Diffstat (limited to 'spec/frontend/boards')
12 files changed, 358 insertions, 28 deletions
diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js index 985902b4a3b..2c3ec69f9ae 100644 --- a/spec/frontend/boards/board_card_inner_spec.js +++ b/spec/frontend/boards/board_card_inner_spec.js @@ -7,6 +7,8 @@ import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import BoardBlockedIcon from '~/boards/components/board_blocked_icon.vue'; import BoardCardInner from '~/boards/components/board_card_inner.vue'; +import BoardCardMoveToPosition from '~/boards/components/board_card_move_to_position.vue'; +import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue'; import { issuableTypes } from '~/boards/constants'; import eventHub from '~/boards/eventhub'; import defaultStore from '~/boards/stores'; @@ -47,6 +49,8 @@ describe('Board card component', () => { const findEpicCountablesTotalWeight = () => wrapper.findByTestId('epic-countables-total-weight'); const findEpicProgressTooltip = () => wrapper.findByTestId('epic-progress-tooltip-content'); const findHiddenIssueIcon = () => wrapper.findByTestId('hidden-icon'); + const findMoveToPositionComponent = () => wrapper.findComponent(BoardCardMoveToPosition); + const findWorkItemIcon = () => wrapper.findComponent(WorkItemTypeIcon); const performSearchMock = jest.fn(); @@ -75,10 +79,12 @@ describe('Board card component', () => { propsData: { list, item: issue, + index: 0, ...props, }, stubs: { GlLoadingIcon: true, + BoardCardMoveToPosition: true, }, directives: { GlTooltip: createMockDirective(), @@ -137,6 +143,20 @@ describe('Board card component', () => { expect(findHiddenIssueIcon().exists()).toBe(false); }); + it('renders the move to position icon', () => { + expect(findMoveToPositionComponent().exists()).toBe(true); + }); + + it('does not render the work type icon by default', () => { + expect(findWorkItemIcon().exists()).toBe(false); + }); + + it('renders the work type icon when props is passed', () => { + createWrapper({ item: issue, list, showWorkItemTypeIcon: true }); + expect(findWorkItemIcon().exists()).toBe(true); + expect(findWorkItemIcon().props('workItemType')).toBe(issue.type); + }); + it('renders issue ID with #', () => { expect(wrapper.find('.board-card-number').text()).toContain(`#${issue.iid}`); }); diff --git a/spec/frontend/boards/board_list_helper.js b/spec/frontend/boards/board_list_helper.js index 04192489817..65a41c49e7f 100644 --- a/spec/frontend/boards/board_list_helper.js +++ b/spec/frontend/boards/board_list_helper.js @@ -75,6 +75,7 @@ export default function createComponent({ id: 1, iid: 1, confidential: false, + referencePath: 'gitlab-org/test-subgroup/gitlab-test#1', labels: [], assignees: [], ...listIssueProps, diff --git a/spec/frontend/boards/components/__snapshots__/board_blocked_icon_spec.js.snap b/spec/frontend/boards/components/__snapshots__/board_blocked_icon_spec.js.snap index 3fb0706fd10..34e4f996ff0 100644 --- a/spec/frontend/boards/components/__snapshots__/board_blocked_icon_spec.js.snap +++ b/spec/frontend/boards/components/__snapshots__/board_blocked_icon_spec.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`BoardBlockedIcon on mouseenter on blocked icon with more than three blocking issues matches the snapshot 1`] = ` -"<div class=\\"gl-display-inline\\"><svg data-testid=\\"issue-blocked-icon\\" role=\\"img\\" aria-hidden=\\"true\\" class=\\"issue-blocked-icon gl-mr-2 gl-cursor-pointer gl-icon s16\\" id=\\"blocked-icon-uniqueId\\"> +"<div class=\\"gl-display-inline\\"><svg data-testid=\\"issue-blocked-icon\\" role=\\"img\\" aria-hidden=\\"true\\" class=\\"issue-blocked-icon gl-mr-2 gl-cursor-pointer gl-text-red-500 gl-icon s16\\" id=\\"blocked-icon-uniqueId\\"> <use href=\\"#issue-block\\"></use> </svg> <div class=\\"gl-popover\\"> diff --git a/spec/frontend/boards/components/board_blocked_icon_spec.js b/spec/frontend/boards/components/board_blocked_icon_spec.js index cf4ba07da16..ffdc0a7cecc 100644 --- a/spec/frontend/boards/components/board_blocked_icon_spec.js +++ b/spec/frontend/boards/components/board_blocked_icon_spec.js @@ -10,13 +10,17 @@ import { blockingIssuablesQueries, issuableTypes } from '~/boards/constants'; import { truncate } from '~/lib/utils/text_utility'; import { mockIssue, + mockEpic, mockBlockingIssue1, mockBlockingIssue2, + mockBlockingEpic1, mockBlockingIssuablesResponse1, mockBlockingIssuablesResponse2, mockBlockingIssuablesResponse3, mockBlockedIssue1, mockBlockedIssue2, + mockBlockedEpic1, + mockBlockingEpicIssuablesResponse1, } from '../mock_data'; describe('BoardBlockedIcon', () => { @@ -51,9 +55,11 @@ describe('BoardBlockedIcon', () => { const createWrapperWithApollo = ({ item = mockBlockedIssue1, blockingIssuablesSpy = jest.fn().mockResolvedValue(mockBlockingIssuablesResponse1), + issuableItem = mockIssue, + issuableType = issuableTypes.issue, } = {}) => { mockApollo = createMockApollo([ - [blockingIssuablesQueries[issuableTypes.issue].query, blockingIssuablesSpy], + [blockingIssuablesQueries[issuableType].query, blockingIssuablesSpy], ]); Vue.use(VueApollo); @@ -62,27 +68,34 @@ describe('BoardBlockedIcon', () => { apolloProvider: mockApollo, propsData: { item: { - ...mockIssue, + ...issuableItem, ...item, }, uniqueId: 'uniqueId', - issuableType: issuableTypes.issue, + issuableType, }, attachTo: document.body, }), ); }; - const createWrapper = ({ item = {}, queries = {}, data = {}, loading = false } = {}) => { + const createWrapper = ({ + item = {}, + queries = {}, + data = {}, + loading = false, + mockIssuable = mockIssue, + issuableType = issuableTypes.issue, + } = {}) => { wrapper = extendedWrapper( shallowMount(BoardBlockedIcon, { propsData: { item: { - ...mockIssue, + ...mockIssuable, ...item, }, uniqueId: 'uniqueid', - issuableType: issuableTypes.issue, + issuableType, }, data() { return { @@ -105,11 +118,24 @@ describe('BoardBlockedIcon', () => { ); }; - it('should render blocked icon', () => { - createWrapper(); + it.each` + mockIssuable | issuableType | expectedIcon + ${mockIssue} | ${issuableTypes.issue} | ${'issue-block'} + ${mockEpic} | ${issuableTypes.epic} | ${'entity-blocked'} + `( + 'should render blocked icon for $issuableType', + ({ mockIssuable, issuableType, expectedIcon }) => { + createWrapper({ + mockIssuable, + issuableType, + }); - expect(findGlIcon().exists()).toBe(true); - }); + expect(findGlIcon().exists()).toBe(true); + const icon = findGlIcon(); + expect(icon.exists()).toBe(true); + expect(icon.props('name')).toBe(expectedIcon); + }, + ); it('should display a loading spinner while loading', () => { createWrapper({ loading: true }); @@ -124,17 +150,29 @@ describe('BoardBlockedIcon', () => { }); describe('on mouseenter on blocked icon', () => { - it('should query for blocking issuables and render the result', async () => { - createWrapperWithApollo(); + it.each` + item | issuableType | mockBlockingIssuable | issuableItem | blockingIssuablesSpy + ${mockBlockedIssue1} | ${issuableTypes.issue} | ${mockBlockingIssue1} | ${mockIssue} | ${jest.fn().mockResolvedValue(mockBlockingIssuablesResponse1)} + ${mockBlockedEpic1} | ${issuableTypes.epic} | ${mockBlockingEpic1} | ${mockEpic} | ${jest.fn().mockResolvedValue(mockBlockingEpicIssuablesResponse1)} + `( + 'should query for blocking issuables and render the result for $issuableType', + async ({ item, issuableType, issuableItem, mockBlockingIssuable, blockingIssuablesSpy }) => { + createWrapperWithApollo({ + item, + issuableType, + issuableItem, + blockingIssuablesSpy, + }); - expect(findGlPopover().text()).not.toContain(mockBlockingIssue1.title); + expect(findGlPopover().text()).not.toContain(mockBlockingIssuable.title); - await mouseenter(); + await mouseenter(); - expect(findGlPopover().exists()).toBe(true); - expect(findIssuableTitle().text()).toContain(mockBlockingIssue1.title); - expect(wrapper.vm.skip).toBe(true); - }); + expect(findGlPopover().exists()).toBe(true); + expect(findIssuableTitle().text()).toContain(mockBlockingIssuable.title); + expect(wrapper.vm.skip).toBe(true); + }, + ); it('should emit "blocking-issuables-error" event on query error', async () => { const mockError = new Error('mayday'); diff --git a/spec/frontend/boards/components/board_card_move_to_position_spec.js b/spec/frontend/boards/components/board_card_move_to_position_spec.js new file mode 100644 index 00000000000..7254b9486ef --- /dev/null +++ b/spec/frontend/boards/components/board_card_move_to_position_spec.js @@ -0,0 +1,133 @@ +import { shallowMount } from '@vue/test-utils'; +import Vue, { nextTick } from 'vue'; +import Vuex from 'vuex'; +import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; + +import BoardCardMoveToPosition from '~/boards/components/board_card_move_to_position.vue'; +import { mockList, mockIssue2, mockIssue, mockIssue3, mockIssue4 } from 'jest/boards/mock_data'; +import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; + +Vue.use(Vuex); + +const dropdownOptions = [ + BoardCardMoveToPosition.i18n.moveToStartText, + BoardCardMoveToPosition.i18n.moveToEndText, +]; + +describe('Board Card Move to position', () => { + let wrapper; + let trackingSpy; + let store; + let dispatch; + const itemIndex = 1; + + const createStoreOptions = () => { + const state = { + pageInfoByListId: { + 'gid://gitlab/List/1': {}, + 'gid://gitlab/List/2': { hasNextPage: true }, + }, + }; + const getters = { + getBoardItemsByList: () => () => [mockIssue, mockIssue2, mockIssue3, mockIssue4], + }; + const actions = { + moveItem: jest.fn(), + }; + + return { + state, + getters, + actions, + }; + }; + + const createComponent = (propsData) => { + wrapper = shallowMount(BoardCardMoveToPosition, { + store, + propsData: { + item: mockIssue2, + list: mockList, + index: 0, + ...propsData, + }, + stubs: { + GlDropdown, + GlDropdownItem, + }, + }); + }; + + beforeEach(() => { + store = new Vuex.Store(createStoreOptions()); + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + const findMoveToPositionDropdown = () => wrapper.findComponent(GlDropdown); + const findDropdownItems = () => findMoveToPositionDropdown().findAllComponents(GlDropdownItem); + const findDropdownItemAtIndex = (index) => findDropdownItems().at(index); + + describe('Dropdown', () => { + describe('Dropdown button', () => { + it('has an icon with vertical ellipsis', () => { + expect(findMoveToPositionDropdown().exists()).toBe(true); + expect(findMoveToPositionDropdown().props('icon')).toBe('ellipsis_v'); + }); + + it('is opened on the click of vertical ellipsis and has 2 dropdown items when number of list items < 10', () => { + findMoveToPositionDropdown().vm.$emit('click'); + expect(findDropdownItems()).toHaveLength(dropdownOptions.length); + }); + }); + + describe('Dropdown options', () => { + beforeEach(() => { + createComponent({ index: itemIndex }); + trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + dispatch = jest.spyOn(store, 'dispatch').mockImplementation(() => {}); + }); + + afterEach(() => { + unmockTracking(); + }); + + it.each` + dropdownIndex | dropdownLabel | trackLabel | positionInList + ${0} | ${BoardCardMoveToPosition.i18n.moveToStartText} | ${'move_to_start'} | ${0} + ${1} | ${BoardCardMoveToPosition.i18n.moveToEndText} | ${'move_to_end'} | ${-1} + `( + 'on click of dropdown index $dropdownIndex with label $dropdownLabel should call moveItem action with tracking label $trackLabel', + async ({ dropdownIndex, dropdownLabel, trackLabel, positionInList }) => { + await findMoveToPositionDropdown().vm.$emit('click'); + + expect(findDropdownItemAtIndex(dropdownIndex).text()).toBe(dropdownLabel); + await findDropdownItemAtIndex(dropdownIndex).vm.$emit('click', { + stopPropagation: () => {}, + }); + + await nextTick(); + + expect(trackingSpy).toHaveBeenCalledWith('boards:list', 'click_toggle_button', { + category: 'boards:list', + label: trackLabel, + property: 'type_card', + }); + expect(dispatch).toHaveBeenCalledWith('moveItem', { + fromListId: mockList.id, + itemId: mockIssue2.id, + itemIid: mockIssue2.iid, + itemPath: mockIssue2.referencePath, + positionInList, + toListId: mockList.id, + allItemsLoadedInList: true, + atIndex: itemIndex, + }); + }, + ); + }); + }); +}); diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js index bb1e63a581e..2feaa5dff8c 100644 --- a/spec/frontend/boards/components/board_card_spec.js +++ b/spec/frontend/boards/components/board_card_spec.js @@ -1,5 +1,5 @@ import { GlLabel } from '@gitlab/ui'; -import { shallowMount, mount } from '@vue/test-utils'; +import { shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; @@ -45,7 +45,10 @@ describe('Board card', () => { item = mockIssue, } = {}) => { wrapper = mountFn(BoardCard, { - stubs, + stubs: { + ...stubs, + BoardCardInner, + }, store, propsData: { list: mockLabelList, @@ -86,7 +89,7 @@ describe('Board card', () => { describe('when GlLabel is clicked in BoardCardInner', () => { it('doesnt call toggleBoardItem', () => { createStore({ initialState: { isShowingLabels: true } }); - mountComponent({ mountFn: mount, stubs: {} }); + mountComponent(); wrapper.findComponent(GlLabel).trigger('mouseup'); diff --git a/spec/frontend/boards/components/board_new_issue_spec.js b/spec/frontend/boards/components/board_new_issue_spec.js index 8b0100d069a..f097f42476a 100644 --- a/spec/frontend/boards/components/board_new_issue_spec.js +++ b/spec/frontend/boards/components/board_new_issue_spec.js @@ -90,7 +90,7 @@ describe('Issue boards new issue form', () => { }); }); - it('it uses the first issue ID as moveAfterId', async () => { + it('uses the first issue ID as moveAfterId', async () => { findBoardNewItem().vm.$emit('form-submit', { title: 'Foo' }); await nextTick(); diff --git a/spec/frontend/boards/components/issue_due_date_spec.js b/spec/frontend/boards/components/issue_due_date_spec.js index 73340c1b96b..45fa10bf03a 100644 --- a/spec/frontend/boards/components/issue_due_date_spec.js +++ b/spec/frontend/boards/components/issue_due_date_spec.js @@ -1,6 +1,6 @@ import { shallowMount } from '@vue/test-utils'; -import dateFormat from 'dateformat'; import IssueDueDate from '~/boards/components/issue_due_date.vue'; +import dateFormat from '~/lib/dateformat'; const createComponent = (dueDate = new Date(), closed = false) => shallowMount(IssueDueDate, { diff --git a/spec/frontend/boards/components/item_count_spec.js b/spec/frontend/boards/components/item_count_spec.js index 06cd3910fc0..0c0c7f66933 100644 --- a/spec/frontend/boards/components/item_count_spec.js +++ b/spec/frontend/boards/components/item_count_spec.js @@ -50,7 +50,7 @@ describe('IssueCount', () => { }); it('contains maxIssueCount in the template', () => { - expect(vm.find('.max-issue-size').text()).toEqual(String(maxIssueCount)); + expect(vm.find('.max-issue-size').text()).toContain(String(maxIssueCount)); }); it('does not have text-danger class when issueSize is less than maxIssueCount', () => { @@ -75,7 +75,7 @@ describe('IssueCount', () => { }); it('contains maxIssueCount in the template', () => { - expect(vm.find('.max-issue-size').text()).toEqual(String(maxIssueCount)); + expect(vm.find('.max-issue-size').text()).toContain(String(maxIssueCount)); }); it('has text-danger class', () => { diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js index 1ee05d81f37..dc1f3246be0 100644 --- a/spec/frontend/boards/mock_data.js +++ b/spec/frontend/boards/mock_data.js @@ -262,9 +262,11 @@ export const rawIssue = { epic: { id: 'gid://gitlab/Epic/41', }, + type: 'ISSUE', }; export const mockIssueFullPath = 'gitlab-org/test-subgroup/gitlab-test'; +export const mockEpicFullPath = 'gitlab-org/test-subgroup'; export const mockIssue = { id: 'gid://gitlab/Issue/436', @@ -287,6 +289,48 @@ export const mockIssue = { epic: { id: 'gid://gitlab/Epic/41', }, + type: 'ISSUE', +}; + +export const mockEpic = { + id: 'gid://gitlab/Epic/26', + iid: '1', + group: { + id: 'gid://gitlab/Group/33', + fullPath: 'twitter', + __typename: 'Group', + }, + title: 'Eum animi debitis occaecati ad non odio repellat voluptatem similique.', + state: 'opened', + reference: '&1', + referencePath: `${mockEpicFullPath}&1`, + webPath: `/groups/${mockEpicFullPath}/-/epics/1`, + webUrl: `${mockEpicFullPath}/-/epics/1`, + createdAt: '2022-01-18T05:15:15Z', + closedAt: null, + __typename: 'Epic', + relativePosition: null, + confidential: false, + subscribed: true, + blocked: true, + blockedByCount: 1, + labels: { + nodes: [], + __typename: 'LabelConnection', + }, + hasIssues: true, + descendantCounts: { + closedEpics: 0, + closedIssues: 0, + openedEpics: 0, + openedIssues: 2, + __typename: 'EpicDescendantCount', + }, + descendantWeightSum: { + closedIssues: 0, + openedIssues: 0, + __typename: 'EpicDescendantWeights', + }, }; export const mockActiveIssue = { @@ -521,6 +565,15 @@ export const mockBlockingIssue1 = { __typename: 'Issue', }; +export const mockBlockingEpic1 = { + id: 'gid://gitlab/Epic/29', + iid: '4', + title: 'Sint nihil exercitationem aspernatur unde molestiae rem accusantium.', + reference: 'twitter&4', + webUrl: 'http://gdk.test:3000/groups/gitlab-org/test-subgroup/-/epics/4', + __typename: 'Epic', +}; + export const mockBlockingIssue2 = { id: 'gid://gitlab/Issue/524', iid: '5', @@ -562,6 +615,23 @@ export const mockBlockingIssuablesResponse1 = { }, }; +export const mockBlockingEpicIssuablesResponse1 = { + data: { + group: { + __typename: 'Group', + id: 'gid://gitlab/Group/33', + issuable: { + __typename: 'Epic', + id: 'gid://gitlab/Epic/26', + blockingIssuables: { + __typename: 'EpicConnection', + nodes: [mockBlockingEpic1], + }, + }, + }, + }, +}; + export const mockBlockingIssuablesResponse2 = { data: { issuable: { @@ -599,6 +669,12 @@ export const mockBlockedIssue2 = { webUrl: 'http://gdk.test:3000/gitlab-org/my-project-1/-/issues/0', }; +export const mockBlockedEpic1 = { + id: '26', + blockedByCount: 1, + webUrl: 'http://gdk.test:3000/gitlab-org/test-subgroup/-/epics/1', +}; + export const mockMoveIssueParams = { itemId: 1, fromListId: 'gid://gitlab/List/1', diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js index e48b946ff1b..e919300228a 100644 --- a/spec/frontend/boards/stores/actions_spec.js +++ b/spec/frontend/boards/stores/actions_spec.js @@ -1056,6 +1056,8 @@ describe('moveIssueCard and undoMoveIssueCard', () => { originalIndex = 0, moveBeforeId = undefined, moveAfterId = undefined, + allItemsLoadedInList = true, + listPosition = undefined, } = {}) => { state = { boardLists: { @@ -1065,12 +1067,28 @@ describe('moveIssueCard and undoMoveIssueCard', () => { boardItems: { [itemId]: originalIssue }, boardItemsByListId: { [fromListId]: [123] }, }; - params = { itemId, fromListId, toListId, moveBeforeId, moveAfterId }; + params = { + itemId, + fromListId, + toListId, + moveBeforeId, + moveAfterId, + listPosition, + allItemsLoadedInList, + }; moveMutations = [ { type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: fromListId } }, { type: types.ADD_BOARD_ITEM_TO_LIST, - payload: { itemId, listId: toListId, moveBeforeId, moveAfterId }, + payload: { + itemId, + listId: toListId, + moveBeforeId, + moveAfterId, + listPosition, + allItemsLoadedInList, + atIndex: originalIndex, + }, }, ]; undoMutations = [ @@ -1366,9 +1384,17 @@ describe('updateIssueOrder', () => { state, [ { + type: types.MUTATE_ISSUE_IN_PROGRESS, + payload: true, + }, + { type: types.MUTATE_ISSUE_SUCCESS, payload: { issue: rawIssue }, }, + { + type: types.MUTATE_ISSUE_IN_PROGRESS, + payload: false, + }, ], [], ); @@ -1390,6 +1416,14 @@ describe('updateIssueOrder', () => { state, [ { + type: types.MUTATE_ISSUE_IN_PROGRESS, + payload: true, + }, + { + type: types.MUTATE_ISSUE_IN_PROGRESS, + payload: false, + }, + { type: types.SET_ERROR, payload: 'An error occurred while moving the issue. Please try again.', }, diff --git a/spec/frontend/boards/stores/mutations_spec.js b/spec/frontend/boards/stores/mutations_spec.js index 1606ca09d8f..87a183c0441 100644 --- a/spec/frontend/boards/stores/mutations_spec.js +++ b/spec/frontend/boards/stores/mutations_spec.js @@ -513,6 +513,31 @@ describe('Board Store Mutations', () => { listState: [mockIssue2.id, mockIssue.id], }, ], + [ + 'to the top of the list', + { + payload: { + itemId: mockIssue2.id, + listId: mockList.id, + positionInList: 0, + atIndex: 1, + }, + listState: [mockIssue2.id, mockIssue.id], + }, + ], + [ + 'to the bottom of the list when the list is fully loaded', + { + payload: { + itemId: mockIssue2.id, + listId: mockList.id, + positionInList: -1, + atIndex: 0, + allItemsLoadedInList: true, + }, + listState: [mockIssue.id, mockIssue2.id], + }, + ], ])(`inserts an item into a list %s`, (_, { payload, listState }) => { mutations.ADD_BOARD_ITEM_TO_LIST(state, payload); |