Commit a2dfdbba authored by Phil Hughes's avatar Phil Hughes

Merge branch '233479-add-test-case-move-support' into 'master'

Add frontend support for moving test cases between projects

See merge request gitlab-org/gitlab!46447
parents ea0e537b 3ba714b8
...@@ -118,6 +118,12 @@ module Types ...@@ -118,6 +118,12 @@ module Types
field :severity, Types::IssuableSeverityEnum, null: true, field :severity, Types::IssuableSeverityEnum, null: true,
description: 'Severity level of the incident' description: 'Severity level of the incident'
field :moved, GraphQL::BOOLEAN_TYPE, method: :moved?, null: true,
description: 'Indicates if issue got moved from other project'
field :moved_to, Types::IssueType, null: true,
description: 'Updated Issue after it got moved to another project'
def user_notes_count def user_notes_count
BatchLoader::GraphQL.for(object.id).batch(key: :issue_user_notes_count) do |ids, loader, args| BatchLoader::GraphQL.for(object.id).batch(key: :issue_user_notes_count) do |ids, loader, args|
counts = Note.count_for_collection(ids, 'Issue').index_by(&:noteable_id) counts = Note.count_for_collection(ids, 'Issue').index_by(&:noteable_id)
...@@ -150,6 +156,10 @@ module Types ...@@ -150,6 +156,10 @@ module Types
Gitlab::Graphql::Loaders::BatchModelLoader.new(Milestone, object.milestone_id).find Gitlab::Graphql::Loaders::BatchModelLoader.new(Milestone, object.milestone_id).find
end end
def moved_to
Gitlab::Graphql::Loaders::BatchModelLoader.new(Issue, object.moved_to_id).find
end
def discussion_locked def discussion_locked
!!object.discussion_locked !!object.discussion_locked
end end
......
...@@ -3,4 +3,5 @@ ...@@ -3,4 +3,5 @@
class MoveToProjectEntity < Grape::Entity class MoveToProjectEntity < Grape::Entity
expose :id expose :id
expose :name_with_namespace expose :name_with_namespace
expose :full_path
end end
---
title: Expose moved and movedTo attributes in Issues query
merge_request: 46447
author:
type: added
...@@ -7645,6 +7645,16 @@ type EpicIssue implements CurrentUserTodos & Noteable { ...@@ -7645,6 +7645,16 @@ type EpicIssue implements CurrentUserTodos & Noteable {
""" """
milestone: Milestone milestone: Milestone
"""
Indicates if issue got moved from other project
"""
moved: Boolean
"""
Updated Issue after it got moved to another project
"""
movedTo: Issue
""" """
All notes on this noteable All notes on this noteable
""" """
...@@ -10169,6 +10179,16 @@ type Issue implements CurrentUserTodos & Noteable { ...@@ -10169,6 +10179,16 @@ type Issue implements CurrentUserTodos & Noteable {
""" """
milestone: Milestone milestone: Milestone
"""
Indicates if issue got moved from other project
"""
moved: Boolean
"""
Updated Issue after it got moved to another project
"""
movedTo: Issue
""" """
All notes on this noteable All notes on this noteable
""" """
......
...@@ -21168,6 +21168,34 @@ ...@@ -21168,6 +21168,34 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "moved",
"description": "Indicates if issue got moved from other project",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "movedTo",
"description": "Updated Issue after it got moved to another project",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Issue",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "notes", "name": "notes",
"description": "All notes on this noteable", "description": "All notes on this noteable",
...@@ -27821,6 +27849,34 @@ ...@@ -27821,6 +27849,34 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "moved",
"description": "Indicates if issue got moved from other project",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "movedTo",
"description": "Updated Issue after it got moved to another project",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Issue",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "notes", "name": "notes",
"description": "All notes on this noteable", "description": "All notes on this noteable",
...@@ -1268,6 +1268,8 @@ Relationship between an epic and an issue. ...@@ -1268,6 +1268,8 @@ Relationship between an epic and an issue.
| `iteration` | Iteration | Iteration of the issue | | `iteration` | Iteration | Iteration of the issue |
| `labels` | LabelConnection | Labels of the issue | | `labels` | LabelConnection | Labels of the issue |
| `milestone` | Milestone | Milestone of the issue | | `milestone` | Milestone | Milestone of the issue |
| `moved` | Boolean | Indicates if issue got moved from other project |
| `movedTo` | Issue | Updated Issue after it got moved to another project |
| `notes` | NoteConnection! | All notes on this noteable | | `notes` | NoteConnection! | All notes on this noteable |
| `participants` | UserConnection | List of participants in the issue | | `participants` | UserConnection | List of participants in the issue |
| `reference` | String! | Internal reference of the issue. Returned in shortened format by default | | `reference` | String! | Internal reference of the issue. Returned in shortened format by default |
...@@ -1534,6 +1536,8 @@ Represents a recorded measurement (object count) for the Admins. ...@@ -1534,6 +1536,8 @@ Represents a recorded measurement (object count) for the Admins.
| `iteration` | Iteration | Iteration of the issue | | `iteration` | Iteration | Iteration of the issue |
| `labels` | LabelConnection | Labels of the issue | | `labels` | LabelConnection | Labels of the issue |
| `milestone` | Milestone | Milestone of the issue | | `milestone` | Milestone | Milestone of the issue |
| `moved` | Boolean | Indicates if issue got moved from other project |
| `movedTo` | Issue | Updated Issue after it got moved to another project |
| `notes` | NoteConnection! | All notes on this noteable | | `notes` | NoteConnection! | All notes on this noteable |
| `participants` | UserConnection | List of participants in the issue | | `participants` | UserConnection | List of participants in the issue |
| `reference` | String! | Internal reference of the issue. Returned in shortened format by default | | `reference` | String! | Internal reference of the issue. Returned in shortened format by default |
......
<script> <script>
import { GlLoadingIcon, GlDropdown, GlDropdownDivider, GlDropdownItem, GlButton } from '@gitlab/ui'; import {
GlLoadingIcon,
GlDropdown,
GlDropdownDivider,
GlDropdownItem,
GlButton,
GlSprintf,
GlLink,
} from '@gitlab/ui';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
...@@ -21,6 +29,8 @@ export default { ...@@ -21,6 +29,8 @@ export default {
GlDropdownDivider, GlDropdownDivider,
GlDropdownItem, GlDropdownItem,
GlButton, GlButton,
GlSprintf,
GlLink,
IssuableShow, IssuableShow,
TestCaseSidebar, TestCaseSidebar,
}, },
...@@ -136,7 +146,17 @@ export default { ...@@ -136,7 +146,17 @@ export default {
@edit-issuable="handleEditTestCase" @edit-issuable="handleEditTestCase"
> >
<template #status-badge> <template #status-badge>
{{ statusBadgeText }} <gl-sprintf
v-if="testCase.moved"
:message="__('Archived (%{movedToStart}moved%{movedToEnd})')"
>
<template #movedTo="{ content }">
<gl-link :href="testCase.movedTo.webUrl" class="text-white text-underline">{{
content
}}</gl-link>
</template>
</gl-sprintf>
<span v-else>{{ statusBadgeText }}</span>
</template> </template>
<template #header-actions> <template #header-actions>
<gl-dropdown <gl-dropdown
...@@ -194,6 +214,7 @@ export default { ...@@ -194,6 +214,7 @@ export default {
:sidebar-expanded="sidebarExpanded" :sidebar-expanded="sidebarExpanded"
:selected-labels="selectedLabels" :selected-labels="selectedLabels"
:todo="todo" :todo="todo"
:moved="testCase.moved"
@test-case-updated="handleTestCaseUpdated" @test-case-updated="handleTestCaseUpdated"
/> />
</template> </template>
......
...@@ -4,6 +4,7 @@ import Mousetrap from 'mousetrap'; ...@@ -4,6 +4,7 @@ import Mousetrap from 'mousetrap';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue'; import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
import ProjectSelect from '~/vue_shared/components/sidebar/issuable_move_dropdown.vue';
import TestCaseGraphQL from '../mixins/test_case_graphql'; import TestCaseGraphQL from '../mixins/test_case_graphql';
...@@ -13,6 +14,7 @@ export default { ...@@ -13,6 +14,7 @@ export default {
GlIcon, GlIcon,
GlLoadingIcon, GlLoadingIcon,
LabelsSelect, LabelsSelect,
ProjectSelect,
}, },
directives: { directives: {
GlTooltip, GlTooltip,
...@@ -21,8 +23,10 @@ export default { ...@@ -21,8 +23,10 @@ export default {
'projectFullPath', 'projectFullPath',
'testCaseId', 'testCaseId',
'canEditTestCase', 'canEditTestCase',
'canMoveTestCase',
'labelsFetchPath', 'labelsFetchPath',
'labelsManagePath', 'labelsManagePath',
'projectsFetchPath',
], ],
mixins: [TestCaseGraphQL], mixins: [TestCaseGraphQL],
props: { props: {
...@@ -39,6 +43,11 @@ export default { ...@@ -39,6 +43,11 @@ export default {
type: Array, type: Array,
required: true, required: true,
}, },
moved: {
type: Boolean,
required: false,
default: false,
},
}, },
data() { data() {
return { return {
...@@ -59,8 +68,14 @@ export default { ...@@ -59,8 +68,14 @@ export default {
todoIcon() { todoIcon() {
return this.isTodoPending ? 'todo-done' : 'todo-add'; return this.isTodoPending ? 'todo-done' : 'todo-add';
}, },
selectProjectDropdownButtonTitle() {
return this.testCaseMoveInProgress
? s__('TestCases|Moving test case')
: s__('TestCases|Move test case');
},
}, },
mounted() { mounted() {
this.sidebarEl = document.querySelector('aside.right-sidebar');
Mousetrap.bind('l', this.handleLabelsCollapsedButtonClick); Mousetrap.bind('l', this.handleLabelsCollapsedButtonClick);
}, },
beforeDestroy() { beforeDestroy() {
...@@ -77,27 +92,39 @@ export default { ...@@ -77,27 +92,39 @@ export default {
toggleSidebar() { toggleSidebar() {
document.querySelector('.js-toggle-right-sidebar-button').dispatchEvent(new Event('click')); document.querySelector('.js-toggle-right-sidebar-button').dispatchEvent(new Event('click'));
}, },
handleLabelsDropdownClose() { expandSidebarAndOpenDropdown(dropdownButtonSelector) {
if (this.sidebarExpandedOnClick) {
this.sidebarExpandedOnClick = false;
this.toggleSidebar();
}
},
handleLabelsCollapsedButtonClick() {
// Expand the sidebar if not already expanded. // Expand the sidebar if not already expanded.
if (!this.sidebarExpanded) { if (!this.sidebarExpanded) {
this.toggleSidebar(); this.toggleSidebar();
this.sidebarExpandedOnClick = true; this.sidebarExpandedOnClick = true;
} }
// Wait for sidebar expand to complete before
// revealing labels dropdown.
this.$nextTick(() => { this.$nextTick(() => {
document // Wait for sidebar expand animation to complete
.querySelector('.js-labels-block .js-sidebar-dropdown-toggle') // before revealing the dropdown.
.dispatchEvent(new Event('click', { bubbles: true, cancelable: false })); this.sidebarEl.addEventListener(
'transitionend',
() => {
document
.querySelector(dropdownButtonSelector)
.dispatchEvent(new Event('click', { bubbles: true, cancelable: false }));
},
{ once: true },
);
}); });
}, },
handleSidebarDropdownClose() {
if (this.sidebarExpandedOnClick) {
this.sidebarExpandedOnClick = false;
this.toggleSidebar();
}
},
handleLabelsCollapsedButtonClick() {
this.expandSidebarAndOpenDropdown('.js-labels-block .js-sidebar-dropdown-toggle');
},
handleProjectsCollapsedButtonClick() {
this.expandSidebarAndOpenDropdown('.js-issuable-move-block .js-sidebar-dropdown-toggle');
},
handleUpdateSelectedLabels(labels) { handleUpdateSelectedLabels(labels) {
// Iterate over selection and check if labels which were // Iterate over selection and check if labels which were
// either selected or removed aren't leading to same selection // either selected or removed aren't leading to same selection
...@@ -170,9 +197,19 @@ export default { ...@@ -170,9 +197,19 @@ export default {
variant="sidebar" variant="sidebar"
class="block labels js-labels-block" class="block labels js-labels-block"
@updateSelectedLabels="handleUpdateSelectedLabels" @updateSelectedLabels="handleUpdateSelectedLabels"
@onDropdownClose="handleLabelsDropdownClose" @onDropdownClose="handleSidebarDropdownClose"
@toggleCollapse="handleLabelsCollapsedButtonClick" @toggleCollapse="handleLabelsCollapsedButtonClick"
>{{ __('None') }}</labels-select >{{ __('None') }}</labels-select
> >
<project-select
v-if="canMoveTestCase && !moved"
:projects-fetch-path="projectsFetchPath"
:dropdown-button-title="selectProjectDropdownButtonTitle"
:dropdown-header-title="__('Move test case')"
:move-in-progress="testCaseMoveInProgress"
@dropdown-close="handleSidebarDropdownClose"
@toggle-collapse="handleProjectsCollapsedButtonClick"
@move-issuable="moveTestCase"
/>
</div> </div>
</template> </template>
import Api from '~/api'; import Api from '~/api';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility';
import projectTestCase from '../queries/project_test_case.query.graphql'; import projectTestCase from '../queries/project_test_case.query.graphql';
import updateTestCase from '../queries/update_test_case.mutation.graphql'; import updateTestCase from '../queries/update_test_case.mutation.graphql';
import markTestCaseTodoDone from '../queries/mark_test_case_todo_done.mutation.graphql'; import markTestCaseTodoDone from '../queries/mark_test_case_todo_done.mutation.graphql';
import moveTestCase from '../queries/move_test_case.mutation.graphql';
export default { export default {
apollo: { apollo: {
...@@ -38,6 +40,7 @@ export default { ...@@ -38,6 +40,7 @@ export default {
testCaseLoading: true, testCaseLoading: true,
testCaseLoadFailed: false, testCaseLoadFailed: false,
testCaseTodoUpdateInProgress: false, testCaseTodoUpdateInProgress: false,
testCaseMoveInProgress: false,
}; };
}, },
methods: { methods: {
...@@ -118,5 +121,36 @@ export default { ...@@ -118,5 +121,36 @@ export default {
this.testCaseTodoUpdateInProgress = false; this.testCaseTodoUpdateInProgress = false;
}); });
}, },
moveTestCase(targetProject) {
this.testCaseMoveInProgress = true;
return this.$apollo
.mutate({
mutation: moveTestCase,
variables: {
moveTestCaseInput: {
projectPath: this.projectFullPath,
iid: this.testCaseId,
targetProjectPath: targetProject.full_path,
},
},
})
.then(({ data = {} }) => {
if (!data.issueMove) return;
const { errors } = data.issueMove;
if (errors?.length) {
throw new Error(`Error moving test case. Error message: ${errors[0].message}`);
}
visitUrl(data.issueMove?.issue.webUrl);
})
.catch(error => {
this.testCaseMoveInProgress = false;
createFlash({
message: s__('TestCases|Something went wrong while moving test case.'),
captureError: true,
error,
});
});
},
}, },
}; };
mutation moveTestCase($moveTestCaseInput: IssueMoveInput!) {
issueMove(input: $moveTestCaseInput) {
errors
clientMutationId
issue {
webUrl
}
}
}
...@@ -16,6 +16,10 @@ fragment TestCase on Issue { ...@@ -16,6 +16,10 @@ fragment TestCase on Issue {
webUrl webUrl
blocked blocked
confidential confidential
moved
movedTo {
webUrl
}
author { author {
...Author ...Author
} }
......
...@@ -19,11 +19,14 @@ export default function initTestCaseShow({ mountPointSelector }) { ...@@ -19,11 +19,14 @@ export default function initTestCaseShow({ mountPointSelector }) {
defaultClient: createDefaultClient(), defaultClient: createDefaultClient(),
}); });
const sidebarOptions = JSON.parse(el.dataset.sidebarOptions);
return new Vue({ return new Vue({
el, el,
apolloProvider, apolloProvider,
provide: { provide: {
...el.dataset, ...el.dataset,
projectsFetchPath: sidebarOptions.projectsAutocompleteEndpoint,
canEditTestCase: parseBoolean(el.dataset.canEditTestCase), canEditTestCase: parseBoolean(el.dataset.canEditTestCase),
}, },
render: createElement => createElement(TestCaseShowApp), render: createElement => createElement(TestCaseShowApp),
......
...@@ -7,10 +7,12 @@ ...@@ -7,10 +7,12 @@
#js-issuable-app{ data: { initial: issuable_initial_data(@issue).to_json, #js-issuable-app{ data: { initial: issuable_initial_data(@issue).to_json,
can_edit_test_case: can?(current_user, :admin_issue, @project).to_s, can_edit_test_case: can?(current_user, :admin_issue, @project).to_s,
can_move_test_case: @issuable_sidebar.dig(:current_user, :can_move).to_s,
description_preview_path: preview_markdown_path(@project), description_preview_path: preview_markdown_path(@project),
description_help_path: help_page_path('user/markdown'), description_help_path: help_page_path('user/markdown'),
project_full_path: @project.full_path, project_full_path: @project.full_path,
labels_manage_path: project_labels_path(@project), labels_manage_path: project_labels_path(@project),
labels_fetch_path: project_labels_path(@project, format: :json), labels_fetch_path: project_labels_path(@project, format: :json),
test_case_new_path: new_project_quality_test_case_path(@project), test_case_new_path: new_project_quality_test_case_path(@project),
sidebar_options: issuable_sidebar_options(@issuable_sidebar).to_json.html_safe,
test_case_id: @issue.iid } } test_case_id: @issue.iid } }
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLink, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import { mockCurrentUserTodo } from 'jest/issuable_list/mock_data'; import { mockCurrentUserTodo } from 'jest/issuable_list/mock_data';
...@@ -33,6 +33,7 @@ const createComponent = ({ testCase, testCaseQueryLoading = false } = {}) => ...@@ -33,6 +33,7 @@ const createComponent = ({ testCase, testCaseQueryLoading = false } = {}) =>
}, },
}, },
stubs: { stubs: {
GlSprintf,
IssuableShow, IssuableShow,
IssuableHeader, IssuableHeader,
IssuableBody, IssuableBody,
...@@ -301,6 +302,27 @@ describe('TestCaseShowRoot', () => { ...@@ -301,6 +302,27 @@ describe('TestCaseShowRoot', () => {
expect(wrapper.find('[data-testid="status"]').text()).toContain('Open'); expect(wrapper.find('[data-testid="status"]').text()).toContain('Open');
}); });
it('renders status-badge slot contents with updated test case URL when testCase.moved is true', () => {
const movedTestCase = {
...mockTestCase,
status: 'closed',
moved: true,
movedTo: {
webUrl: 'http://0.0.0.0:3000/gitlab-org/gitlab-test/-/issues/30',
},
};
const wrapperMoved = createComponent({
testCase: movedTestCase,
});
const statusEl = wrapperMoved.find('[data-testid="status"]');
expect(statusEl.text()).toContain('Archived');
expect(statusEl.find(GlLink).attributes('href')).toBe(movedTestCase.movedTo.webUrl);
wrapperMoved.destroy();
});
it('renders header-actions slot contents', () => { it('renders header-actions slot contents', () => {
expect(wrapper.find('[data-testid="actions-dropdown"]').exists()).toBe(true); expect(wrapper.find('[data-testid="actions-dropdown"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="archive-test-case"]').exists()).toBe(true); expect(wrapper.find('[data-testid="archive-test-case"]').exists()).toBe(true);
......
...@@ -7,6 +7,7 @@ import { mockCurrentUserTodo, mockLabels } from 'jest/issuable_list/mock_data'; ...@@ -7,6 +7,7 @@ import { mockCurrentUserTodo, mockLabels } from 'jest/issuable_list/mock_data';
import TestCaseSidebar from 'ee/test_case_show/components/test_case_sidebar.vue'; import TestCaseSidebar from 'ee/test_case_show/components/test_case_sidebar.vue';
import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue'; import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
import ProjectSelect from '~/vue_shared/components/sidebar/issuable_move_dropdown.vue';
import { mockProvide, mockTestCase } from '../mock_data'; import { mockProvide, mockTestCase } from '../mock_data';
...@@ -41,6 +42,7 @@ describe('TestCaseSidebar', () => { ...@@ -41,6 +42,7 @@ describe('TestCaseSidebar', () => {
let wrapper; let wrapper;
beforeEach(() => { beforeEach(() => {
setFixtures('<aside class="right-sidebar"></aside>');
mousetrapSpy = jest.spyOn(Mousetrap, 'bind'); mousetrapSpy = jest.spyOn(Mousetrap, 'bind');
wrapper = createComponent(); wrapper = createComponent();
}); });
...@@ -75,6 +77,25 @@ describe('TestCaseSidebar', () => { ...@@ -75,6 +77,25 @@ describe('TestCaseSidebar', () => {
expect(wrapper.vm[propName]).toBe(propValue); expect(wrapper.vm[propName]).toBe(propValue);
}); });
}); });
describe('selectProjectDropdownButtonTitle', () => {
it.each`
testCaseMoveInProgress | returnValue
${true} | ${'Moving test case'}
${false} | ${'Move test case'}
`(
'returns $returnValue when testCaseMoveInProgress is $testCaseMoveInProgress',
async ({ testCaseMoveInProgress, returnValue }) => {
wrapper.setData({
testCaseMoveInProgress,
});
await wrapper.vm.$nextTick();
expect(wrapper.vm.selectProjectDropdownButtonTitle).toBe(returnValue);
},
);
});
}); });
describe('mounted', () => { describe('mounted', () => {
...@@ -129,23 +150,7 @@ describe('TestCaseSidebar', () => { ...@@ -129,23 +150,7 @@ describe('TestCaseSidebar', () => {
}); });
}); });
describe('handleLabelsDropdownClose', () => { describe('expandSidebarAndOpenDropdown', () => {
it('sets `sidebarExpandedOnClick` to false and calls `toggleSidebar` method when `sidebarExpandedOnClick` is true', async () => {
jest.spyOn(wrapper.vm, 'toggleSidebar').mockImplementation(jest.fn());
wrapper.setData({
sidebarExpandedOnClick: true,
});
await wrapper.vm.$nextTick();
wrapper.vm.handleLabelsDropdownClose();
expect(wrapper.vm.sidebarExpandedOnClick).toBe(false);
expect(wrapper.vm.toggleSidebar).toHaveBeenCalled();
});
});
describe('handleLabelsCollapsedButtonClick', () => {
beforeEach(() => { beforeEach(() => {
setFixtures(` setFixtures(`
<div class="js-labels-block"> <div class="js-labels-block">
...@@ -162,7 +167,7 @@ describe('TestCaseSidebar', () => { ...@@ -162,7 +167,7 @@ describe('TestCaseSidebar', () => {
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
wrapper.vm.handleLabelsCollapsedButtonClick(); wrapper.vm.expandSidebarAndOpenDropdown('.js-labels-block .js-sidebar-dropdown-toggle');
expect(wrapper.vm.toggleSidebar).toHaveBeenCalled(); expect(wrapper.vm.toggleSidebar).toHaveBeenCalled();
expect(wrapper.vm.sidebarExpandedOnClick).toBe(true); expect(wrapper.vm.sidebarExpandedOnClick).toBe(true);
...@@ -178,10 +183,12 @@ describe('TestCaseSidebar', () => { ...@@ -178,10 +183,12 @@ describe('TestCaseSidebar', () => {
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
wrapper.vm.handleLabelsCollapsedButtonClick(); wrapper.vm.expandSidebarAndOpenDropdown('.js-labels-block .js-sidebar-dropdown-toggle');
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
wrapper.vm.sidebarEl.dispatchEvent(new Event('transitionend'));
expect(buttonEl.dispatchEvent).toHaveBeenCalledWith( expect(buttonEl.dispatchEvent).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
type: 'click', type: 'click',
...@@ -192,6 +199,22 @@ describe('TestCaseSidebar', () => { ...@@ -192,6 +199,22 @@ describe('TestCaseSidebar', () => {
}); });
}); });
describe('handleSidebarDropdownClose', () => {
it('sets `sidebarExpandedOnClick` to false and calls `toggleSidebar` method when `sidebarExpandedOnClick` is true', async () => {
jest.spyOn(wrapper.vm, 'toggleSidebar').mockImplementation(jest.fn());
wrapper.setData({
sidebarExpandedOnClick: true,
});
await wrapper.vm.$nextTick();
wrapper.vm.handleSidebarDropdownClose();
expect(wrapper.vm.sidebarExpandedOnClick).toBe(false);
expect(wrapper.vm.toggleSidebar).toHaveBeenCalled();
});
});
describe('handleUpdateSelectedLabels', () => { describe('handleUpdateSelectedLabels', () => {
const updatedLabels = [ const updatedLabels = [
{ {
...@@ -281,5 +304,19 @@ describe('TestCaseSidebar', () => { ...@@ -281,5 +304,19 @@ describe('TestCaseSidebar', () => {
}); });
expect(labelSelectEl.text()).toBe('None'); expect(labelSelectEl.text()).toBe('None');
}); });
it('renders project-select', async () => {
const { selectProjectDropdownButtonTitle, testCaseMoveInProgress } = wrapper.vm;
const { projectsFetchPath } = mockProvide;
const projectSelectEl = wrapper.find(ProjectSelect);
expect(projectSelectEl.exists()).toBe(true);
expect(projectSelectEl.props()).toMatchObject({
projectsFetchPath,
dropdownButtonTitle: selectProjectDropdownButtonTitle,
dropdownHeaderTitle: 'Move test case',
moveInProgress: testCaseMoveInProgress,
});
});
}); });
}); });
...@@ -5,13 +5,16 @@ import { mockCurrentUserTodo } from 'jest/issuable_list/mock_data'; ...@@ -5,13 +5,16 @@ import { mockCurrentUserTodo } from 'jest/issuable_list/mock_data';
import TestCaseShowRoot from 'ee/test_case_show/components/test_case_show_root.vue'; import TestCaseShowRoot from 'ee/test_case_show/components/test_case_show_root.vue';
import updateTestCase from 'ee/test_case_show/queries/update_test_case.mutation.graphql'; import updateTestCase from 'ee/test_case_show/queries/update_test_case.mutation.graphql';
import markTestCaseTodoDone from 'ee/test_case_show/queries/mark_test_case_todo_done.mutation.graphql'; import markTestCaseTodoDone from 'ee/test_case_show/queries/mark_test_case_todo_done.mutation.graphql';
import moveTestCase from 'ee/test_case_show/queries/move_test_case.mutation.graphql';
import createFlash from '~/flash'; import createFlash from '~/flash';
import Api from '~/api'; import Api from '~/api';
import { visitUrl } from '~/lib/utils/url_utility';
import { mockProvide, mockTestCase } from '../mock_data'; import { mockProvide, mockTestCase } from '../mock_data';
jest.mock('~/flash'); jest.mock('~/flash');
jest.mock('~/lib/utils/url_utility');
const createComponent = ({ testCase, testCaseQueryLoading = false } = {}) => const createComponent = ({ testCase, testCaseQueryLoading = false } = {}) =>
shallowMount(TestCaseShowRoot, { shallowMount(TestCaseShowRoot, {
...@@ -214,4 +217,67 @@ describe('TestCaseGraphQL Mixin', () => { ...@@ -214,4 +217,67 @@ describe('TestCaseGraphQL Mixin', () => {
}); });
}); });
}); });
describe('moveTestCase', () => {
const mockTargetProject = {
full_path: 'gitlab-org/gitlab-shell',
};
const moveResolvedMutation = {
data: {
issueMove: {
errors: [],
issue: {
webUrl: mockTestCase.webUrl,
},
},
},
};
it('sets `testCaseMoveInProgress` to true', () => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(moveResolvedMutation);
wrapper.vm.moveTestCase(mockTargetProject);
expect(wrapper.vm.testCaseMoveInProgress).toBe(true);
});
it('calls `$apollo.mutate` with moveTestCase mutation and moveTestCaseInput variables', () => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(moveResolvedMutation);
wrapper.vm.moveTestCase(mockTargetProject);
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: moveTestCase,
variables: {
moveTestCaseInput: {
projectPath: mockProvide.projectFullPath,
iid: mockProvide.testCaseId,
targetProjectPath: mockTargetProject.full_path,
},
},
});
});
it('calls `visitUrl` with updated test case URL on mutation promise resolve', async () => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(moveResolvedMutation);
await wrapper.vm.moveTestCase(mockTargetProject);
expect(wrapper.vm.testCaseMoveInProgress).toBe(true);
expect(visitUrl).toHaveBeenCalledWith(moveResolvedMutation.data.issueMove.issue.webUrl);
});
it('calls `createFlash` with errorMessage on mutation promise reject', async () => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue({});
await wrapper.vm.moveTestCase(mockTargetProject);
expect(createFlash).toHaveBeenCalledWith({
message: 'Something went wrong while moving test case.',
captureError: true,
error: expect.any(Object),
});
expect(wrapper.vm.testCaseMoveInProgress).toBe(false);
});
});
}); });
...@@ -2,6 +2,7 @@ import { mockIssuable, mockCurrentUserTodo } from 'jest/issuable_list/mock_data' ...@@ -2,6 +2,7 @@ import { mockIssuable, mockCurrentUserTodo } from 'jest/issuable_list/mock_data'
export const mockTestCase = { export const mockTestCase = {
...mockIssuable, ...mockIssuable,
moved: false,
currentUserTodos: { currentUserTodos: {
nodes: [mockCurrentUserTodo], nodes: [mockCurrentUserTodo],
}, },
...@@ -12,8 +13,10 @@ export const mockProvide = { ...@@ -12,8 +13,10 @@ export const mockProvide = {
testCaseNewPath: '/gitlab-org/gitlab-test/-/quality/test_cases/new', testCaseNewPath: '/gitlab-org/gitlab-test/-/quality/test_cases/new',
testCaseId: mockIssuable.iid, testCaseId: mockIssuable.iid,
canEditTestCase: true, canEditTestCase: true,
canMoveTestCase: true,
descriptionPreviewPath: '/gitlab-org/gitlab-test/preview_markdown', descriptionPreviewPath: '/gitlab-org/gitlab-test/preview_markdown',
descriptionHelpPath: '/help/user/markdown', descriptionHelpPath: '/help/user/markdown',
labelsFetchPath: '/gitlab-org/gitlab-test/-/labels.json', labelsFetchPath: '/gitlab-org/gitlab-test/-/labels.json',
labelsManagePath: '/gitlab-org/gitlab-shell/-/labels', labelsManagePath: '/gitlab-org/gitlab-shell/-/labels',
projectsFetchPath: '/-/autocomplete/projects?project_id=1',
}; };
...@@ -3546,6 +3546,9 @@ msgstr "" ...@@ -3546,6 +3546,9 @@ msgstr ""
msgid "Archived" msgid "Archived"
msgstr "" msgstr ""
msgid "Archived (%{movedToStart}moved%{movedToEnd})"
msgstr ""
msgid "Archived in this version" msgid "Archived in this version"
msgstr "" msgstr ""
...@@ -17691,6 +17694,9 @@ msgstr "" ...@@ -17691,6 +17694,9 @@ msgstr ""
msgid "Move selection up" msgid "Move selection up"
msgstr "" msgstr ""
msgid "Move test case"
msgstr ""
msgid "Move this issue to another project." msgid "Move this issue to another project."
msgstr "" msgstr ""
...@@ -26495,6 +26501,12 @@ msgstr[1] "" ...@@ -26495,6 +26501,12 @@ msgstr[1] ""
msgid "Test settings" msgid "Test settings"
msgstr "" msgstr ""
msgid "TestCases|Move test case"
msgstr ""
msgid "TestCases|Moving test case"
msgstr ""
msgid "TestCases|New Test Case" msgid "TestCases|New Test Case"
msgstr "" msgstr ""
...@@ -26522,6 +26534,9 @@ msgstr "" ...@@ -26522,6 +26534,9 @@ msgstr ""
msgid "TestCases|Something went wrong while marking test case todo as done." msgid "TestCases|Something went wrong while marking test case todo as done."
msgstr "" msgstr ""
msgid "TestCases|Something went wrong while moving test case."
msgstr ""
msgid "TestCases|Something went wrong while updating the test case labels." msgid "TestCases|Something went wrong while updating the test case labels."
msgstr "" msgstr ""
......
...@@ -17,7 +17,7 @@ RSpec.describe GitlabSchema.types['Issue'] do ...@@ -17,7 +17,7 @@ RSpec.describe GitlabSchema.types['Issue'] do
fields = %i[id iid title description state reference author assignees updated_by participants labels milestone due_date fields = %i[id iid title description state reference author assignees updated_by participants labels milestone due_date
confidential discussion_locked upvotes downvotes user_notes_count user_discussions_count web_path web_url relative_position confidential discussion_locked upvotes downvotes user_notes_count user_discussions_count web_path web_url relative_position
emails_disabled subscribed time_estimate total_time_spent human_time_estimate human_total_time_spent closed_at created_at updated_at task_completion_status emails_disabled subscribed time_estimate total_time_spent human_time_estimate human_total_time_spent closed_at created_at updated_at task_completion_status
designs design_collection alert_management_alert severity current_user_todos] designs design_collection alert_management_alert severity current_user_todos moved moved_to]
fields.each do |field_name| fields.each do |field_name|
expect(described_class).to have_graphql_field(field_name) expect(described_class).to have_graphql_field(field_name)
......
...@@ -83,6 +83,25 @@ RSpec.describe 'Query.issue(id)' do ...@@ -83,6 +83,25 @@ RSpec.describe 'Query.issue(id)' do
end end
end end
context 'when issue got moved' do
let_it_be(:issue_fields) { ['moved', 'movedTo { title }'] }
let_it_be(:new_issue) { create(:issue) }
let_it_be(:issue) { create(:issue, project: project, moved_to: new_issue) }
let_it_be(:issue_params) { { 'id' => issue.to_global_id.to_s } }
before_all do
new_issue.project.add_developer(current_user)
end
it 'returns correct attributes' do
post_graphql(query, current_user: current_user)
expect(issue_data.keys).to eq( %w(moved movedTo) )
expect(issue_data['moved']).to eq(true)
expect(issue_data['movedTo']['title']).to eq(new_issue.title)
end
end
context 'when passed a non-Issue gid' do context 'when passed a non-Issue gid' do
let(:mr) {create(:merge_request)} let(:mr) {create(:merge_request)}
......
...@@ -12,8 +12,12 @@ RSpec.describe MoveToProjectEntity do ...@@ -12,8 +12,12 @@ RSpec.describe MoveToProjectEntity do
expect(subject[:id]).to eq(project.id) expect(subject[:id]).to eq(project.id)
end end
it 'includes the full path' do it 'includes the human-readable full path' do
expect(subject[:name_with_namespace]).to eq(project.name_with_namespace) expect(subject[:name_with_namespace]).to eq(project.name_with_namespace)
end end
it 'includes the full path' do
expect(subject[:full_path]).to eq(project.full_path)
end
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment