Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
da977f43
Commit
da977f43
authored
Oct 19, 2020
by
Phil Hughes
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch '233479-add-test-case-show' into 'master'
Add Test Case show See merge request gitlab-org/gitlab!43522
parents
fa79e06f
cbc5129a
Changes
21
Hide whitespace changes
Inline
Side-by-side
Showing
21 changed files
with
1719 additions
and
13 deletions
+1719
-13
app/assets/javascripts/api.js
app/assets/javascripts/api.js
+9
-0
app/assets/javascripts/issuable_show/constants.js
app/assets/javascripts/issuable_show/constants.js
+5
-0
app/assets/javascripts/pages/projects/issues/show.js
app/assets/javascripts/pages/projects/issues/show.js
+19
-10
ee/app/assets/javascripts/pages/projects/issues/show/index.js
...pp/assets/javascripts/pages/projects/issues/show/index.js
+14
-0
ee/app/assets/javascripts/test_case_show/components/test_case_show_root.vue
...scripts/test_case_show/components/test_case_show_root.vue
+202
-0
ee/app/assets/javascripts/test_case_show/components/test_case_sidebar.vue
...vascripts/test_case_show/components/test_case_sidebar.vue
+178
-0
ee/app/assets/javascripts/test_case_show/mixins/test_case_graphql.js
...ts/javascripts/test_case_show/mixins/test_case_graphql.js
+122
-0
ee/app/assets/javascripts/test_case_show/queries/mark_test_case_todo_done.mutation.graphql
...se_show/queries/mark_test_case_todo_done.mutation.graphql
+9
-0
ee/app/assets/javascripts/test_case_show/queries/project_test_case.query.graphql
...ts/test_case_show/queries/project_test_case.query.graphql
+10
-0
ee/app/assets/javascripts/test_case_show/queries/test_case.fragment.graphql
...scripts/test_case_show/queries/test_case.fragment.graphql
+30
-0
ee/app/assets/javascripts/test_case_show/queries/update_test_case.mutation.graphql
.../test_case_show/queries/update_test_case.mutation.graphql
+11
-0
ee/app/assets/javascripts/test_case_show/test_case_show_bundle.js
...ssets/javascripts/test_case_show/test_case_show_bundle.js
+31
-0
ee/app/views/projects/quality/test_cases/_show.html.haml
ee/app/views/projects/quality/test_cases/_show.html.haml
+9
-2
ee/spec/features/projects/quality/test_case_show_spec.rb
ee/spec/features/projects/quality/test_case_show_spec.rb
+173
-0
ee/spec/frontend/test_case_show/components/test_case_show_root_spec.js
...end/test_case_show/components/test_case_show_root_spec.js
+325
-0
ee/spec/frontend/test_case_show/components/test_case_sidebar_spec.js
...ntend/test_case_show/components/test_case_sidebar_spec.js
+285
-0
ee/spec/frontend/test_case_show/mixins/test_case_graphql_spec.js
.../frontend/test_case_show/mixins/test_case_graphql_spec.js
+217
-0
ee/spec/frontend/test_case_show/mock_data.js
ee/spec/frontend/test_case_show/mock_data.js
+19
-0
locale/gitlab.pot
locale/gitlab.pot
+21
-0
spec/frontend/api_spec.js
spec/frontend/api_spec.js
+19
-0
spec/frontend/issuable_list/mock_data.js
spec/frontend/issuable_list/mock_data.js
+11
-1
No files found.
app/assets/javascripts/api.js
View file @
da977f43
...
...
@@ -30,6 +30,7 @@ const Api = {
projectProtectedBranchesPath
:
'
/api/:version/projects/:id/protected_branches
'
,
projectSearchPath
:
'
/api/:version/projects/:id/search
'
,
projectMilestonesPath
:
'
/api/:version/projects/:id/milestones
'
,
projectIssuePath
:
'
/api/:version/projects/:id/issues/:issue_iid
'
,
mergeRequestsPath
:
'
/api/:version/merge_requests
'
,
groupLabelsPath
:
'
/groups/:namespace_path/-/labels
'
,
issuableTemplatePath
:
'
/:namespace_path/:project_path/templates/:type/:key
'
,
...
...
@@ -328,6 +329,14 @@ const Api = {
});
},
addProjectIssueAsTodo
(
projectId
,
issueIid
)
{
const
url
=
Api
.
buildUrl
(
Api
.
projectIssuePath
)
.
replace
(
'
:id
'
,
encodeURIComponent
(
projectId
))
.
replace
(
'
:issue_iid
'
,
encodeURIComponent
(
issueIid
));
return
axios
.
post
(
`
${
url
}
/todo`
);
},
mergeRequests
(
params
=
{})
{
const
url
=
Api
.
buildUrl
(
Api
.
mergeRequestsPath
);
...
...
app/assets/javascripts/issuable_show/constants.js
0 → 100644
View file @
da977f43
export
const
IssuableType
=
{
Issue
:
'
issue
'
,
Incident
:
'
incident
'
,
TestCase
:
'
test_case
'
,
};
app/assets/javascripts/pages/projects/issues/show.js
View file @
da977f43
...
...
@@ -14,13 +14,20 @@ import { parseIssuableData } from '~/issue_show/utils/parse_data';
import
initInviteMemberTrigger
from
'
~/invite_member/init_invite_member_trigger
'
;
import
initInviteMemberModal
from
'
~/invite_member/init_invite_member_modal
'
;
import
{
IssuableType
}
from
'
~/issuable_show/constants
'
;
export
default
function
()
{
const
{
issueType
,
...
issuableData
}
=
parseIssuableData
();
if
(
issueType
===
'
incident
'
)
{
initIncidentApp
(
issuableData
);
}
else
if
(
issueType
===
'
issue
'
)
{
initIssueApp
(
issuableData
);
switch
(
issueType
)
{
case
IssuableType
.
Incident
:
initIncidentApp
(
issuableData
);
break
;
case
IssuableType
.
Issue
:
initIssueApp
(
issuableData
);
break
;
default
:
break
;
}
initIssuableHeaderWarning
(
store
);
...
...
@@ -31,12 +38,14 @@ export default function() {
.
then
(
module
=>
module
.
default
())
.
catch
(()
=>
{});
new
Issue
();
// eslint-disable-line no-new
new
ShortcutsIssuable
();
// eslint-disable-line no-new
new
ZenMode
();
// eslint-disable-line no-new
initIssuableSidebar
();
loadAwardsHandler
();
initInviteMemberModal
();
initInviteMemberTrigger
();
if
(
issueType
!==
IssuableType
.
TestCase
)
{
new
Issue
();
// eslint-disable-line no-new
new
ShortcutsIssuable
();
// eslint-disable-line no-new
initIssuableSidebar
();
loadAwardsHandler
();
initInviteMemberModal
();
initInviteMemberTrigger
();
}
}
ee/app/assets/javascripts/pages/projects/issues/show/index.js
View file @
da977f43
import
initSidebarBundle
from
'
ee/sidebar/sidebar_bundle
'
;
import
trackShowInviteMemberLink
from
'
ee/projects/track_invite_members
'
;
import
initTestCaseShow
from
'
ee/test_case_show/test_case_show_bundle
'
;
import
{
parseIssuableData
}
from
'
~/issue_show/utils/parse_data
'
;
import
initRelatedIssues
from
'
~/related_issues
'
;
import
initShow
from
'
~/pages/projects/issues/show
'
;
import
UserCallout
from
'
~/user_callout
'
;
import
{
IssuableType
}
from
'
~/issuable_show/constants
'
;
const
{
issueType
}
=
parseIssuableData
();
initShow
();
if
(
issueType
===
IssuableType
.
TestCase
)
{
initTestCaseShow
({
mountPointSelector
:
'
#js-issuable-app
'
,
});
}
if
(
gon
.
features
&&
!
gon
.
features
.
vueIssuableSidebar
)
{
initSidebarBundle
();
}
...
...
ee/app/assets/javascripts/test_case_show/components/test_case_show_root.vue
0 → 100644
View file @
da977f43
<
script
>
import
{
GlLoadingIcon
,
GlDropdown
,
GlDropdownDivider
,
GlDropdownItem
,
GlButton
}
from
'
@gitlab/ui
'
;
import
{
s__
,
__
}
from
'
~/locale
'
;
import
{
getIdFromGraphQLId
}
from
'
~/graphql_shared/utils
'
;
import
IssuableShow
from
'
~/issuable_show/components/issuable_show_root.vue
'
;
import
IssuableEventHub
from
'
~/issuable_show/event_hub
'
;
import
TestCaseSidebar
from
'
./test_case_sidebar.vue
'
;
import
TestCaseGraphQL
from
'
../mixins/test_case_graphql
'
;
const
stateEvent
=
{
Close
:
'
CLOSE
'
,
Reopen
:
'
REOPEN
'
,
};
export
default
{
components
:
{
GlLoadingIcon
,
GlDropdown
,
GlDropdownDivider
,
GlDropdownItem
,
GlButton
,
IssuableShow
,
TestCaseSidebar
,
},
inject
:
[
'
projectFullPath
'
,
'
testCaseNewPath
'
,
'
testCaseId
'
,
'
canEditTestCase
'
,
'
descriptionPreviewPath
'
,
'
descriptionHelpPath
'
,
],
mixins
:
[
TestCaseGraphQL
],
data
()
{
return
{
testCase
:
{},
editTestCaseFormVisible
:
false
,
testCaseSaveInProgress
:
false
,
testCaseStateChangeInProgress
:
false
,
};
},
computed
:
{
isTestCaseOpen
()
{
return
this
.
testCase
.
state
===
'
opened
'
;
},
statusBadgeClass
()
{
return
this
.
isTestCaseOpen
?
'
status-box-open
'
:
'
status-box-issue-closed
'
;
},
statusIcon
()
{
return
this
.
isTestCaseOpen
?
'
issue-open-m
'
:
'
mobile-issue-close
'
;
},
statusBadgeText
()
{
return
this
.
isTestCaseOpen
?
__
(
'
Open
'
)
:
__
(
'
Archived
'
);
},
testCaseActionButtonVariant
()
{
return
this
.
isTestCaseOpen
?
'
warning
'
:
'
default
'
;
},
testCaseActionTitle
()
{
return
this
.
isTestCaseOpen
?
__
(
'
Archive test case
'
)
:
__
(
'
Reopen test case
'
);
},
todo
()
{
const
todos
=
this
.
testCase
.
currentUserTodos
.
nodes
;
return
todos
.
length
?
todos
[
0
]
:
null
;
},
selectedLabels
()
{
return
this
.
testCase
.
labels
.
nodes
.
map
(
label
=>
({
...
label
,
id
:
getIdFromGraphQLId
(
label
.
id
),
}));
},
},
methods
:
{
handleTestCaseStateChange
()
{
this
.
testCaseStateChangeInProgress
=
true
;
return
this
.
updateTestCase
({
variables
:
{
stateEvent
:
this
.
isTestCaseOpen
?
stateEvent
.
Close
:
stateEvent
.
Reopen
,
},
errorMessage
:
s__
(
'
TestCases|Something went wrong while updating the test case.
'
),
})
.
then
(
updatedTestCase
=>
{
this
.
testCase
=
updatedTestCase
;
})
.
finally
(()
=>
{
this
.
testCaseStateChangeInProgress
=
false
;
});
},
handleEditTestCase
()
{
this
.
editTestCaseFormVisible
=
true
;
},
handleSaveTestCase
({
issuableTitle
,
issuableDescription
})
{
this
.
testCaseSaveInProgress
=
true
;
return
this
.
updateTestCase
({
variables
:
{
title
:
issuableTitle
,
description
:
issuableDescription
,
},
errorMessage
:
s__
(
'
TestCases|Something went wrong while updating the test case.
'
),
})
.
then
(
updatedTestCase
=>
{
this
.
testCase
=
updatedTestCase
;
this
.
editTestCaseFormVisible
=
false
;
IssuableEventHub
.
$emit
(
'
update.issuable
'
);
})
.
finally
(()
=>
{
this
.
testCaseSaveInProgress
=
false
;
});
},
handleCancelClick
()
{
this
.
editTestCaseFormVisible
=
false
;
IssuableEventHub
.
$emit
(
'
close.form
'
);
},
handleTestCaseUpdated
(
updatedTestCase
)
{
this
.
testCase
=
updatedTestCase
;
},
},
};
</
script
>
<
template
>
<div
class=
"test-case-container"
>
<gl-loading-icon
v-if=
"testCaseLoading"
size=
"md"
class=
"gl-mt-3"
/>
<issuable-show
v-if=
"!testCaseLoading && !testCaseLoadFailed"
:issuable=
"testCase"
:status-badge-class=
"statusBadgeClass"
:status-icon=
"statusIcon"
:enable-edit=
"canEditTestCase"
:enable-autocomplete=
"true"
:edit-form-visible=
"editTestCaseFormVisible"
:description-preview-path=
"descriptionPreviewPath"
:description-help-path=
"descriptionHelpPath"
@
edit-issuable=
"handleEditTestCase"
>
<template
#status-badge
>
{{
statusBadgeText
}}
</
template
>
<
template
#header-actions
>
<gl-dropdown
v-if=
"canEditTestCase"
data-testid=
"actions-dropdown"
:text=
"__('Options')"
:right=
"true"
class=
"d-md-none d-lg-none d-xl-none gl-flex-grow-1"
>
<gl-dropdown-item>
{{
testCaseActionTitle
}}
</gl-dropdown-item>
<gl-dropdown-divider
/>
<gl-dropdown-item
:href=
"testCaseNewPath"
>
{{
__
(
'
New test case
'
)
}}
</gl-dropdown-item>
</gl-dropdown>
<gl-button
v-if=
"canEditTestCase"
data-testid=
"archive-test-case"
category=
"secondary"
class=
"d-none d-sm-none d-md-inline-block gl-mr-2"
:variant=
"testCaseActionButtonVariant"
:loading=
"testCaseStateChangeInProgress"
@
click=
"handleTestCaseStateChange"
>
{{
testCaseActionTitle
}}
</gl-button
>
<gl-button
data-testid=
"new-test-case"
category=
"secondary"
variant=
"success"
class=
"d-md-inline-block"
:class=
"
{ 'd-none d-sm-none': canEditTestCase, 'gl-flex-grow-1': !canEditTestCase }"
:href="testCaseNewPath"
>
{{
__
(
'
New test case
'
)
}}
</gl-button
>
</
template
>
<
template
#edit-form-actions=
"issuableMeta"
>
<gl-button
data-testid=
"save-test-case"
:disable=
"testCaseSaveInProgress || !issuableMeta.issuableTitle.length"
:loading=
"testCaseSaveInProgress"
category=
"primary"
variant=
"success"
class=
"float-left qa-save-button"
@
click.prevent=
"handleSaveTestCase(issuableMeta)"
>
{{
__
(
'
Save changes
'
)
}}
</gl-button
>
<gl-button
data-testid=
"cancel-test-case-edit"
class=
"float-right"
@
click=
"handleCancelClick"
>
{{
__
(
'
Cancel
'
)
}}
</gl-button>
</
template
>
<
template
#right-sidebar-items=
"{ sidebarExpanded }"
>
<test-case-sidebar
:sidebar-expanded=
"sidebarExpanded"
:selected-labels=
"selectedLabels"
:todo=
"todo"
@
test-case-updated=
"handleTestCaseUpdated"
/>
</
template
>
</issuable-show>
</div>
</template>
ee/app/assets/javascripts/test_case_show/components/test_case_sidebar.vue
0 → 100644
View file @
da977f43
<
script
>
import
{
GlTooltipDirective
as
GlTooltip
,
GlButton
,
GlIcon
,
GlLoadingIcon
}
from
'
@gitlab/ui
'
;
import
Mousetrap
from
'
mousetrap
'
;
import
{
s__
,
__
}
from
'
~/locale
'
;
import
LabelsSelect
from
'
~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
'
;
import
TestCaseGraphQL
from
'
../mixins/test_case_graphql
'
;
export
default
{
components
:
{
GlButton
,
GlIcon
,
GlLoadingIcon
,
LabelsSelect
,
},
directives
:
{
GlTooltip
,
},
inject
:
[
'
projectFullPath
'
,
'
testCaseId
'
,
'
canEditTestCase
'
,
'
labelsFetchPath
'
,
'
labelsManagePath
'
,
],
mixins
:
[
TestCaseGraphQL
],
props
:
{
sidebarExpanded
:
{
type
:
Boolean
,
required
:
true
,
},
todo
:
{
type
:
Object
,
required
:
false
,
default
:
null
,
},
selectedLabels
:
{
type
:
Array
,
required
:
true
,
},
},
data
()
{
return
{
sidebarExpandedOnClick
:
false
,
testCaseLabelsSelectInProgress
:
false
,
};
},
computed
:
{
isTodoPending
()
{
return
this
.
todo
?.
state
===
'
pending
'
;
},
todoUpdateInProgress
()
{
return
this
.
$apollo
.
queries
.
testCase
.
loading
||
this
.
testCaseTodoUpdateInProgress
;
},
todoActionText
()
{
return
this
.
isTodoPending
?
__
(
'
Mark as done
'
)
:
__
(
'
Add a to do
'
);
},
todoIcon
()
{
return
this
.
isTodoPending
?
'
todo-done
'
:
'
todo-add
'
;
},
},
mounted
()
{
Mousetrap
.
bind
(
'
l
'
,
this
.
handleLabelsCollapsedButtonClick
);
},
beforeDestroy
()
{
Mousetrap
.
unbind
(
'
l
'
);
},
methods
:
{
handleTodoButtonClick
()
{
if
(
this
.
isTodoPending
)
{
this
.
markTestCaseTodoDone
();
}
else
{
this
.
addTestCaseAsTodo
();
}
},
toggleSidebar
()
{
document
.
querySelector
(
'
.js-toggle-right-sidebar-button
'
).
dispatchEvent
(
new
Event
(
'
click
'
));
},
handleLabelsDropdownClose
()
{
if
(
this
.
sidebarExpandedOnClick
)
{
this
.
sidebarExpandedOnClick
=
false
;
this
.
toggleSidebar
();
}
},
handleLabelsCollapsedButtonClick
()
{
// Expand the sidebar if not already expanded.
if
(
!
this
.
sidebarExpanded
)
{
this
.
toggleSidebar
();
this
.
sidebarExpandedOnClick
=
true
;
}
// Wait for sidebar expand to complete before
// revealing labels dropdown.
this
.
$nextTick
(()
=>
{
document
.
querySelector
(
'
.js-labels-block .js-sidebar-dropdown-toggle
'
)
.
dispatchEvent
(
new
Event
(
'
click
'
,
{
bubbles
:
true
,
cancelable
:
false
}));
});
},
handleUpdateSelectedLabels
(
labels
)
{
// Iterate over selection and check if labels which were
// either selected or removed aren't leading to same selection
// as current one, as then we don't want to make network call
// since nothing has changed.
const
anyLabelUpdated
=
labels
.
some
(
label
=>
{
// Find this label in existing selection.
const
existingLabel
=
this
.
selectedLabels
.
find
(
l
=>
l
.
id
===
label
.
id
);
// Check either of the two following conditions;
// 1. A label that's not currently applied is being applied.
// 2. A label that's already applied is being removed.
return
(
!
existingLabel
&&
label
.
set
)
||
(
existingLabel
&&
!
label
.
set
);
});
// Only proceed with action if there are any label updates to be done.
if
(
anyLabelUpdated
)
{
this
.
testCaseLabelsSelectInProgress
=
true
;
return
this
.
updateTestCase
({
variables
:
{
addLabelIds
:
labels
.
filter
(
label
=>
label
.
set
).
map
(
label
=>
label
.
id
),
removeLabelIds
:
labels
.
filter
(
label
=>
!
label
.
set
).
map
(
label
=>
label
.
id
),
},
errorMessage
:
s__
(
'
TestCases|Something went wrong while updating the test case labels.
'
),
})
.
then
(
updatedTestCase
=>
{
this
.
$emit
(
'
test-case-updated
'
,
updatedTestCase
);
})
.
finally
(()
=>
{
this
.
testCaseLabelsSelectInProgress
=
false
;
});
}
return
null
;
},
},
};
</
script
>
<
template
>
<div
class=
"test-case-sidebar-items"
>
<template
v-if=
"canEditTestCase"
>
<div
v-if=
"sidebarExpanded"
data-testid=
"todo"
class=
"block todo gl-display-flex"
>
<span
class=
"gl-flex-grow-1"
>
{{
__
(
'
To Do
'
)
}}
</span>
<gl-button
:loading=
"todoUpdateInProgress"
size=
"small"
@
click=
"handleTodoButtonClick"
>
{{
todoActionText
}}
</gl-button>
</div>
<div
v-else
class=
"block todo"
>
<button
v-gl-tooltip.viewport=
"
{ placement: 'left' }"
:title="todoActionText"
class="btn-blank sidebar-collapsed-icon"
@click="handleTodoButtonClick"
>
<gl-loading-icon
v-if=
"todoUpdateInProgress"
/>
<gl-icon
v-else
:name=
"todoIcon"
:class=
"
{ 'todo-undone': isTodoPending }" />
</button>
</div>
</
template
>
<labels-select
:allow-label-edit=
"canEditTestCase"
:allow-label-create=
"true"
:allow-multiselect=
"true"
:allow-scoped-labels=
"true"
:selected-labels=
"selectedLabels"
:labels-select-in-progress=
"testCaseLabelsSelectInProgress"
:labels-fetch-path=
"labelsFetchPath"
:labels-manage-path=
"labelsManagePath"
variant=
"sidebar"
class=
"block labels js-labels-block"
@
updateSelectedLabels=
"handleUpdateSelectedLabels"
@
onDropdownClose=
"handleLabelsDropdownClose"
@
toggleCollapse=
"handleLabelsCollapsedButtonClick"
>
{{ __('None') }}
</labels-select
>
</div>
</template>
ee/app/assets/javascripts/test_case_show/mixins/test_case_graphql.js
0 → 100644
View file @
da977f43
import
Api
from
'
~/api
'
;
import
createFlash
from
'
~/flash
'
;
import
{
s__
}
from
'
~/locale
'
;
import
projectTestCase
from
'
../queries/project_test_case.query.graphql
'
;
import
updateTestCase
from
'
../queries/update_test_case.mutation.graphql
'
;
import
markTestCaseTodoDone
from
'
../queries/mark_test_case_todo_done.mutation.graphql
'
;
export
default
{
apollo
:
{
testCase
:
{
query
:
projectTestCase
,
variables
()
{
return
{
projectPath
:
this
.
projectFullPath
,
testCaseId
:
this
.
testCaseId
,
};
},
update
(
data
)
{
return
data
.
project
?.
issue
;
},
result
()
{
this
.
testCaseLoading
=
false
;
},
error
(
error
)
{
this
.
testCaseLoadFailed
=
true
;
createFlash
({
message
:
s__
(
'
TestCases|Something went wrong while fetching test case.
'
),
captureError
:
true
,
error
,
});
throw
error
;
},
},
},
data
()
{
return
{
testCaseLoading
:
true
,
testCaseLoadFailed
:
false
,
testCaseTodoUpdateInProgress
:
false
,
};
},
methods
:
{
updateTestCase
({
variables
,
errorMessage
})
{
return
this
.
$apollo
.
mutate
({
mutation
:
updateTestCase
,
variables
:
{
updateTestCaseInput
:
{
projectPath
:
this
.
projectFullPath
,
iid
:
this
.
testCaseId
,
...
variables
,
},
},
})
.
then
(({
data
=
{}
})
=>
{
const
errors
=
data
.
updateIssue
?.
errors
;
if
(
errors
?.
length
)
{
throw
new
Error
(
`Error updating test case. Error message:
${
errors
[
0
].
message
}
`
);
}
return
data
.
updateIssue
?.
issue
;
})
.
catch
(
error
=>
{
createFlash
({
message
:
errorMessage
,
captureError
:
true
,
error
,
});
});
},
/**
* We're using Public REST API to add Test Case as a Todo since
* GraphQL mutation to do the same is unavailable as of now.
*/
addTestCaseAsTodo
()
{
this
.
testCaseTodoUpdateInProgress
=
true
;
return
Api
.
addProjectIssueAsTodo
(
this
.
projectFullPath
,
this
.
testCaseId
)
.
then
(()
=>
{
this
.
$apollo
.
queries
.
testCase
.
refetch
();
})
.
catch
(
error
=>
{
createFlash
({
message
:
s__
(
'
TestCases|Something went wrong while adding test case to Todo.
'
),
captureError
:
true
,
error
,
});
})
.
finally
(()
=>
{
this
.
testCaseTodoUpdateInProgress
=
false
;
});
},
markTestCaseTodoDone
()
{
this
.
testCaseTodoUpdateInProgress
=
true
;
return
this
.
$apollo
.
mutate
({
mutation
:
markTestCaseTodoDone
,
variables
:
{
todoMarkDoneInput
:
{
id
:
this
.
todo
.
id
,
},
},
})
.
then
(({
data
=
{}
})
=>
{
const
errors
=
data
.
todoMarkDone
?.
errors
;
if
(
errors
?.
length
)
{
throw
new
Error
(
`Error marking todo as done. Error message:
${
errors
[
0
].
message
}
`
);
}
this
.
$apollo
.
queries
.
testCase
.
refetch
();
})
.
catch
(
error
=>
{
createFlash
({
message
:
s__
(
'
TestCases|Something went wrong while marking test case todo as done.
'
),
captureError
:
true
,
error
,
});
})
.
finally
(()
=>
{
this
.
testCaseTodoUpdateInProgress
=
false
;
});
},
},
};
ee/app/assets/javascripts/test_case_show/queries/mark_test_case_todo_done.mutation.graphql
0 → 100644
View file @
da977f43
mutation
markTestCaseTodoDone
(
$todoMarkDoneInput
:
TodoMarkDoneInput
!)
{
todoMarkDone
(
input
:
$todoMarkDoneInput
)
{
errors
clientMutationId
todo
{
id
}
}
}
ee/app/assets/javascripts/test_case_show/queries/project_test_case.query.graphql
0 → 100644
View file @
da977f43
#import "./test_case.fragment.graphql"
query
projectTestCase
(
$projectPath
:
ID
!,
$testCaseId
:
String
)
{
project
(
fullPath
:
$projectPath
)
{
name
issue
(
iid
:
$testCaseId
)
{
...
TestCase
}
}
}
ee/app/assets/javascripts/test_case_show/queries/test_case.fragment.graphql
0 → 100644
View file @
da977f43
#import "~/graphql_shared/fragments/author.fragment.graphql"
#import "~/graphql_shared/fragments/label.fragment.graphql"
fragment
TestCase
on
Issue
{
id
title
titleHtml
description
descriptionHtml
state
createdAt
updatedAt
webUrl
blocked
confidential
author
{
...
Author
}
labels
{
nodes
{
...
Label
}
}
currentUserTodos
(
first
:
1
)
{
nodes
{
id
state
}
}
}
ee/app/assets/javascripts/test_case_show/queries/update_test_case.mutation.graphql
0 → 100644
View file @
da977f43
#import "./test_case.fragment.graphql"
mutation
updateTestCase
(
$updateTestCaseInput
:
UpdateIssueInput
!)
{
updateIssue
(
input
:
$updateTestCaseInput
)
{
clientMutationId
errors
issue
{
...
TestCase
}
}
}
ee/app/assets/javascripts/test_case_show/test_case_show_bundle.js
0 → 100644
View file @
da977f43
import
Vue
from
'
vue
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
createDefaultClient
from
'
~/lib/graphql
'
;
import
{
parseBoolean
}
from
'
~/lib/utils/common_utils
'
;
import
TestCaseShowApp
from
'
./components/test_case_show_root.vue
'
;
Vue
.
use
(
VueApollo
);
export
default
function
initTestCaseShow
({
mountPointSelector
})
{
const
el
=
document
.
querySelector
(
mountPointSelector
);
if
(
!
el
)
{
return
null
;
}
const
apolloProvider
=
new
VueApollo
({
defaultClient
:
createDefaultClient
(),
});
return
new
Vue
({
el
,
apolloProvider
,
provide
:
{
...
el
.
dataset
,
canEditTestCase
:
parseBoolean
(
el
.
dataset
.
canEditTestCase
),
},
render
:
createElement
=>
createElement
(
TestCaseShowApp
),
});
}
ee/app/views/projects/quality/test_cases/_show.html.haml
View file @
da977f43
...
...
@@ -5,5 +5,12 @@
-
page_title
"
#{
@issue
.
title
}
(
#{
@issue
.
to_reference
}
)"
,
_
(
'Test Cases'
)
-
page_description
@issue
.
description
-# haml-lint:disable InlineJavaScript
%script
#js-issuable-app-initial-data
{
type:
"application/json"
}=
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
,
description_preview_path:
preview_markdown_path
(
@project
),
description_help_path:
help_page_path
(
'user/markdown'
),
project_full_path:
@project
.
full_path
,
labels_manage_path:
project_labels_path
(
@project
),
labels_fetch_path:
project_labels_path
(
@project
,
format: :json
),
test_case_new_path:
new_project_quality_test_case_path
(
@project
),
test_case_id:
@issue
.
iid
}
}
ee/spec/features/projects/quality/test_case_show_spec.rb
0 → 100644
View file @
da977f43
# frozen_string_literal: true
require
'spec_helper'
RSpec
.
describe
'Test Cases'
,
:js
do
let_it_be
(
:user
)
{
create
(
:user
)
}
let_it_be
(
:project
)
{
create
(
:project
,
:repository
)
}
let_it_be
(
:label_bug
)
{
create
(
:label
,
project:
project
,
title:
'bug'
)
}
let_it_be
(
:label_doc
)
{
create
(
:label
,
project:
project
,
title:
'documentation'
)
}
let_it_be
(
:test_case
)
{
create
(
:quality_test_case
,
project:
project
,
author:
user
,
description:
'Sample description'
,
created_at:
5
.
days
.
ago
,
updated_at:
2
.
days
.
ago
,
labels:
[
label_bug
])
}
before
do
project
.
add_developer
(
user
)
stub_licensed_features
(
quality_management:
true
)
sign_in
(
user
)
end
context
'test case page'
do
before
do
visit
project_issue_path
(
project
,
test_case
)
wait_for_all_requests
end
context
'header'
do
it
'shows status, created date and author'
do
page
.
within
(
'.test-case-container .detail-page-header-body'
)
do
expect
(
page
.
find
(
'.issuable-status-box'
)).
to
have_content
(
'Open'
)
expect
(
page
.
find
(
'.issuable-meta'
)).
to
have_content
(
'Opened 5 days ago'
)
expect
(
page
.
find
(
'.issuable-meta'
)).
to
have_link
(
user
.
name
)
end
end
it
'shows action buttons'
do
page
.
within
(
'.test-case-container .detail-page-header-actions'
)
do
expect
(
page
).
to
have_selector
(
'.dropdown'
,
visible:
false
)
expect
(
page
).
to
have_button
(
'Archive test case'
)
expect
(
page
).
to
have_link
(
'New test case'
,
href:
new_project_quality_test_case_path
(
project
))
end
end
it
'archives test case'
do
page
.
within
(
'.test-case-container'
)
do
click_button
'Archive test case'
wait_for_requests
expect
(
page
.
find
(
'.issuable-status-box'
)).
to
have_content
(
'Archived'
)
expect
(
page
).
to
have_button
(
'Reopen test case'
)
end
end
end
context
'body'
do
it
'shows title, description and edit button'
do
page
.
within
(
'.test-case-container .issuable-details'
)
do
expect
(
page
.
find
(
'.title'
)).
to
have_content
(
test_case
.
title
)
expect
(
page
.
find
(
'.description'
)).
to
have_content
(
test_case
.
description
)
expect
(
page
).
to
have_selector
(
'button.js-issuable-edit'
)
end
end
it
'makes title and description editable on edit click'
do
find
(
'.test-case-container .issuable-details .js-issuable-edit'
).
click
page
.
within
(
'.test-case-container .issuable-details form'
)
do
expect
(
page
.
find
(
'input#issuable-title'
).
value
).
to
eq
(
test_case
.
title
)
expect
(
page
.
find
(
'textarea#issuable-description'
).
value
).
to
eq
(
test_case
.
description
)
expect
(
page
).
to
have_button
(
'Save changes'
)
expect
(
page
).
to
have_button
(
'Cancel'
)
end
end
it
'update title and description'
do
title
=
'Updated title'
description
=
'Updated test case description.'
find
(
'.test-case-container .issuable-details .js-issuable-edit'
).
click
page
.
within
(
'.test-case-container .issuable-details form'
)
do
page
.
find
(
'input#issuable-title'
).
set
(
title
)
page
.
find
(
'textarea#issuable-description'
).
set
(
description
)
click_button
'Save changes'
end
wait_for_requests
page
.
within
(
'.test-case-container .issuable-details'
)
do
expect
(
page
.
find
(
'.title'
)).
to
have_content
(
title
)
expect
(
page
.
find
(
'.description'
)).
to
have_content
(
description
)
expect
(
page
.
find
(
'.edited-text'
)).
to
have_content
(
''
)
end
end
end
context
'sidebar'
do
it
'shows expand/collapse button'
do
page
.
within
(
'.test-case-container .right-sidebar'
)
do
expect
(
page
).
to
have_button
(
'Collapse sidebar'
)
end
end
context
'todo'
do
it
'shows todo status'
do
page
.
within
(
'.test-case-container .issuable-sidebar'
)
do
expect
(
page
.
find
(
'.block.todo'
)).
to
have_content
(
'To Do'
)
expect
(
page
).
to
have_button
(
'Add a to do'
)
end
end
it
'add test case as todo'
do
page
.
within
(
'.test-case-container .issuable-sidebar'
)
do
click_button
'Add a to do'
wait_for_all_requests
expect
(
page
).
to
have_button
(
'Mark as done'
)
end
end
it
'mark test case todo as done'
do
page
.
within
(
'.test-case-container .issuable-sidebar'
)
do
click_button
'Add a to do'
wait_for_all_requests
click_button
'Mark as done'
wait_for_all_requests
expect
(
page
).
to
have_button
(
'Add a to do'
)
end
end
end
context
'labels'
do
it
'shows assigned labels'
do
page
.
within
(
'.test-case-container .issuable-sidebar'
)
do
expect
(
page
).
to
have_selector
(
'.labels-select-wrapper'
)
expect
(
page
.
find
(
'.labels-select-wrapper .value'
)).
to
have_content
(
label_bug
.
title
)
end
end
it
'shows labels dropdown on edit click'
do
page
.
within
(
'.test-case-container .issuable-sidebar .labels-select-wrapper'
)
do
click_button
'Edit'
wait_for_requests
expect
(
page
.
find
(
'.js-labels-list .dropdown-content'
)).
to
have_selector
(
'li'
,
count:
2
)
expect
(
page
.
find
(
'.js-labels-list .dropdown-footer'
)).
to
have_selector
(
'li'
,
count:
2
)
end
end
it
'applies label using labels dropdown'
do
page
.
within
(
'.test-case-container .issuable-sidebar .labels-select-wrapper'
)
do
click_button
'Edit'
wait_for_requests
click_link
label_doc
.
title
click_button
'Edit'
wait_for_requests
expect
(
page
.
find
(
'.labels-select-wrapper .value'
)).
to
have_content
(
label_doc
.
title
)
end
end
end
end
end
end
ee/spec/frontend/test_case_show/components/test_case_show_root_spec.js
0 → 100644
View file @
da977f43
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
GlLoadingIcon
}
from
'
@gitlab/ui
'
;
import
{
mockCurrentUserTodo
}
from
'
jest/issuable_list/mock_data
'
;
import
TestCaseShowRoot
from
'
ee/test_case_show/components/test_case_show_root.vue
'
;
import
TestCaseSidebar
from
'
ee/test_case_show/components/test_case_sidebar.vue
'
;
import
IssuableShow
from
'
~/issuable_show/components/issuable_show_root.vue
'
;
import
IssuableHeader
from
'
~/issuable_show/components/issuable_header.vue
'
;
import
IssuableBody
from
'
~/issuable_show/components/issuable_body.vue
'
;
import
IssuableEditForm
from
'
~/issuable_show/components/issuable_edit_form.vue
'
;
import
IssuableSidebar
from
'
~/issuable_sidebar/components/issuable_sidebar_root.vue
'
;
import
IssuableEventHub
from
'
~/issuable_show/event_hub
'
;
import
{
mockProvide
,
mockTestCase
}
from
'
../mock_data
'
;
jest
.
mock
(
'
~/issuable_show/event_hub
'
);
const
createComponent
=
({
testCase
,
testCaseQueryLoading
=
false
}
=
{})
=>
shallowMount
(
TestCaseShowRoot
,
{
provide
:
{
...
mockProvide
,
},
mocks
:
{
$apollo
:
{
queries
:
{
testCase
:
{
loading
:
testCaseQueryLoading
,
refetch
:
jest
.
fn
(),
},
},
},
},
stubs
:
{
IssuableShow
,
IssuableHeader
,
IssuableBody
,
IssuableEditForm
,
IssuableSidebar
,
},
data
()
{
return
{
testCaseLoading
:
testCaseQueryLoading
,
testCase
:
testCaseQueryLoading
?
{}
:
{
...
mockTestCase
,
...
testCase
,
},
};
},
});
describe
(
'
TestCaseShowRoot
'
,
()
=>
{
let
wrapper
;
beforeEach
(()
=>
{
wrapper
=
createComponent
();
});
afterEach
(()
=>
{
wrapper
.
destroy
();
});
describe
(
'
computed
'
,
()
=>
{
describe
.
each
`
state | isTestCaseOpen | statusBadgeClass | statusIcon | statusBadgeText | testCaseActionButtonVariant | testCaseActionTitle
${
'
opened
'
}
|
${
true
}
|
${
'
status-box-open
'
}
|
${
'
issue-open-m
'
}
|
${
'
Open
'
}
|
${
'
warning
'
}
|
${
'
Archive test case
'
}
${
'
closed
'
}
|
${
false
}
|
${
'
status-box-issue-closed
'
}
|
${
'
mobile-issue-close
'
}
|
${
'
Archived
'
}
|
${
'
default
'
}
|
${
'
Reopen test case
'
}
`
(
'
when `testCase.state` is $state
'
,
({
state
,
isTestCaseOpen
,
statusBadgeClass
,
statusIcon
,
statusBadgeText
,
testCaseActionButtonVariant
,
testCaseActionTitle
,
})
=>
{
beforeEach
(
async
()
=>
{
wrapper
.
setData
({
testCase
:
{
...
mockTestCase
,
state
,
},
});
await
wrapper
.
vm
.
$nextTick
();
});
it
.
each
`
propName | propValue
${
'
isTestCaseOpen
'
}
|
${
isTestCaseOpen
}
${
'
statusBadgeClass
'
}
|
${
statusBadgeClass
}
${
'
statusIcon
'
}
|
${
statusIcon
}
${
'
statusBadgeText
'
}
|
${
statusBadgeText
}
${
'
testCaseActionButtonVariant
'
}
|
${
testCaseActionButtonVariant
}
${
'
testCaseActionTitle
'
}
|
${
testCaseActionTitle
}
`
(
'
computed prop $propName returns $propValue
'
,
({
propName
,
propValue
})
=>
{
expect
(
wrapper
.
vm
[
propName
]).
toBe
(
propValue
);
});
},
);
describe
(
'
todo
'
,
()
=>
{
it
(
'
returns first todo object from `testCase.currentUserTodos.nodes` array
'
,
()
=>
{
expect
(
wrapper
.
vm
.
todo
).
toBe
(
mockCurrentUserTodo
);
});
});
describe
(
'
selectedLabels
'
,
()
=>
{
it
(
'
returns `testCase.labels.nodes` array with GraphQL IDs converted to numeric IDs
'
,
()
=>
{
mockTestCase
.
labels
.
nodes
.
forEach
((
label
,
index
)
=>
{
expect
(
label
.
id
.
endsWith
(
`
${
wrapper
.
vm
.
selectedLabels
[
index
].
id
}
`
)).
toBe
(
true
);
});
});
});
});
describe
(
'
methods
'
,
()
=>
{
describe
(
'
handleTestCaseStateChange
'
,
()
=>
{
const
updateTestCase
=
{
...
mockTestCase
,
state
:
'
closed
'
,
};
beforeEach
(()
=>
{
jest
.
spyOn
(
wrapper
.
vm
,
'
updateTestCase
'
).
mockResolvedValue
(
updateTestCase
);
});
it
(
'
sets `testCaseStateChangeInProgress` prop to true
'
,
()
=>
{
wrapper
.
vm
.
handleTestCaseStateChange
();
expect
(
wrapper
.
vm
.
testCaseStateChangeInProgress
).
toBe
(
true
);
});
it
(
'
calls `wrapper.vm.updateTestCase` with variable `stateEvent` and errorMessage string
'
,
()
=>
{
wrapper
.
vm
.
handleTestCaseStateChange
();
expect
(
wrapper
.
vm
.
updateTestCase
).
toHaveBeenCalledWith
({
variables
:
{
stateEvent
:
'
CLOSE
'
,
},
errorMessage
:
'
Something went wrong while updating the test case.
'
,
});
});
it
(
'
sets `testCase` prop with updated test case received in response
'
,
()
=>
{
return
wrapper
.
vm
.
handleTestCaseStateChange
().
then
(()
=>
{
expect
(
wrapper
.
vm
.
testCase
).
toBe
(
updateTestCase
);
});
});
it
(
'
sets `testCaseStateChangeInProgress` prop to false
'
,
()
=>
{
return
wrapper
.
vm
.
handleTestCaseStateChange
().
then
(()
=>
{
expect
(
wrapper
.
vm
.
testCaseStateChangeInProgress
).
toBe
(
false
);
});
});
});
describe
(
'
handleEditTestCase
'
,
()
=>
{
it
(
'
sets `editTestCaseFormVisible` prop to true
'
,
()
=>
{
wrapper
.
vm
.
handleEditTestCase
();
expect
(
wrapper
.
vm
.
editTestCaseFormVisible
).
toBe
(
true
);
});
});
describe
(
'
handleSaveTestCase
'
,
()
=>
{
const
updateTestCase
=
{
...
mockTestCase
,
title
:
'
Foo
'
,
description
:
'
Bar
'
,
};
beforeEach
(()
=>
{
jest
.
spyOn
(
wrapper
.
vm
,
'
updateTestCase
'
).
mockResolvedValue
(
updateTestCase
);
});
it
(
'
sets `testCaseSaveInProgress` prop to true
'
,
()
=>
{
wrapper
.
vm
.
handleSaveTestCase
({
issuableTitle
:
'
Foo
'
,
issuableDescription
:
'
Bar
'
,
});
expect
(
wrapper
.
vm
.
testCaseSaveInProgress
).
toBe
(
true
);
});
it
(
'
calls `wrapper.vm.updateTestCase` with variables `title` & `description` and errorMessage string
'
,
()
=>
{
wrapper
.
vm
.
handleSaveTestCase
({
issuableTitle
:
'
Foo
'
,
issuableDescription
:
'
Bar
'
,
});
expect
(
wrapper
.
vm
.
updateTestCase
).
toHaveBeenCalledWith
({
variables
:
{
title
:
'
Foo
'
,
description
:
'
Bar
'
,
},
errorMessage
:
'
Something went wrong while updating the test case.
'
,
});
});
it
(
'
sets `testCase` prop with updated test case received in response and emits "update.issuable" on IssuableEventHub
'
,
()
=>
{
return
wrapper
.
vm
.
handleSaveTestCase
({
issuableTitle
:
'
Foo
'
,
issuableDescription
:
'
Bar
'
,
})
.
then
(()
=>
{
expect
(
wrapper
.
vm
.
testCase
).
toBe
(
updateTestCase
);
expect
(
wrapper
.
vm
.
editTestCaseFormVisible
).
toBe
(
false
);
expect
(
IssuableEventHub
.
$emit
).
toHaveBeenCalledWith
(
'
update.issuable
'
);
});
});
it
(
'
sets `testCaseSaveInProgress` prop to false
'
,
()
=>
{
return
wrapper
.
vm
.
handleSaveTestCase
({
issuableTitle
:
'
Foo
'
,
issuableDescription
:
'
Bar
'
,
})
.
then
(()
=>
{
expect
(
wrapper
.
vm
.
testCaseSaveInProgress
).
toBe
(
false
);
});
});
});
describe
(
'
handleCancelClick
'
,
()
=>
{
it
(
'
sets `editTestCaseFormVisible` prop to false and emits "close.form" even in IssuableEventHub
'
,
async
()
=>
{
wrapper
.
setData
({
editTestCaseFormVisible
:
true
,
});
await
wrapper
.
vm
.
$nextTick
();
wrapper
.
vm
.
handleCancelClick
();
expect
(
wrapper
.
vm
.
editTestCaseFormVisible
).
toBe
(
false
);
expect
(
IssuableEventHub
.
$emit
).
toHaveBeenCalledWith
(
'
close.form
'
);
});
});
describe
(
'
handleTestCaseUpdated
'
,
()
=>
{
it
(
'
assigns value of provided testCase param to `testCase` prop
'
,
()
=>
{
const
updatedTestCase
=
{
...
mockTestCase
,
title
:
'
Foo
'
,
};
wrapper
.
vm
.
handleTestCaseUpdated
(
updatedTestCase
);
expect
(
wrapper
.
vm
.
testCase
).
toBe
(
updatedTestCase
);
});
});
});
describe
(
'
template
'
,
()
=>
{
it
(
'
renders gl-loading-icon when testCaseLoading prop is true
'
,
async
()
=>
{
wrapper
.
setData
({
testCaseLoading
:
true
,
});
await
wrapper
.
vm
.
$nextTick
();
expect
(
wrapper
.
find
(
GlLoadingIcon
).
exists
()).
toBe
(
true
);
});
it
(
'
renders issuable-show when `testCaseLoading` prop is false
'
,
()
=>
{
const
{
statusBadgeClass
,
statusIcon
,
editTestCaseFormVisible
}
=
wrapper
.
vm
;
const
{
canEditTestCase
,
descriptionPreviewPath
,
descriptionHelpPath
}
=
mockProvide
;
const
issuableShowEl
=
wrapper
.
find
(
IssuableShow
);
expect
(
issuableShowEl
.
exists
()).
toBe
(
true
);
expect
(
issuableShowEl
.
props
()).
toMatchObject
({
statusBadgeClass
,
statusIcon
,
descriptionPreviewPath
,
descriptionHelpPath
,
enableAutocomplete
:
true
,
issuable
:
mockTestCase
,
enableEdit
:
canEditTestCase
,
editFormVisible
:
editTestCaseFormVisible
,
});
});
it
(
'
does not render issuable-show when `testCaseLoading` prop is false and `testCaseLoadFailed` prop is true
'
,
async
()
=>
{
wrapper
.
setData
({
testCaseLoading
:
false
,
testCaseLoadFailed
:
true
,
});
await
wrapper
.
vm
.
$nextTick
();
expect
(
wrapper
.
find
(
IssuableShow
).
exists
()).
toBe
(
false
);
});
it
(
'
renders status-badge slot contents
'
,
()
=>
{
expect
(
wrapper
.
find
(
'
[data-testid="status"]
'
).
text
()).
toContain
(
'
Open
'
);
});
it
(
'
renders header-actions slot contents
'
,
()
=>
{
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="new-test-case"]
'
).
exists
()).
toBe
(
true
);
});
it
(
'
renders edit-form-actions slot contents
'
,
async
()
=>
{
wrapper
.
setData
({
editTestCaseFormVisible
:
true
,
});
await
wrapper
.
vm
.
$nextTick
();
expect
(
wrapper
.
find
(
'
[data-testid="save-test-case"]
'
).
exists
()).
toBe
(
true
);
expect
(
wrapper
.
find
(
'
[data-testid="cancel-test-case-edit"]
'
).
exists
()).
toBe
(
true
);
});
it
(
'
renders test-case-sidebar
'
,
async
()
=>
{
expect
(
wrapper
.
find
(
TestCaseSidebar
).
exists
()).
toBe
(
true
);
});
});
});
ee/spec/frontend/test_case_show/components/test_case_sidebar_spec.js
0 → 100644
View file @
da977f43
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
GlButton
,
GlIcon
}
from
'
@gitlab/ui
'
;
import
Mousetrap
from
'
mousetrap
'
;
import
{
mockCurrentUserTodo
,
mockLabels
}
from
'
jest/issuable_list/mock_data
'
;
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
{
mockProvide
,
mockTestCase
}
from
'
../mock_data
'
;
const
createComponent
=
({
sidebarExpanded
=
true
,
todo
=
mockCurrentUserTodo
,
selectedLabels
=
mockLabels
,
testCaseLoading
=
false
,
}
=
{})
=>
shallowMount
(
TestCaseSidebar
,
{
provide
:
{
...
mockProvide
,
},
propsData
:
{
sidebarExpanded
,
todo
,
selectedLabels
,
},
mocks
:
{
$apollo
:
{
queries
:
{
testCase
:
{
loading
:
testCaseLoading
,
},
},
},
},
});
describe
(
'
TestCaseSidebar
'
,
()
=>
{
let
mousetrapSpy
;
let
wrapper
;
beforeEach
(()
=>
{
mousetrapSpy
=
jest
.
spyOn
(
Mousetrap
,
'
bind
'
);
wrapper
=
createComponent
();
});
afterEach
(()
=>
{
wrapper
.
destroy
();
});
describe
(
'
computed
'
,
()
=>
{
describe
.
each
`
state | isTodoPending | todoActionText | todoIcon
${
'
pending
'
}
|
${
true
}
|
${
'
Mark as done
'
}
|
${
'
todo-done
'
}
${
'
done
'
}
|
${
false
}
|
${
'
Add a to do
'
}
|
${
'
todo-add
'
}
`
(
'
when `todo.state` is "$state"
'
,
({
state
,
isTodoPending
,
todoActionText
,
todoIcon
})
=>
{
beforeEach
(
async
()
=>
{
wrapper
.
setProps
({
todo
:
{
...
mockCurrentUserTodo
,
state
,
},
});
await
wrapper
.
vm
.
$nextTick
();
});
it
.
each
`
propName | propValue
${
'
isTodoPending
'
}
|
${
isTodoPending
}
${
'
todoActionText
'
}
|
${
todoActionText
}
${
'
todoIcon
'
}
|
${
todoIcon
}
`
(
'
computed prop `$propName` returns $propValue
'
,
({
propName
,
propValue
})
=>
{
expect
(
wrapper
.
vm
[
propName
]).
toBe
(
propValue
);
});
});
});
describe
(
'
mounted
'
,
()
=>
{
it
(
'
binds key-press listener for `l` on Mousetrap
'
,
()
=>
{
expect
(
mousetrapSpy
).
toHaveBeenCalledWith
(
'
l
'
,
wrapper
.
vm
.
handleLabelsCollapsedButtonClick
);
});
});
describe
(
'
methods
'
,
()
=>
{
describe
(
'
handleTodoButtonClick
'
,
()
=>
{
it
.
each
`
state | methodToCall
${
'
pending
'
}
|
${
'
markTestCaseTodoDone
'
}
${
'
done
'
}
|
${
'
addTestCaseAsTodo
'
}
`
(
'
calls `wrapper.vm.$methodToCall` when `todo.state` is "$state"
'
,
async
({
state
,
methodToCall
})
=>
{
jest
.
spyOn
(
wrapper
.
vm
,
methodToCall
).
mockImplementation
(
jest
.
fn
());
wrapper
.
setProps
({
todo
:
{
...
mockCurrentUserTodo
,
state
,
},
});
await
wrapper
.
vm
.
$nextTick
();
wrapper
.
vm
.
handleTodoButtonClick
();
expect
(
wrapper
.
vm
[
methodToCall
]).
toHaveBeenCalled
();
},
);
});
describe
(
'
toggleSidebar
'
,
()
=>
{
beforeEach
(()
=>
{
setFixtures
(
'
<button class="js-toggle-right-sidebar-button"></button>
'
);
});
it
(
'
dispatches click event on sidebar toggle button
'
,
()
=>
{
const
buttonEl
=
document
.
querySelector
(
'
.js-toggle-right-sidebar-button
'
);
jest
.
spyOn
(
buttonEl
,
'
dispatchEvent
'
);
wrapper
.
vm
.
toggleSidebar
();
expect
(
buttonEl
.
dispatchEvent
).
toHaveBeenCalledWith
(
expect
.
objectContaining
({
type
:
'
click
'
,
}),
);
});
});
describe
(
'
handleLabelsDropdownClose
'
,
()
=>
{
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
(()
=>
{
setFixtures
(
`
<div class="js-labels-block">
<button class="js-sidebar-dropdown-toggle"></button>
</div>
`
);
});
it
(
'
calls `toggleSidebar` method and sets `sidebarExpandedOnClick` to true when `sidebarExpanded` prop is false
'
,
async
()
=>
{
jest
.
spyOn
(
wrapper
.
vm
,
'
toggleSidebar
'
).
mockImplementation
(
jest
.
fn
());
wrapper
.
setProps
({
sidebarExpanded
:
false
,
});
await
wrapper
.
vm
.
$nextTick
();
wrapper
.
vm
.
handleLabelsCollapsedButtonClick
();
expect
(
wrapper
.
vm
.
toggleSidebar
).
toHaveBeenCalled
();
expect
(
wrapper
.
vm
.
sidebarExpandedOnClick
).
toBe
(
true
);
});
it
(
'
dispatches click event on label edit button
'
,
async
()
=>
{
const
buttonEl
=
document
.
querySelector
(
'
.js-sidebar-dropdown-toggle
'
);
jest
.
spyOn
(
wrapper
.
vm
,
'
toggleSidebar
'
).
mockImplementation
(
jest
.
fn
());
jest
.
spyOn
(
buttonEl
,
'
dispatchEvent
'
);
wrapper
.
setProps
({
sidebarExpanded
:
false
,
});
await
wrapper
.
vm
.
$nextTick
();
wrapper
.
vm
.
handleLabelsCollapsedButtonClick
();
await
wrapper
.
vm
.
$nextTick
();
expect
(
buttonEl
.
dispatchEvent
).
toHaveBeenCalledWith
(
expect
.
objectContaining
({
type
:
'
click
'
,
bubbles
:
true
,
cancelable
:
false
,
}),
);
});
});
describe
(
'
handleUpdateSelectedLabels
'
,
()
=>
{
const
updatedLabels
=
[
{
...
mockLabels
[
0
],
set
:
false
,
},
];
it
(
'
sets `testCaseLabelsSelectInProgress` to true when provided labels param includes any of the additions or removals
'
,
()
=>
{
jest
.
spyOn
(
wrapper
.
vm
,
'
updateTestCase
'
).
mockResolvedValue
(
mockTestCase
);
wrapper
.
vm
.
handleUpdateSelectedLabels
(
updatedLabels
);
expect
(
wrapper
.
vm
.
testCaseLabelsSelectInProgress
).
toBe
(
true
);
});
it
(
'
calls `updateTestCase` method with variables `addLabelIds` & `removeLabelIds` and erroMessage when provided labels param includes any of the additions or removals
'
,
()
=>
{
jest
.
spyOn
(
wrapper
.
vm
,
'
updateTestCase
'
).
mockResolvedValue
(
mockTestCase
);
wrapper
.
vm
.
handleUpdateSelectedLabels
(
updatedLabels
);
expect
(
wrapper
.
vm
.
updateTestCase
).
toHaveBeenCalledWith
({
variables
:
{
addLabelIds
:
[],
removeLabelIds
:
[
updatedLabels
[
0
].
id
],
},
errorMessage
:
'
Something went wrong while updating the test case labels.
'
,
});
});
it
(
'
emits "test-case-updated" event on component upon promise resolve
'
,
()
=>
{
jest
.
spyOn
(
wrapper
.
vm
,
'
updateTestCase
'
).
mockResolvedValue
(
mockTestCase
);
jest
.
spyOn
(
wrapper
.
vm
,
'
$emit
'
);
return
wrapper
.
vm
.
handleUpdateSelectedLabels
(
updatedLabels
).
then
(()
=>
{
expect
(
wrapper
.
vm
.
$emit
).
toHaveBeenCalledWith
(
'
test-case-updated
'
,
mockTestCase
);
});
});
it
(
'
sets `testCaseLabelsSelectInProgress` to false
'
,
()
=>
{
jest
.
spyOn
(
wrapper
.
vm
,
'
updateTestCase
'
).
mockResolvedValue
(
mockTestCase
);
return
wrapper
.
vm
.
handleUpdateSelectedLabels
(
updatedLabels
).
finally
(()
=>
{
expect
(
wrapper
.
vm
.
testCaseLabelsSelectInProgress
).
toBe
(
false
);
});
});
});
});
describe
(
'
template
'
,
()
=>
{
it
(
'
renders todo button
'
,
async
()
=>
{
let
todoEl
=
wrapper
.
find
(
'
[data-testid="todo"]
'
);
expect
(
todoEl
.
exists
()).
toBe
(
true
);
expect
(
todoEl
.
text
()).
toContain
(
'
To Do
'
);
expect
(
todoEl
.
find
(
GlButton
).
exists
()).
toBe
(
true
);
expect
(
todoEl
.
find
(
GlButton
).
text
()).
toBe
(
'
Add a to do
'
);
wrapper
.
setProps
({
sidebarExpanded
:
false
,
});
await
wrapper
.
vm
.
$nextTick
();
todoEl
=
wrapper
.
find
(
'
button
'
);
expect
(
todoEl
.
exists
()).
toBe
(
true
);
expect
(
todoEl
.
attributes
(
'
title
'
)).
toBe
(
'
Add a to do
'
);
expect
(
todoEl
.
find
(
GlIcon
).
exists
()).
toBe
(
true
);
});
it
(
'
renders label-select
'
,
async
()
=>
{
const
{
selectedLabels
,
testCaseLabelsSelectInProgress
}
=
wrapper
.
vm
;
const
{
canEditTestCase
,
labelsFetchPath
,
labelsManagePath
}
=
mockProvide
;
const
labelSelectEl
=
wrapper
.
find
(
LabelsSelect
);
expect
(
labelSelectEl
.
exists
()).
toBe
(
true
);
expect
(
labelSelectEl
.
props
()).
toMatchObject
({
selectedLabels
,
labelsFetchPath
,
labelsManagePath
,
allowLabelCreate
:
true
,
allowMultiselect
:
true
,
variant
:
'
sidebar
'
,
allowLabelEdit
:
canEditTestCase
,
labelsSelectInProgress
:
testCaseLabelsSelectInProgress
,
});
expect
(
labelSelectEl
.
text
()).
toBe
(
'
None
'
);
});
});
});
ee/spec/frontend/test_case_show/mixins/test_case_graphql_spec.js
0 → 100644
View file @
da977f43
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
mockCurrentUserTodo
}
from
'
jest/issuable_list/mock_data
'
;
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
markTestCaseTodoDone
from
'
ee/test_case_show/queries/mark_test_case_todo_done.mutation.graphql
'
;
import
createFlash
from
'
~/flash
'
;
import
Api
from
'
~/api
'
;
import
{
mockProvide
,
mockTestCase
}
from
'
../mock_data
'
;
jest
.
mock
(
'
~/flash
'
);
const
createComponent
=
({
testCase
,
testCaseQueryLoading
=
false
}
=
{})
=>
shallowMount
(
TestCaseShowRoot
,
{
provide
:
{
...
mockProvide
,
},
mocks
:
{
$apollo
:
{
queries
:
{
testCase
:
{
loading
:
testCaseQueryLoading
,
refetch
:
jest
.
fn
(),
},
},
mutate
:
jest
.
fn
(),
},
},
data
()
{
return
{
testCaseLoading
:
testCaseQueryLoading
,
testCase
:
testCaseQueryLoading
?
{}
:
{
...
mockTestCase
,
...
testCase
,
},
};
},
});
describe
(
'
TestCaseGraphQL Mixin
'
,
()
=>
{
let
wrapper
;
beforeEach
(()
=>
{
wrapper
=
createComponent
();
});
afterEach
(()
=>
{
wrapper
.
destroy
();
});
describe
(
'
updateTestCase
'
,
()
=>
{
it
(
'
calls `$apollo.mutate` with updateTestCase mutation and updateTestCaseInput variables
'
,
()
=>
{
jest
.
spyOn
(
wrapper
.
vm
.
$apollo
,
'
mutate
'
).
mockResolvedValue
({
data
:
{
updateIssue
:
{
errors
:
[],
issue
:
mockTestCase
,
},
},
});
wrapper
.
vm
.
updateTestCase
({
variables
:
{
title
:
'
Foo
'
,
},
errorMessage
:
'
Something went wrong
'
,
});
expect
(
wrapper
.
vm
.
$apollo
.
mutate
).
toHaveBeenCalledWith
({
mutation
:
updateTestCase
,
variables
:
{
updateTestCaseInput
:
{
projectPath
:
mockProvide
.
projectFullPath
,
iid
:
mockProvide
.
testCaseId
,
title
:
'
Foo
'
,
},
},
});
});
it
(
'
calls `createFlash` with errorMessage on promise reject
'
,
()
=>
{
jest
.
spyOn
(
wrapper
.
vm
.
$apollo
,
'
mutate
'
).
mockRejectedValue
({});
return
wrapper
.
vm
.
updateTestCase
({
variables
:
{
title
:
'
Foo
'
,
},
errorMessage
:
'
Something went wrong
'
,
})
.
then
(()
=>
{
expect
(
createFlash
).
toHaveBeenCalledWith
({
message
:
'
Something went wrong
'
,
captureError
:
true
,
error
:
expect
.
any
(
Object
),
});
});
});
});
describe
(
'
addTestCaseAsTodo
'
,
()
=>
{
it
(
'
sets `testCaseTodoUpdateInProgress` to true
'
,
()
=>
{
jest
.
spyOn
(
Api
,
'
addProjectIssueAsTodo
'
).
mockResolvedValue
({});
wrapper
.
vm
.
addTestCaseAsTodo
();
expect
(
wrapper
.
vm
.
testCaseTodoUpdateInProgress
).
toBe
(
true
);
});
it
(
'
calls `Api.addProjectIssueAsTodo` method with params `projectFullPath` and `testCaseId`
'
,
()
=>
{
jest
.
spyOn
(
Api
,
'
addProjectIssueAsTodo
'
).
mockResolvedValue
({});
wrapper
.
vm
.
addTestCaseAsTodo
();
expect
(
Api
.
addProjectIssueAsTodo
).
toHaveBeenCalledWith
(
mockProvide
.
projectFullPath
,
mockProvide
.
testCaseId
,
);
});
it
(
'
calls `$apollo.queries.testCase.refetch` method on request promise resolve
'
,
()
=>
{
jest
.
spyOn
(
Api
,
'
addProjectIssueAsTodo
'
).
mockResolvedValue
({});
jest
.
spyOn
(
wrapper
.
vm
.
$apollo
.
queries
.
testCase
,
'
refetch
'
);
return
wrapper
.
vm
.
addTestCaseAsTodo
().
then
(()
=>
{
expect
(
wrapper
.
vm
.
$apollo
.
queries
.
testCase
.
refetch
).
toHaveBeenCalled
();
});
});
it
(
'
calls `createFlash` method on request promise reject
'
,
()
=>
{
jest
.
spyOn
(
Api
,
'
addProjectIssueAsTodo
'
).
mockRejectedValue
({});
return
wrapper
.
vm
.
addTestCaseAsTodo
().
then
(()
=>
{
expect
(
createFlash
).
toHaveBeenCalledWith
({
message
:
'
Something went wrong while adding test case to Todo.
'
,
captureError
:
true
,
error
:
expect
.
any
(
Object
),
});
});
});
it
(
'
sets `testCaseTodoUpdateInProgress` to false on request promise resolve or reject
'
,
()
=>
{
jest
.
spyOn
(
Api
,
'
addProjectIssueAsTodo
'
).
mockRejectedValue
({});
return
wrapper
.
vm
.
addTestCaseAsTodo
().
finally
(()
=>
{
expect
(
wrapper
.
vm
.
testCaseTodoUpdateInProgress
).
toBe
(
false
);
});
});
});
describe
(
'
markTestCaseTodoDone
'
,
()
=>
{
const
todoResolvedMutation
=
{
data
:
{
todoMarkDone
:
{
errors
:
[],
},
},
};
it
(
'
sets `testCaseTodoUpdateInProgress` to true
'
,
()
=>
{
jest
.
spyOn
(
wrapper
.
vm
.
$apollo
,
'
mutate
'
).
mockResolvedValue
(
todoResolvedMutation
);
wrapper
.
vm
.
markTestCaseTodoDone
();
expect
(
wrapper
.
vm
.
testCaseTodoUpdateInProgress
).
toBe
(
true
);
});
it
(
'
calls `$apollo.mutate` with markTestCaseTodoDone mutation and todoMarkDoneInput variables
'
,
()
=>
{
jest
.
spyOn
(
wrapper
.
vm
.
$apollo
,
'
mutate
'
).
mockResolvedValue
(
todoResolvedMutation
);
wrapper
.
vm
.
markTestCaseTodoDone
();
expect
(
wrapper
.
vm
.
$apollo
.
mutate
).
toHaveBeenCalledWith
({
mutation
:
markTestCaseTodoDone
,
variables
:
{
todoMarkDoneInput
:
{
id
:
mockCurrentUserTodo
.
id
,
},
},
});
});
it
(
'
calls `$apollo.queries.testCase.refetch` on mutation promise resolve
'
,
()
=>
{
jest
.
spyOn
(
wrapper
.
vm
.
$apollo
,
'
mutate
'
).
mockResolvedValue
(
todoResolvedMutation
);
jest
.
spyOn
(
wrapper
.
vm
.
$apollo
.
queries
.
testCase
,
'
refetch
'
);
return
wrapper
.
vm
.
markTestCaseTodoDone
().
then
(()
=>
{
expect
(
wrapper
.
vm
.
$apollo
.
queries
.
testCase
.
refetch
).
toHaveBeenCalled
();
});
});
it
(
'
calls `createFlash` with errorMessage on mutation promise reject
'
,
()
=>
{
jest
.
spyOn
(
wrapper
.
vm
.
$apollo
,
'
mutate
'
).
mockRejectedValue
({});
return
wrapper
.
vm
.
markTestCaseTodoDone
().
then
(()
=>
{
expect
(
createFlash
).
toHaveBeenCalledWith
({
message
:
'
Something went wrong while marking test case todo as done.
'
,
captureError
:
true
,
error
:
expect
.
any
(
Object
),
});
});
});
it
(
'
sets `testCaseTodoUpdateInProgress` to false on mutation promise resolve or reject
'
,
()
=>
{
jest
.
spyOn
(
wrapper
.
vm
.
$apollo
,
'
mutate
'
).
mockResolvedValue
(
todoResolvedMutation
);
return
wrapper
.
vm
.
markTestCaseTodoDone
().
finally
(()
=>
{
expect
(
wrapper
.
vm
.
testCaseTodoUpdateInProgress
).
toBe
(
false
);
});
});
});
});
ee/spec/frontend/test_case_show/mock_data.js
0 → 100644
View file @
da977f43
import
{
mockIssuable
,
mockCurrentUserTodo
}
from
'
jest/issuable_list/mock_data
'
;
export
const
mockTestCase
=
{
...
mockIssuable
,
currentUserTodos
:
{
nodes
:
[
mockCurrentUserTodo
],
},
};
export
const
mockProvide
=
{
projectFullPath
:
'
gitlab-org/gitlab-test
'
,
testCaseNewPath
:
'
/gitlab-org/gitlab-test/-/quality/test_cases/new
'
,
testCaseId
:
mockIssuable
.
iid
,
canEditTestCase
:
true
,
descriptionPreviewPath
:
'
/gitlab-org/gitlab-test/preview_markdown
'
,
descriptionHelpPath
:
'
/help/user/markdown
'
,
labelsFetchPath
:
'
/gitlab-org/gitlab-test/-/labels.json
'
,
labelsManagePath
:
'
/gitlab-org/gitlab-shell/-/labels
'
,
};
locale/gitlab.pot
View file @
da977f43
...
...
@@ -3425,6 +3425,9 @@ msgstr ""
msgid "Archive project"
msgstr ""
msgid "Archive test case"
msgstr ""
msgid "Archived"
msgstr ""
...
...
@@ -22070,6 +22073,9 @@ msgstr ""
msgid "Reopen milestone"
msgstr ""
msgid "Reopen test case"
msgstr ""
msgid "Reopen this %{quick_action_target}"
msgstr ""
...
...
@@ -25897,15 +25903,30 @@ msgstr ""
msgid "TestCases|Search test cases"
msgstr ""
msgid "TestCases|Something went wrong while adding test case to Todo."
msgstr ""
msgid "TestCases|Something went wrong while creating a test case."
msgstr ""
msgid "TestCases|Something went wrong while fetching count of test cases."
msgstr ""
msgid "TestCases|Something went wrong while fetching test case."
msgstr ""
msgid "TestCases|Something went wrong while fetching test cases list."
msgstr ""
msgid "TestCases|Something went wrong while marking test case todo as done."
msgstr ""
msgid "TestCases|Something went wrong while updating the test case labels."
msgstr ""
msgid "TestCases|Something went wrong while updating the test case."
msgstr ""
msgid "TestCases|Submit test case"
msgstr ""
...
...
spec/frontend/api_spec.js
View file @
da977f43
...
...
@@ -421,6 +421,25 @@ describe('Api', () => {
});
});
describe
(
'
addProjectIssueAsTodo
'
,
()
=>
{
it
(
'
adds issue ID as a todo
'
,
()
=>
{
const
projectId
=
1
;
const
issueIid
=
11
;
const
expectedUrl
=
`
${
dummyUrlRoot
}
/api/
${
dummyApiVersion
}
/projects/1/issues/11/todo`
;
mock
.
onPost
(
expectedUrl
).
reply
(
200
,
{
id
:
112
,
project
:
{
id
:
1
,
},
});
return
Api
.
addProjectIssueAsTodo
(
projectId
,
issueIid
).
then
(({
data
})
=>
{
expect
(
data
.
id
).
toBe
(
112
);
expect
(
data
.
project
.
id
).
toBe
(
projectId
);
});
});
});
describe
(
'
newLabel
'
,
()
=>
{
it
(
'
creates a new label
'
,
done
=>
{
const
namespace
=
'
some namespace
'
;
...
...
spec/frontend/issuable_list/mock_data.js
View file @
da977f43
...
...
@@ -30,13 +30,23 @@ export const mockScopedLabel = {
export
const
mockLabels
=
[
mockRegularLabel
,
mockScopedLabel
];
export
const
mockCurrentUserTodo
=
{
id
:
'
gid://gitlab/Todo/489
'
,
state
:
'
done
'
,
};
export
const
mockIssuable
=
{
iid
:
'
30
'
,
title
:
'
Dismiss Cipher with no integrity
'
,
description
:
null
,
titleHtml
:
'
Dismiss Cipher with no integrity
'
,
description
:
'
fortitudinis _fomentis_ dolor mitigari solet.
'
,
descriptionHtml
:
'
fortitudinis <i>fomentis</i> dolor mitigari solet.
'
,
state
:
'
opened
'
,
createdAt
:
'
2020-06-29T13:52:56Z
'
,
updatedAt
:
'
2020-09-10T11:41:13Z
'
,
webUrl
:
'
http://0.0.0.0:3000/gitlab-org/gitlab-shell/-/issues/30
'
,
blocked
:
false
,
confidential
:
false
,
author
:
mockAuthor
,
labels
:
{
nodes
:
mockLabels
,
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment