diff options
Diffstat (limited to 'spec/frontend/boards')
22 files changed, 372 insertions, 113 deletions
diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js index 36556ba00af..1740676161f 100644 --- a/spec/frontend/boards/board_card_inner_spec.js +++ b/spec/frontend/boards/board_card_inner_spec.js @@ -2,6 +2,7 @@ import { GlLabel, GlLoadingIcon, GlTooltip } from '@gitlab/ui'; import { range } from 'lodash'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; +// eslint-disable-next-line no-restricted-imports import Vuex from 'vuex'; import createMockApollo from 'helpers/mock_apollo_helper'; import setWindowLocation from 'helpers/set_window_location_helper'; diff --git a/spec/frontend/boards/board_list_helper.js b/spec/frontend/boards/board_list_helper.js index e3cdec1ab6e..7367b34c4df 100644 --- a/spec/frontend/boards/board_list_helper.js +++ b/spec/frontend/boards/board_list_helper.js @@ -1,6 +1,7 @@ import { shallowMount } from '@vue/test-utils'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; +// eslint-disable-next-line no-restricted-imports import Vuex from 'vuex'; import BoardCard from '~/boards/components/board_card.vue'; diff --git a/spec/frontend/boards/cache_updates_spec.js b/spec/frontend/boards/cache_updates_spec.js new file mode 100644 index 00000000000..bc661f20451 --- /dev/null +++ b/spec/frontend/boards/cache_updates_spec.js @@ -0,0 +1,37 @@ +import * as Sentry from '@sentry/browser'; +import { setError } from '~/boards/graphql/cache_updates'; +import { defaultClient } from '~/graphql_shared/issuable_client'; +import setErrorMutation from '~/boards/graphql/client/set_error.mutation.graphql'; + +describe('setError', () => { + let sentryCaptureExceptionSpy; + const errorMessage = 'Error'; + const error = new Error(errorMessage); + + beforeEach(() => { + jest.spyOn(defaultClient, 'mutate').mockResolvedValue(); + sentryCaptureExceptionSpy = jest.spyOn(Sentry, 'captureException'); + }); + + it('calls setErrorMutation and capture Sentry error', () => { + setError({ message: errorMessage, error }); + + expect(defaultClient.mutate).toHaveBeenCalledWith({ + mutation: setErrorMutation, + variables: { error: errorMessage }, + }); + + expect(sentryCaptureExceptionSpy).toHaveBeenCalledWith(error); + }); + + it('does not capture Sentry error when captureError is false', () => { + setError({ message: errorMessage, error, captureError: false }); + + expect(defaultClient.mutate).toHaveBeenCalledWith({ + mutation: setErrorMutation, + variables: { error: errorMessage }, + }); + + expect(sentryCaptureExceptionSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/spec/frontend/boards/components/board_add_new_column_form_spec.js b/spec/frontend/boards/components/board_add_new_column_form_spec.js index 35296f36b89..719e36629c2 100644 --- a/spec/frontend/boards/components/board_add_new_column_form_spec.js +++ b/spec/frontend/boards/components/board_add_new_column_form_spec.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +// eslint-disable-next-line no-restricted-imports import Vuex from 'vuex'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form.vue'; diff --git a/spec/frontend/boards/components/board_add_new_column_spec.js b/spec/frontend/boards/components/board_add_new_column_spec.js index 8d6cc9373af..1a847d35900 100644 --- a/spec/frontend/boards/components/board_add_new_column_spec.js +++ b/spec/frontend/boards/components/board_add_new_column_spec.js @@ -1,14 +1,17 @@ import { GlCollapsibleListbox } from '@gitlab/ui'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; +// eslint-disable-next-line no-restricted-imports import Vuex from 'vuex'; import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import BoardAddNewColumn from '~/boards/components/board_add_new_column.vue'; import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form.vue'; import defaultState from '~/boards/stores/state'; import createBoardListMutation from 'ee_else_ce/boards/graphql/board_list_create.mutation.graphql'; import boardLabelsQuery from '~/boards/graphql/board_labels.query.graphql'; +import * as cacheUpdates from '~/boards/graphql/cache_updates'; import { mockLabelList, createBoardListResponse, @@ -21,13 +24,14 @@ Vue.use(VueApollo); describe('BoardAddNewColumn', () => { let wrapper; + let mockApollo; const createBoardListQueryHandler = jest.fn().mockResolvedValue(createBoardListResponse); const labelsQueryHandler = jest.fn().mockResolvedValue(labelsQueryResponse); - const mockApollo = createMockApollo([ - [boardLabelsQuery, labelsQueryHandler], - [createBoardListMutation, createBoardListQueryHandler], - ]); + const errorMessage = 'Failed to create list'; + const createBoardListQueryHandlerFailure = jest.fn().mockRejectedValue(new Error(errorMessage)); + const errorMessageLabels = 'Failed to fetch labels'; + const labelsQueryHandlerFailure = jest.fn().mockRejectedValue(new Error(errorMessageLabels)); const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox); const findAddNewColumnForm = () => wrapper.findComponent(BoardAddNewColumnForm); @@ -53,7 +57,14 @@ describe('BoardAddNewColumn', () => { actions = {}, provide = {}, lists = {}, + labelsHandler = labelsQueryHandler, + createHandler = createBoardListQueryHandler, } = {}) => { + mockApollo = createMockApollo([ + [boardLabelsQuery, labelsHandler], + [createBoardListMutation, createHandler], + ]); + wrapper = shallowMountExtended(BoardAddNewColumn, { apolloProvider: mockApollo, propsData: { @@ -111,6 +122,10 @@ describe('BoardAddNewColumn', () => { mockApollo.clients.defaultClient.cache.writeQuery = jest.fn(); }; + beforeEach(() => { + cacheUpdates.setError = jest.fn(); + }); + describe('Add list button', () => { it('calls addList', async () => { const getListByLabelId = jest.fn().mockReturnValue(null); @@ -208,11 +223,52 @@ describe('BoardAddNewColumn', () => { findAddNewColumnForm().vm.$emit('add-list'); - await nextTick(); + await waitForPromises(); expect(wrapper.emitted('highlight-list')).toEqual([[mockLabelList.id]]); expect(createBoardListQueryHandler).not.toHaveBeenCalledWith(); }); }); + + describe('when fetch labels query fails', () => { + beforeEach(() => { + mountComponent({ + provide: { isApolloBoard: true }, + labelsHandler: labelsQueryHandlerFailure, + }); + }); + + it('sets error', async () => { + findDropdown().vm.$emit('show'); + + await waitForPromises(); + expect(cacheUpdates.setError).toHaveBeenCalled(); + }); + }); + + describe('when create list mutation fails', () => { + beforeEach(() => { + mountComponent({ + selectedId: mockLabelList.label.id, + provide: { isApolloBoard: true }, + createHandler: createBoardListQueryHandlerFailure, + }); + }); + + it('sets error', async () => { + findDropdown().vm.$emit('show'); + + await nextTick(); + expect(labelsQueryHandler).toHaveBeenCalled(); + + selectLabel(mockLabelList.label.id); + + findAddNewColumnForm().vm.$emit('add-list'); + + await waitForPromises(); + + expect(cacheUpdates.setError).toHaveBeenCalled(); + }); + }); }); }); diff --git a/spec/frontend/boards/components/board_add_new_column_trigger_spec.js b/spec/frontend/boards/components/board_add_new_column_trigger_spec.js index 825cfc9453a..396ec7d67cd 100644 --- a/spec/frontend/boards/components/board_add_new_column_trigger_spec.js +++ b/spec/frontend/boards/components/board_add_new_column_trigger_spec.js @@ -1,5 +1,6 @@ import { GlButton } from '@gitlab/ui'; import Vue from 'vue'; +// eslint-disable-next-line no-restricted-imports import Vuex from 'vuex'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue'; diff --git a/spec/frontend/boards/components/board_app_spec.js b/spec/frontend/boards/components/board_app_spec.js index e7624437ac5..b16f9b26f40 100644 --- a/spec/frontend/boards/components/board_app_spec.js +++ b/spec/frontend/boards/components/board_app_spec.js @@ -1,6 +1,7 @@ import { shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; +// eslint-disable-next-line no-restricted-imports import Vuex from 'vuex'; import createMockApollo from 'helpers/mock_apollo_helper'; @@ -9,13 +10,17 @@ import BoardApp from '~/boards/components/board_app.vue'; import eventHub from '~/boards/eventhub'; import activeBoardItemQuery from 'ee_else_ce/boards/graphql/client/active_board_item.query.graphql'; import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql'; +import * as cacheUpdates from '~/boards/graphql/cache_updates'; import { rawIssue, boardListsQueryResponse } from '../mock_data'; describe('BoardApp', () => { let wrapper; let store; + let mockApollo; + + const errorMessage = 'Failed to fetch lists'; const boardListQueryHandler = jest.fn().mockResolvedValue(boardListsQueryResponse); - const mockApollo = createMockApollo([[boardListsQuery, boardListQueryHandler]]); + const boardListQueryHandlerFailure = jest.fn().mockRejectedValue(new Error(errorMessage)); Vue.use(Vuex); Vue.use(VueApollo); @@ -33,7 +38,12 @@ describe('BoardApp', () => { }); }; - const createComponent = ({ isApolloBoard = false, issue = rawIssue } = {}) => { + const createComponent = ({ + isApolloBoard = false, + issue = rawIssue, + handler = boardListQueryHandler, + } = {}) => { + mockApollo = createMockApollo([[boardListsQuery, handler]]); mockApollo.clients.defaultClient.cache.writeQuery({ query: activeBoardItemQuery, data: { @@ -57,6 +67,10 @@ describe('BoardApp', () => { }); }; + beforeEach(() => { + cacheUpdates.setError = jest.fn(); + }); + afterEach(() => { store = null; }); @@ -104,5 +118,13 @@ describe('BoardApp', () => { expect(eventHub.$on).toHaveBeenCalledWith('updateBoard', wrapper.vm.refetchLists); }); + + it('sets error on fetch lists failure', async () => { + createComponent({ isApolloBoard: true, handler: boardListQueryHandlerFailure }); + + await waitForPromises(); + + expect(cacheUpdates.setError).toHaveBeenCalled(); + }); }); }); 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 index 5f308be5580..20beaf2e9bd 100644 --- a/spec/frontend/boards/components/board_card_move_to_position_spec.js +++ b/spec/frontend/boards/components/board_card_move_to_position_spec.js @@ -1,5 +1,6 @@ import { shallowMount } from '@vue/test-utils'; import Vue from 'vue'; +// eslint-disable-next-line no-restricted-imports import Vuex from 'vuex'; import { GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui'; import { diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js index 897219303b5..167efb94fcc 100644 --- a/spec/frontend/boards/components/board_card_spec.js +++ b/spec/frontend/boards/components/board_card_spec.js @@ -1,5 +1,6 @@ import { GlLabel } from '@gitlab/ui'; import Vue, { nextTick } from 'vue'; +// eslint-disable-next-line no-restricted-imports import Vuex from 'vuex'; import VueApollo from 'vue-apollo'; diff --git a/spec/frontend/boards/components/board_content_sidebar_spec.js b/spec/frontend/boards/components/board_content_sidebar_spec.js index 9be2696de56..01eea12bf0a 100644 --- a/spec/frontend/boards/components/board_content_sidebar_spec.js +++ b/spec/frontend/boards/components/board_content_sidebar_spec.js @@ -2,6 +2,7 @@ import { GlDrawer } from '@gitlab/ui'; import { MountingPortal } from 'portal-vue'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; +// eslint-disable-next-line no-restricted-imports import Vuex from 'vuex'; import SidebarDropdownWidget from 'ee_else_ce/sidebar/components/sidebar_dropdown_widget.vue'; import createMockApollo from 'helpers/mock_apollo_helper'; diff --git a/spec/frontend/boards/components/board_content_spec.js b/spec/frontend/boards/components/board_content_spec.js index 0a2a78479fb..675b79a8b1a 100644 --- a/spec/frontend/boards/components/board_content_spec.js +++ b/spec/frontend/boards/components/board_content_spec.js @@ -1,8 +1,9 @@ import { GlAlert } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import VueApollo from 'vue-apollo'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import Draggable from 'vuedraggable'; +// eslint-disable-next-line no-restricted-imports import Vuex from 'vuex'; import createMockApollo from 'helpers/mock_apollo_helper'; @@ -10,6 +11,7 @@ import { stubComponent } from 'helpers/stub_component'; import waitForPromises from 'helpers/wait_for_promises'; import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue'; import getters from 'ee_else_ce/boards/stores/getters'; +import * as cacheUpdates from '~/boards/graphql/cache_updates'; import BoardColumn from '~/boards/components/board_column.vue'; import BoardContent from '~/boards/components/board_content.vue'; import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue'; @@ -36,6 +38,8 @@ describe('BoardContent', () => { let mockApollo; const updateListHandler = jest.fn().mockResolvedValue(updateBoardListResponse); + const errorMessage = 'Failed to update list'; + const updateListHandlerFailure = jest.fn().mockRejectedValue(new Error(errorMessage)); const defaultState = { isShowingEpicsSwimlanes: false, @@ -60,8 +64,9 @@ describe('BoardContent', () => { issuableType = 'issue', isIssueBoard = true, isEpicBoard = false, + handler = updateListHandler, } = {}) => { - mockApollo = createMockApollo([[updateBoardListMutation, updateListHandler]]); + mockApollo = createMockApollo([[updateBoardListMutation, handler]]); const listQueryVariables = { isProject: true }; mockApollo.clients.defaultClient.writeQuery({ @@ -107,6 +112,11 @@ describe('BoardContent', () => { const findBoardColumns = () => wrapper.findAllComponents(BoardColumn); const findBoardAddNewColumn = () => wrapper.findComponent(BoardAddNewColumn); const findDraggable = () => wrapper.findComponent(Draggable); + const findError = () => wrapper.findComponent(GlAlert); + + beforeEach(() => { + cacheUpdates.setError = jest.fn(); + }); describe('default', () => { beforeEach(() => { @@ -123,7 +133,7 @@ describe('BoardContent', () => { it('does not display EpicsSwimlanes component', () => { expect(wrapper.findComponent(EpicsSwimlanes).exists()).toBe(false); - expect(wrapper.findComponent(GlAlert).exists()).toBe(false); + expect(findError().exists()).toBe(false); }); it('sets delay and delayOnTouchOnly attributes on board list', () => { @@ -169,6 +179,18 @@ describe('BoardContent', () => { }); describe('when Apollo boards FF is on', () => { + const moveList = () => { + const movableListsOrder = [mockLists[0].id, mockLists[1].id]; + + findDraggable().vm.$emit('end', { + item: { dataset: { listId: mockLists[0].id, draggableItemType: DraggableItemTypes.list } }, + newIndex: 1, + to: { + children: movableListsOrder.map((listId) => ({ dataset: { listId } })), + }, + }); + }; + beforeEach(async () => { createComponent({ isApolloBoard: true }); await waitForPromises(); @@ -183,19 +205,38 @@ describe('BoardContent', () => { }); it('reorders lists', async () => { - const movableListsOrder = [mockLists[0].id, mockLists[1].id]; - - findDraggable().vm.$emit('end', { - item: { dataset: { listId: mockLists[0].id, draggableItemType: DraggableItemTypes.list } }, - newIndex: 1, - to: { - children: movableListsOrder.map((listId) => ({ dataset: { listId } })), - }, - }); + moveList(); await waitForPromises(); expect(updateListHandler).toHaveBeenCalled(); }); + + it('sets error on reorder lists failure', async () => { + createComponent({ isApolloBoard: true, handler: updateListHandlerFailure }); + + moveList(); + await waitForPromises(); + + expect(cacheUpdates.setError).toHaveBeenCalled(); + }); + + describe('when error is passed', () => { + beforeEach(async () => { + createComponent({ isApolloBoard: true, props: { apolloError: 'Error' } }); + await waitForPromises(); + }); + + it('displays error banner', () => { + expect(findError().exists()).toBe(true); + }); + + it('dismisses error', async () => { + findError().vm.$emit('dismiss'); + await nextTick(); + + expect(cacheUpdates.setError).toHaveBeenCalledWith({ message: null, captureError: false }); + }); + }); }); describe('when "add column" form is visible', () => { diff --git a/spec/frontend/boards/components/board_filtered_search_spec.js b/spec/frontend/boards/components/board_filtered_search_spec.js index 5a976816f74..0bd936c9abd 100644 --- a/spec/frontend/boards/components/board_filtered_search_spec.js +++ b/spec/frontend/boards/components/board_filtered_search_spec.js @@ -1,5 +1,6 @@ import { shallowMount } from '@vue/test-utils'; import Vue from 'vue'; +// eslint-disable-next-line no-restricted-imports import Vuex from 'vuex'; import BoardFilteredSearch from '~/boards/components/board_filtered_search.vue'; import { updateHistory } from '~/lib/utils/url_utility'; diff --git a/spec/frontend/boards/components/board_form_spec.js b/spec/frontend/boards/components/board_form_spec.js index 5604c589e37..15ee3976fb1 100644 --- a/spec/frontend/boards/components/board_form_spec.js +++ b/spec/frontend/boards/components/board_form_spec.js @@ -1,5 +1,6 @@ import { GlModal } from '@gitlab/ui'; import Vue from 'vue'; +// eslint-disable-next-line no-restricted-imports import Vuex from 'vuex'; import VueApollo from 'vue-apollo'; import setWindowLocation from 'helpers/set_window_location_helper'; diff --git a/spec/frontend/boards/components/board_list_header_spec.js b/spec/frontend/boards/components/board_list_header_spec.js index 0c9e1b4646e..76e969f1725 100644 --- a/spec/frontend/boards/components/board_list_header_spec.js +++ b/spec/frontend/boards/components/board_list_header_spec.js @@ -1,9 +1,11 @@ import { GlButtonGroup } from '@gitlab/ui'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; +// eslint-disable-next-line no-restricted-imports import Vuex from 'vuex'; import createMockApollo from 'helpers/mock_apollo_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; import { boardListQueryResponse, mockLabelList, @@ -12,6 +14,7 @@ import { import BoardListHeader from '~/boards/components/board_list_header.vue'; import updateBoardListMutation from '~/boards/graphql/board_list_update.mutation.graphql'; import { ListType } from '~/boards/constants'; +import * as cacheUpdates from '~/boards/graphql/cache_updates'; import listQuery from 'ee_else_ce/boards/graphql/board_lists_deferred.query.graphql'; Vue.use(VueApollo); @@ -25,7 +28,11 @@ describe('Board List Header Component', () => { const updateListSpy = jest.fn(); const toggleListCollapsedSpy = jest.fn(); const mockClientToggleListCollapsedResolver = jest.fn(); - const updateListHandler = jest.fn().mockResolvedValue(updateBoardListResponse); + const updateListHandlerSuccess = jest.fn().mockResolvedValue(updateBoardListResponse); + + beforeEach(() => { + cacheUpdates.setError = jest.fn(); + }); afterEach(() => { fakeApollo = null; @@ -39,6 +46,7 @@ describe('Board List Header Component', () => { withLocalStorage = true, currentUserId = 1, listQueryHandler = jest.fn().mockResolvedValue(boardListQueryResponse()), + updateListHandler = updateListHandlerSuccess, injectedProps = {}, } = {}) => { const boardId = 'gid://gitlab/Board/1'; @@ -271,7 +279,7 @@ describe('Board List Header Component', () => { findCaret().vm.$emit('click'); await nextTick(); - expect(updateListHandler).not.toHaveBeenCalled(); + expect(updateListHandlerSuccess).not.toHaveBeenCalled(); }); it('calls update list mutation when user is logged in', async () => { @@ -280,7 +288,50 @@ describe('Board List Header Component', () => { findCaret().vm.$emit('click'); await nextTick(); - expect(updateListHandler).toHaveBeenCalledWith({ listId: mockLabelList.id, collapsed: true }); + expect(updateListHandlerSuccess).toHaveBeenCalledWith({ + listId: mockLabelList.id, + collapsed: true, + }); + }); + + describe('when fetch list query fails', () => { + const errorMessage = 'Failed to fetch list'; + const listQueryHandlerFailure = jest.fn().mockRejectedValue(new Error(errorMessage)); + + beforeEach(() => { + createComponent({ + listQueryHandler: listQueryHandlerFailure, + injectedProps: { isApolloBoard: true }, + }); + }); + + it('sets error', async () => { + await waitForPromises(); + + expect(cacheUpdates.setError).toHaveBeenCalled(); + }); + }); + + describe('when update list mutation fails', () => { + const errorMessage = 'Failed to update list'; + const updateListHandlerFailure = jest.fn().mockRejectedValue(new Error(errorMessage)); + + beforeEach(() => { + createComponent({ + currentUserId: 1, + updateListHandler: updateListHandlerFailure, + injectedProps: { isApolloBoard: true }, + }); + }); + + it('sets error', async () => { + await waitForPromises(); + + findCaret().vm.$emit('click'); + await waitForPromises(); + + expect(cacheUpdates.setError).toHaveBeenCalled(); + }); }); }); }); diff --git a/spec/frontend/boards/components/board_new_issue_spec.js b/spec/frontend/boards/components/board_new_issue_spec.js index a1088f1e8f7..bf2608d0594 100644 --- a/spec/frontend/boards/components/board_new_issue_spec.js +++ b/spec/frontend/boards/components/board_new_issue_spec.js @@ -1,5 +1,6 @@ import { shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; +// eslint-disable-next-line no-restricted-imports import Vuex from 'vuex'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; diff --git a/spec/frontend/boards/components/board_settings_sidebar_spec.js b/spec/frontend/boards/components/board_settings_sidebar_spec.js index affe1260c66..f6ed483dfc5 100644 --- a/spec/frontend/boards/components/board_settings_sidebar_spec.js +++ b/spec/frontend/boards/components/board_settings_sidebar_spec.js @@ -3,14 +3,17 @@ import { shallowMount } from '@vue/test-utils'; import { MountingPortal } from 'portal-vue'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; +// eslint-disable-next-line no-restricted-imports import Vuex from 'vuex'; import createMockApollo from 'helpers/mock_apollo_helper'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; +import waitForPromises from 'helpers/wait_for_promises'; import { stubComponent } from 'helpers/stub_component'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import BoardSettingsSidebar from '~/boards/components/board_settings_sidebar.vue'; import { inactiveId, LIST } from '~/boards/constants'; import destroyBoardListMutation from '~/boards/graphql/board_list_destroy.mutation.graphql'; +import * as cacheUpdates from '~/boards/graphql/cache_updates'; import actions from '~/boards/stores/actions'; import getters from '~/boards/stores/getters'; import mutations from '~/boards/stores/mutations'; @@ -31,12 +34,17 @@ describe('BoardSettingsSidebar', () => { const destroyBoardListMutationHandlerSuccess = jest .fn() .mockResolvedValue(destroyBoardListMutationResponse); + const errorMessage = 'Failed to delete list'; + const destroyBoardListMutationHandlerFailure = jest + .fn() + .mockRejectedValue(new Error(errorMessage)); const createComponent = ({ canAdminList = false, list = {}, sidebarType = LIST, activeId = inactiveId, + destroyBoardListMutationHandler = destroyBoardListMutationHandlerSuccess, isApolloBoard = false, } = {}) => { const boardLists = { @@ -49,9 +57,7 @@ describe('BoardSettingsSidebar', () => { actions, }); - mockApollo = createMockApollo([ - [destroyBoardListMutation, destroyBoardListMutationHandlerSuccess], - ]); + mockApollo = createMockApollo([[destroyBoardListMutation, destroyBoardListMutationHandler]]); wrapper = extendedWrapper( shallowMount(BoardSettingsSidebar, { @@ -90,6 +96,10 @@ describe('BoardSettingsSidebar', () => { const findModal = () => wrapper.findComponent(GlModal); const findRemoveButton = () => wrapper.findComponent(GlButton); + beforeEach(() => { + cacheUpdates.setError = jest.fn(); + }); + it('finds a MountingPortal component', () => { createComponent(); @@ -214,5 +224,23 @@ describe('BoardSettingsSidebar', () => { createComponent({ canAdminList: true, activeId: listId, list: mockLabelList }); expect(findModal().props('modalId')).toBe(modalID); }); + + it('sets error when destroy list mutation fails', async () => { + createComponent({ + canAdminList: true, + activeId: listId, + list: mockLabelList, + destroyBoardListMutationHandler: destroyBoardListMutationHandlerFailure, + isApolloBoard: true, + }); + + findRemoveButton().vm.$emit('click'); + + wrapper.findComponent(GlModal).vm.$emit('primary'); + + await waitForPromises(); + + expect(cacheUpdates.setError).toHaveBeenCalled(); + }); }); }); diff --git a/spec/frontend/boards/components/board_top_bar_spec.js b/spec/frontend/boards/components/board_top_bar_spec.js index afc7da97617..87abe630688 100644 --- a/spec/frontend/boards/components/board_top_bar_spec.js +++ b/spec/frontend/boards/components/board_top_bar_spec.js @@ -1,8 +1,10 @@ import { shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; +// eslint-disable-next-line no-restricted-imports import Vuex from 'vuex'; import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; import BoardTopBar from '~/boards/components/board_top_bar.vue'; import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue'; @@ -11,6 +13,7 @@ import ConfigToggle from '~/boards/components/config_toggle.vue'; import IssueBoardFilteredSearch from '~/boards/components/issue_board_filtered_search.vue'; import NewBoardButton from '~/boards/components/new_board_button.vue'; import ToggleFocus from '~/boards/components/toggle_focus.vue'; +import * as cacheUpdates from '~/boards/graphql/cache_updates'; import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants'; import groupBoardQuery from '~/boards/graphql/group_board.query.graphql'; @@ -32,12 +35,18 @@ describe('BoardTopBar', () => { const projectBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockProjectBoardResponse); const groupBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockGroupBoardResponse); - - const createComponent = ({ provide = {} } = {}) => { + const errorMessage = 'Failed to fetch board'; + const boardQueryHandlerFailure = jest.fn().mockRejectedValue(new Error(errorMessage)); + + const createComponent = ({ + provide = {}, + projectBoardQueryHandler = projectBoardQueryHandlerSuccess, + groupBoardQueryHandler = groupBoardQueryHandlerSuccess, + } = {}) => { const store = createStore(); mockApollo = createMockApollo([ - [projectBoardQuery, projectBoardQueryHandlerSuccess], - [groupBoardQuery, groupBoardQueryHandlerSuccess], + [projectBoardQuery, projectBoardQueryHandler], + [groupBoardQuery, groupBoardQueryHandler], ]); wrapper = shallowMount(BoardTopBar, { @@ -65,6 +74,10 @@ describe('BoardTopBar', () => { }); }; + beforeEach(() => { + cacheUpdates.setError = jest.fn(); + }); + afterEach(() => { mockApollo = null; }); @@ -134,5 +147,25 @@ describe('BoardTopBar', () => { expect(queryHandler).toHaveBeenCalled(); expect(notCalledHandler).not.toHaveBeenCalled(); }); + + it.each` + boardType + ${WORKSPACE_GROUP} + ${WORKSPACE_PROJECT} + `('sets error when $boardType board query fails', async ({ boardType }) => { + createComponent({ + provide: { + boardType, + isProjectBoard: boardType === WORKSPACE_PROJECT, + isGroupBoard: boardType === WORKSPACE_GROUP, + isApolloBoard: true, + }, + groupBoardQueryHandler: boardQueryHandlerFailure, + projectBoardQueryHandler: boardQueryHandlerFailure, + }); + + await waitForPromises(); + expect(cacheUpdates.setError).toHaveBeenCalled(); + }); }); }); diff --git a/spec/frontend/boards/components/boards_selector_spec.js b/spec/frontend/boards/components/boards_selector_spec.js index 13c017706ef..b17a5589c07 100644 --- a/spec/frontend/boards/components/boards_selector_spec.js +++ b/spec/frontend/boards/components/boards_selector_spec.js @@ -1,6 +1,7 @@ import { GlDropdown, GlLoadingIcon, GlDropdownSectionHeader } from '@gitlab/ui'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; +// eslint-disable-next-line no-restricted-imports import Vuex from 'vuex'; import waitForPromises from 'helpers/wait_for_promises'; import { TEST_HOST } from 'spec/test_constants'; @@ -9,6 +10,7 @@ import groupBoardsQuery from '~/boards/graphql/group_boards.query.graphql'; import projectBoardsQuery from '~/boards/graphql/project_boards.query.graphql'; import groupRecentBoardsQuery from '~/boards/graphql/group_recent_boards.query.graphql'; import projectRecentBoardsQuery from '~/boards/graphql/project_recent_boards.query.graphql'; +import * as cacheUpdates from '~/boards/graphql/cache_updates'; import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants'; import createMockApollo from 'helpers/mock_apollo_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper'; @@ -37,7 +39,6 @@ describe('BoardsSelector', () => { const createStore = () => { store = new Vuex.Store({ actions: { - setError: jest.fn(), setBoardConfig: jest.fn(), }, state: { @@ -77,16 +78,19 @@ describe('BoardsSelector', () => { .fn() .mockResolvedValue(mockEmptyProjectRecentBoardsResponse); + const boardsHandlerFailure = jest.fn().mockRejectedValue(new Error('error')); + const createComponent = ({ projectBoardsQueryHandler = projectBoardsQueryHandlerSuccess, projectRecentBoardsQueryHandler = projectRecentBoardsQueryHandlerSuccess, + groupBoardsQueryHandler = groupBoardsQueryHandlerSuccess, isGroupBoard = false, isProjectBoard = false, provide = {}, } = {}) => { fakeApollo = createMockApollo([ [projectBoardsQuery, projectBoardsQueryHandler], - [groupBoardsQuery, groupBoardsQueryHandlerSuccess], + [groupBoardsQuery, groupBoardsQueryHandler], [projectRecentBoardsQuery, projectRecentBoardsQueryHandler], [groupRecentBoardsQuery, groupRecentBoardsQueryHandlerSuccess], ]); @@ -115,6 +119,10 @@ describe('BoardsSelector', () => { }); }; + beforeEach(() => { + cacheUpdates.setError = jest.fn(); + }); + afterEach(() => { fakeApollo = null; }); @@ -173,8 +181,7 @@ describe('BoardsSelector', () => { it('shows only matching boards when filtering', async () => { const filterTerm = 'board1'; - const expectedCount = boards.filter((board) => board.node.name.includes(filterTerm)) - .length; + const expectedCount = boards.filter((board) => board.name.includes(filterTerm)).length; fillSearchBox(filterTerm); @@ -246,6 +253,29 @@ describe('BoardsSelector', () => { expect(queryHandler).toHaveBeenCalled(); expect(notCalledHandler).not.toHaveBeenCalled(); }); + + it.each` + boardType + ${WORKSPACE_GROUP} + ${WORKSPACE_PROJECT} + `('sets error when fetching $boardType boards fails', async ({ boardType }) => { + createStore(); + createComponent({ + isGroupBoard: boardType === WORKSPACE_GROUP, + isProjectBoard: boardType === WORKSPACE_PROJECT, + projectBoardsQueryHandler: boardsHandlerFailure, + groupBoardsQueryHandler: boardsHandlerFailure, + }); + + await nextTick(); + + // Emits gl-dropdown show event to simulate the dropdown is opened at initialization time + findDropdown().vm.$emit('show'); + + await waitForPromises(); + + expect(cacheUpdates.setError).toHaveBeenCalled(); + }); }); describe('dropdown visibility', () => { diff --git a/spec/frontend/boards/components/config_toggle_spec.js b/spec/frontend/boards/components/config_toggle_spec.js index 5330721451e..3d505038331 100644 --- a/spec/frontend/boards/components/config_toggle_spec.js +++ b/spec/frontend/boards/components/config_toggle_spec.js @@ -1,7 +1,9 @@ +// eslint-disable-next-line no-restricted-imports import Vuex from 'vuex'; import Vue from 'vue'; import { shallowMount } from '@vue/test-utils'; import { GlButton } from '@gitlab/ui'; +import { __ } from '~/locale'; import ConfigToggle from '~/boards/components/config_toggle.vue'; import eventHub from '~/boards/eventhub'; import store from '~/boards/stores'; @@ -12,13 +14,14 @@ describe('ConfigToggle', () => { Vue.use(Vuex); - const createComponent = (provide = {}) => + const createComponent = (provide = {}, props = {}) => shallowMount(ConfigToggle, { store, provide: { canAdminList: true, ...provide, }, + propsData: props, }); const findButton = () => wrapper.findComponent(GlButton); @@ -52,4 +55,20 @@ describe('ConfigToggle', () => { label: 'edit_board', }); }); + + it.each` + boardHasScope + ${true} + ${false} + `('renders dot highlight and tooltip depending on boardHasScope prop', ({ boardHasScope }) => { + wrapper = createComponent({}, { boardHasScope }); + + expect(findButton().classes('dot-highlight')).toBe(boardHasScope); + + if (boardHasScope) { + expect(findButton().attributes('title')).toBe(__("This board's scope is reduced")); + } else { + expect(findButton().attributes('title')).toBe(''); + } + }); }); diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js deleted file mode 100644 index b01ee01120e..00000000000 --- a/spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js +++ /dev/null @@ -1,67 +0,0 @@ -/* - To avoid duplicating tests in time_tracker.spec, - this spec only contains a simple test to check rendering. - - A detailed feature spec is used to test time tracking feature - in swimlanes sidebar. -*/ - -import { shallowMount } from '@vue/test-utils'; -import BoardSidebarTimeTracker from '~/boards/components/sidebar/board_sidebar_time_tracker.vue'; -import { createStore } from '~/boards/stores'; -import IssuableTimeTracker from '~/sidebar/components/time_tracking/time_tracker.vue'; - -describe('BoardSidebarTimeTracker', () => { - let wrapper; - let store; - - const createComponent = (options) => { - wrapper = shallowMount(BoardSidebarTimeTracker, { - store, - ...options, - }); - }; - - beforeEach(() => { - store = createStore(); - store.state.boardItems = { - 1: { - id: 1, - iid: 1, - timeEstimate: 3600, - totalTimeSpent: 1800, - humanTimeEstimate: '1h', - humanTotalTimeSpent: '30min', - }, - }; - store.state.activeId = '1'; - }); - - it.each` - timeTrackingLimitToHours | canUpdate - ${true} | ${false} - ${true} | ${true} - ${false} | ${false} - ${false} | ${true} - `( - 'renders IssuableTimeTracker with correct spent and estimated time (timeTrackingLimitToHours=$timeTrackingLimitToHours, canUpdate=$canUpdate)', - ({ timeTrackingLimitToHours, canUpdate }) => { - createComponent({ provide: { timeTrackingLimitToHours, canUpdate } }); - - expect(wrapper.findComponent(IssuableTimeTracker).props()).toEqual({ - limitToHours: timeTrackingLimitToHours, - canAddTimeEntries: canUpdate, - showCollapsed: false, - issuableId: '1', - issuableIid: '1', - fullPath: '', - initialTimeTracking: { - timeEstimate: 3600, - totalTimeSpent: 1800, - humanTimeEstimate: '1h', - humanTotalTimeSpent: '30min', - }, - }); - }, - ); -}); diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js index 8235c3e4194..8f57a6eb7da 100644 --- a/spec/frontend/boards/mock_data.js +++ b/spec/frontend/boards/mock_data.js @@ -110,12 +110,10 @@ function boardGenerator(n) { const name = `board${id}`; return { - node: { - id, - name, - weight: 0, - __typename: 'Board', - }, + id, + name, + weight: 0, + __typename: 'Board', }; }); } @@ -127,7 +125,7 @@ export const mockSmallProjectAllBoardsResponse = { data: { project: { id: 'gid://gitlab/Project/114', - boards: { edges: boardGenerator(3) }, + boards: { nodes: boardGenerator(3) }, __typename: 'Project', }, }, @@ -137,7 +135,7 @@ export const mockEmptyProjectRecentBoardsResponse = { data: { project: { id: 'gid://gitlab/Project/114', - recentIssueBoards: { edges: [] }, + recentIssueBoards: { nodes: [] }, __typename: 'Project', }, }, @@ -147,7 +145,7 @@ export const mockGroupAllBoardsResponse = { data: { group: { id: 'gid://gitlab/Group/114', - boards: { edges: boards }, + boards: { nodes: boards }, __typename: 'Group', }, }, @@ -157,7 +155,7 @@ export const mockProjectAllBoardsResponse = { data: { project: { id: 'gid://gitlab/Project/1', - boards: { edges: boards }, + boards: { nodes: boards }, __typename: 'Project', }, }, @@ -167,7 +165,7 @@ export const mockGroupRecentBoardsResponse = { data: { group: { id: 'gid://gitlab/Group/114', - recentIssueBoards: { edges: recentIssueBoards }, + recentIssueBoards: { nodes: recentIssueBoards }, __typename: 'Group', }, }, @@ -177,7 +175,7 @@ export const mockProjectRecentBoardsResponse = { data: { project: { id: 'gid://gitlab/Project/1', - recentIssueBoards: { edges: recentIssueBoards }, + recentIssueBoards: { nodes: recentIssueBoards }, __typename: 'Project', }, }, diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js index a2961fb1302..5b4b79c650a 100644 --- a/spec/frontend/boards/stores/actions_spec.js +++ b/spec/frontend/boards/stores/actions_spec.js @@ -1,6 +1,7 @@ import * as Sentry from '@sentry/browser'; import { cloneDeep } from 'lodash'; import Vue from 'vue'; +// eslint-disable-next-line no-restricted-imports import Vuex from 'vuex'; import { inactiveId, ISSUABLE, ListType, DraggableItemTypes } from 'ee_else_ce/boards/constants'; import issueMoveListMutation from 'ee_else_ce/boards/graphql/issue_move_list.mutation.graphql'; |