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/issuable/bulk_update_sidebar/components/move_issues_button_spec.js')
-rw-r--r--spec/frontend/issuable/bulk_update_sidebar/components/move_issues_button_spec.js554
1 files changed, 554 insertions, 0 deletions
diff --git a/spec/frontend/issuable/bulk_update_sidebar/components/move_issues_button_spec.js b/spec/frontend/issuable/bulk_update_sidebar/components/move_issues_button_spec.js
new file mode 100644
index 00000000000..c432d722637
--- /dev/null
+++ b/spec/frontend/issuable/bulk_update_sidebar/components/move_issues_button_spec.js
@@ -0,0 +1,554 @@
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+import { cloneDeep } from 'lodash';
+import VueApollo from 'vue-apollo';
+import { GlAlert } from '@gitlab/ui';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
+import createFlash from '~/flash';
+import { logError } from '~/lib/logger';
+import IssuableMoveDropdown from '~/vue_shared/components/sidebar/issuable_move_dropdown.vue';
+import MoveIssuesButton from '~/issuable/bulk_update_sidebar/components/move_issues_button.vue';
+import issuableEventHub from '~/issues/list/eventhub';
+import moveIssueMutation from '~/issuable/bulk_update_sidebar/components/graphql/mutations/move_issue.mutation.graphql';
+import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql';
+import getIssuesCountsQuery from 'ee_else_ce/issues/list/queries/get_issues_counts.query.graphql';
+import { getIssuesCountsQueryResponse, getIssuesQueryResponse } from 'jest/issues/list/mock_data';
+import {
+ WORK_ITEM_TYPE_ENUM_ISSUE,
+ WORK_ITEM_TYPE_ENUM_INCIDENT,
+ WORK_ITEM_TYPE_ENUM_TASK,
+ WORK_ITEM_TYPE_ENUM_TEST_CASE,
+} from '~/work_items/constants';
+
+jest.mock('~/flash');
+jest.mock('~/lib/logger');
+useMockLocationHelper();
+
+const mockDefaultProps = {
+ projectFullPath: 'flight/FlightJS',
+ projectsFetchPath: '/-/autocomplete/projects?project_id=1',
+};
+
+const mockDestinationProject = {
+ full_path: 'gitlab-org/GitLabTest',
+};
+
+const mockMutationErrorMessage = 'Example error message';
+
+const mockIssue = {
+ iid: '15',
+ type: WORK_ITEM_TYPE_ENUM_ISSUE,
+};
+
+const mockIncident = {
+ iid: '32',
+ type: WORK_ITEM_TYPE_ENUM_INCIDENT,
+};
+
+const mockTask = {
+ iid: '40',
+ type: WORK_ITEM_TYPE_ENUM_TASK,
+};
+
+const mockTestCase = {
+ iid: '51',
+ type: WORK_ITEM_TYPE_ENUM_TEST_CASE,
+};
+
+const selectedIssuesMocks = {
+ tasksOnly: [mockTask],
+ testCasesOnly: [mockTestCase],
+ issuesOnly: [mockIssue, mockIncident],
+ tasksAndTestCases: [mockTask, mockTestCase],
+ issuesAndTasks: [mockIssue, mockIncident, mockTask],
+ issuesAndTestCases: [mockIssue, mockIncident, mockTestCase],
+ issuesTasksAndTestCases: [mockIssue, mockIncident, mockTask, mockTestCase],
+};
+
+let getIssuesQueryCompleteResponse = getIssuesQueryResponse;
+if (IS_EE) {
+ getIssuesQueryCompleteResponse = cloneDeep(getIssuesQueryResponse);
+ getIssuesQueryCompleteResponse.data.project.issues.nodes[0].blockingCount = 1;
+ getIssuesQueryCompleteResponse.data.project.issues.nodes[0].healthStatus = null;
+ getIssuesQueryCompleteResponse.data.project.issues.nodes[0].weight = 5;
+}
+
+const resolvedMutationWithoutErrorsMock = jest.fn().mockResolvedValue({
+ data: {
+ issueMove: {
+ errors: [],
+ },
+ },
+});
+
+const resolvedMutationWithErrorsMock = jest.fn().mockResolvedValue({
+ data: {
+ issueMove: {
+ errors: [{ message: mockMutationErrorMessage }],
+ },
+ },
+});
+
+const rejectedMutationMock = jest.fn().mockRejectedValue({});
+
+const mockIssuesQueryResponse = jest.fn().mockResolvedValue(getIssuesQueryCompleteResponse);
+const mockIssuesCountsQueryResponse = jest.fn().mockResolvedValue(getIssuesCountsQueryResponse);
+
+describe('MoveIssuesButton', () => {
+ Vue.use(VueApollo);
+
+ let wrapper;
+ let fakeApollo;
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findDropdown = () => wrapper.findComponent(IssuableMoveDropdown);
+ const emitMoveIssuablesEvent = () => {
+ findDropdown().vm.$emit('move-issuable', mockDestinationProject);
+ };
+
+ const createComponent = (data = {}, mutationResolverMock = rejectedMutationMock) => {
+ fakeApollo = createMockApollo([
+ [moveIssueMutation, mutationResolverMock],
+ [getIssuesQuery, mockIssuesQueryResponse],
+ [getIssuesCountsQuery, mockIssuesCountsQueryResponse],
+ ]);
+
+ fakeApollo.defaultClient.cache.writeQuery({
+ query: getIssuesQuery,
+ variables: {
+ isProject: true,
+ fullPath: mockDefaultProps.projectFullPath,
+ },
+ data: getIssuesQueryCompleteResponse.data,
+ });
+
+ fakeApollo.defaultClient.cache.writeQuery({
+ query: getIssuesCountsQuery,
+ variables: {
+ isProject: true,
+ },
+ data: getIssuesCountsQueryResponse.data,
+ });
+
+ wrapper = shallowMount(MoveIssuesButton, {
+ data() {
+ return {
+ ...data,
+ };
+ },
+ propsData: {
+ ...mockDefaultProps,
+ },
+ apolloProvider: fakeApollo,
+ });
+ };
+
+ beforeEach(() => {
+ // Needed due to a bug in Apollo: https://github.com/apollographql/apollo-client/issues/8900
+ // eslint-disable-next-line no-console
+ console.warn = jest.fn();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ fakeApollo = null;
+ });
+
+ describe('`Move selected` dropdown', () => {
+ it('renders disabled by default', () => {
+ createComponent();
+ expect(findDropdown().exists()).toBe(true);
+ expect(findDropdown().attributes('disabled')).toBe('true');
+ });
+
+ it.each`
+ selectedIssuablesMock | disabled | status | testMessage
+ ${[]} | ${true} | ${'disabled'} | ${'nothing is selected'}
+ ${selectedIssuesMocks.tasksOnly} | ${true} | ${'disabled'} | ${'only tasks are selected'}
+ ${selectedIssuesMocks.testCasesOnly} | ${true} | ${'disabled'} | ${'only test cases are selected'}
+ ${selectedIssuesMocks.issuesOnly} | ${false} | ${'enabled'} | ${'only issues are selected'}
+ ${selectedIssuesMocks.tasksAndTestCases} | ${true} | ${'disabled'} | ${'tasks and test cases are selected'}
+ ${selectedIssuesMocks.issuesAndTasks} | ${false} | ${'enabled'} | ${'issues and tasks are selected'}
+ ${selectedIssuesMocks.issuesAndTestCases} | ${false} | ${'enabled'} | ${'issues and test cases are selected'}
+ ${selectedIssuesMocks.issuesTasksAndTestCases} | ${false} | ${'enabled'} | ${'issues and tasks and test cases are selected'}
+ `('renders $status if $testMessage', async ({ selectedIssuablesMock, disabled }) => {
+ createComponent({ selectedIssuables: selectedIssuablesMock });
+
+ await nextTick();
+
+ if (disabled) {
+ expect(findDropdown().attributes('disabled')).toBe('true');
+ } else {
+ expect(findDropdown().attributes('disabled')).toBeUndefined();
+ }
+ });
+ });
+
+ describe('warning message', () => {
+ it.each`
+ selectedIssuablesMock | warningExists | visibility | message | testMessage
+ ${[]} | ${false} | ${'not visible'} | ${'empty'} | ${'nothing is selected'}
+ ${selectedIssuesMocks.tasksOnly} | ${true} | ${'visible'} | ${'Tasks can not be moved.'} | ${'only tasks are selected'}
+ ${selectedIssuesMocks.testCasesOnly} | ${true} | ${'visible'} | ${'Test cases can not be moved.'} | ${'only test cases are selected'}
+ ${selectedIssuesMocks.issuesOnly} | ${false} | ${'not visible'} | ${'empty'} | ${'only issues are selected'}
+ ${selectedIssuesMocks.tasksAndTestCases} | ${true} | ${'visible'} | ${'Tasks and test cases can not be moved.'} | ${'tasks and test cases are selected'}
+ ${selectedIssuesMocks.issuesAndTasks} | ${true} | ${'visible'} | ${'Tasks can not be moved.'} | ${'issues and tasks are selected'}
+ ${selectedIssuesMocks.issuesAndTestCases} | ${true} | ${'visible'} | ${'Test cases can not be moved.'} | ${'issues and test cases are selected'}
+ ${selectedIssuesMocks.issuesTasksAndTestCases} | ${true} | ${'visible'} | ${'Tasks and test cases can not be moved.'} | ${'issues and tasks and test cases are selected'}
+ `(
+ 'is $visibility with `$message` message if $testMessage',
+ async ({ selectedIssuablesMock, warningExists, message }) => {
+ createComponent({ selectedIssuables: selectedIssuablesMock });
+
+ await nextTick();
+
+ const alert = findAlert();
+ expect(alert.exists()).toBe(warningExists);
+
+ if (warningExists) {
+ expect(alert.text()).toBe(message);
+ expect(alert.attributes('variant')).toBe('warning');
+ }
+ },
+ );
+ });
+
+ describe('moveIssues method', () => {
+ describe('changes the `Move selected` dropdown loading state', () => {
+ it('keeps loading state to false when no issue is selected', async () => {
+ createComponent();
+ emitMoveIssuablesEvent();
+
+ await nextTick();
+
+ expect(findDropdown().props('moveInProgress')).toBe(false);
+ });
+
+ it('keeps loading state to false when only tasks are selected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.tasksOnly });
+ emitMoveIssuablesEvent();
+
+ await nextTick();
+
+ expect(findDropdown().props('moveInProgress')).toBe(false);
+ });
+
+ it('keeps loading state to false when only test cases are selected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.testCasesOnly });
+ emitMoveIssuablesEvent();
+
+ await nextTick();
+
+ expect(findDropdown().props('moveInProgress')).toBe(false);
+ });
+
+ it('keeps loading state to false when only tasks and test cases are selected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.tasksAndTestCases });
+ emitMoveIssuablesEvent();
+
+ await nextTick();
+
+ expect(findDropdown().props('moveInProgress')).toBe(false);
+ });
+
+ it('sets loading state to true when issues are moving', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases });
+ emitMoveIssuablesEvent();
+
+ await nextTick();
+
+ expect(findDropdown().props('moveInProgress')).toBe(true);
+ });
+
+ it('sets loading state to false when all mutations succeed', async () => {
+ createComponent(
+ { selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases },
+ resolvedMutationWithoutErrorsMock,
+ );
+ emitMoveIssuablesEvent();
+
+ await nextTick();
+ await waitForPromises();
+
+ expect(findDropdown().props('moveInProgress')).toBe(false);
+ });
+
+ it('sets loading state to false when a mutation returns errors', async () => {
+ createComponent(
+ { selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases },
+ resolvedMutationWithErrorsMock,
+ );
+ emitMoveIssuablesEvent();
+
+ await nextTick();
+ await waitForPromises();
+
+ expect(findDropdown().props('moveInProgress')).toBe(false);
+ });
+
+ it('sets loading state to false when a mutation is rejected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases });
+ emitMoveIssuablesEvent();
+
+ await nextTick();
+ await waitForPromises();
+
+ expect(findDropdown().props('moveInProgress')).toBe(false);
+ });
+ });
+
+ describe('handles events', () => {
+ beforeEach(() => {
+ jest.spyOn(issuableEventHub, '$emit');
+ });
+
+ it('does not emit any event when no issue is selected', async () => {
+ createComponent();
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(issuableEventHub.$emit).not.toHaveBeenCalled();
+ });
+
+ it('does not emit any event when only tasks are selected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.tasksOnly });
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(issuableEventHub.$emit).not.toHaveBeenCalled();
+ });
+
+ it('does not emit any event when only test cases are selected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.testCasesOnly });
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(issuableEventHub.$emit).not.toHaveBeenCalled();
+ });
+
+ it('does not emit any event when only tasks and test cases are selected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.tasksAndTestCases });
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(issuableEventHub.$emit).not.toHaveBeenCalled();
+ });
+
+ it('emits `issuables:bulkMoveStarted` when issues are moving', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases });
+ emitMoveIssuablesEvent();
+
+ expect(issuableEventHub.$emit).toHaveBeenCalledWith('issuables:bulkMoveStarted');
+ });
+
+ it('emits `issuables:bulkMoveEnded` when all mutations succeed', async () => {
+ createComponent(
+ { selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases },
+ resolvedMutationWithoutErrorsMock,
+ );
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(issuableEventHub.$emit).toHaveBeenCalledWith('issuables:bulkMoveEnded');
+ });
+
+ it('emits `issuables:bulkMoveEnded` when a mutation returns errors', async () => {
+ createComponent(
+ { selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases },
+ resolvedMutationWithErrorsMock,
+ );
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(issuableEventHub.$emit).toHaveBeenCalledWith('issuables:bulkMoveEnded');
+ });
+
+ it('emits `issuables:bulkMoveEnded` when a mutation is rejected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases });
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(issuableEventHub.$emit).toHaveBeenCalledWith('issuables:bulkMoveEnded');
+ });
+ });
+
+ describe('shows errors', () => {
+ it('does not create flashes or logs errors when no issue is selected', async () => {
+ createComponent();
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(logError).not.toHaveBeenCalled();
+ expect(createFlash).not.toHaveBeenCalled();
+ });
+
+ it('does not create flashes or logs errors when only tasks are selected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.tasksOnly });
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(logError).not.toHaveBeenCalled();
+ expect(createFlash).not.toHaveBeenCalled();
+ });
+
+ it('does not create flashes or logs errors when only test cases are selected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.testCasesOnly });
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(logError).not.toHaveBeenCalled();
+ expect(createFlash).not.toHaveBeenCalled();
+ });
+
+ it('does not create flashes or logs errors when only tasks and test cases are selected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.tasksAndTestCases });
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(logError).not.toHaveBeenCalled();
+ expect(createFlash).not.toHaveBeenCalled();
+ });
+
+ it('does not create flashes or logs errors when issues are moved without errors', async () => {
+ createComponent(
+ { selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases },
+ resolvedMutationWithoutErrorsMock,
+ );
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(logError).not.toHaveBeenCalled();
+ expect(createFlash).not.toHaveBeenCalled();
+ });
+
+ it('creates a flash and logs errors when a mutation returns errors', async () => {
+ createComponent(
+ { selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases },
+ resolvedMutationWithErrorsMock,
+ );
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ // We're mocking two issues so it will log two errors
+ expect(logError).toHaveBeenCalledTimes(2);
+ expect(logError).toHaveBeenNthCalledWith(
+ 1,
+ `Error moving issue. Error message: ${mockMutationErrorMessage}`,
+ );
+ expect(logError).toHaveBeenNthCalledWith(
+ 2,
+ `Error moving issue. Error message: ${mockMutationErrorMessage}`,
+ );
+
+ // Only one flash is created even if multiple errors are reported
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'There was an error while moving the issues.',
+ });
+ });
+
+ it('creates a flash but not logs errors when a mutation is rejected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases });
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(logError).not.toHaveBeenCalled();
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'There was an error while moving the issues.',
+ });
+ });
+ });
+
+ describe('calls mutations', () => {
+ it('does not call any mutation when no issue is selected', async () => {
+ createComponent({}, resolvedMutationWithoutErrorsMock);
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(resolvedMutationWithoutErrorsMock).not.toHaveBeenCalled();
+ });
+
+ it('does not call any mutation when only tasks are selected', async () => {
+ createComponent(
+ { selectedIssuables: selectedIssuesMocks.tasksOnly },
+ resolvedMutationWithoutErrorsMock,
+ );
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(resolvedMutationWithoutErrorsMock).not.toHaveBeenCalled();
+ });
+
+ it('does not call any mutation when only test cases are selected', async () => {
+ createComponent(
+ { selectedIssuables: selectedIssuesMocks.testCasesOnly },
+ resolvedMutationWithoutErrorsMock,
+ );
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(resolvedMutationWithoutErrorsMock).not.toHaveBeenCalled();
+ });
+
+ it('does not call any mutation when only tasks and test cases are selected', async () => {
+ createComponent(
+ { selectedIssuables: selectedIssuesMocks.tasksAndTestCases },
+ resolvedMutationWithoutErrorsMock,
+ );
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(resolvedMutationWithoutErrorsMock).not.toHaveBeenCalled();
+ });
+
+ it('calls a mutation for every selected issue skipping tasks', async () => {
+ createComponent(
+ { selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases },
+ resolvedMutationWithoutErrorsMock,
+ );
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ // We mock three elements but only two are valid issues since the task is skipped
+ expect(resolvedMutationWithoutErrorsMock).toHaveBeenCalledTimes(2);
+ expect(resolvedMutationWithoutErrorsMock).toHaveBeenNthCalledWith(1, {
+ moveIssueInput: {
+ projectPath: mockDefaultProps.projectFullPath,
+ iid: selectedIssuesMocks.issuesTasksAndTestCases[0].iid.toString(),
+ targetProjectPath: mockDestinationProject.full_path,
+ },
+ });
+
+ expect(resolvedMutationWithoutErrorsMock).toHaveBeenNthCalledWith(2, {
+ moveIssueInput: {
+ projectPath: mockDefaultProps.projectFullPath,
+ iid: selectedIssuesMocks.issuesTasksAndTestCases[1].iid.toString(),
+ targetProjectPath: mockDestinationProject.full_path,
+ },
+ });
+ });
+ });
+ });
+});