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
7ec3d2c2
Commit
7ec3d2c2
authored
Jun 01, 2021
by
Florie Guibert
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Sidebar iteration widget refactor
Use generic dropdown sidebar widget for iterations
parent
b284e889
Changes
13
Show whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
27 additions
and
816 deletions
+27
-816
app/assets/javascripts/boards/components/board_content_sidebar.vue
...s/javascripts/boards/components/board_content_sidebar.vue
+5
-4
ee/app/assets/javascripts/sidebar/components/sidebar_iteration_widget.vue
...vascripts/sidebar/components/sidebar_iteration_widget.vue
+0
-296
ee/app/assets/javascripts/sidebar/mount_sidebar.js
ee/app/assets/javascripts/sidebar/mount_sidebar.js
+4
-4
ee/app/assets/javascripts/sidebar/queries/group_iterations.query.graphql
...avascripts/sidebar/queries/group_iterations.query.graphql
+1
-1
ee/app/assets/javascripts/sidebar/queries/project_issue_iteration.mutation.graphql
.../sidebar/queries/project_issue_iteration.mutation.graphql
+4
-4
ee/app/assets/javascripts/sidebar/queries/project_issue_iteration.query.graphql
...pts/sidebar/queries/project_issue_iteration.query.graphql
+1
-1
ee/spec/features/boards/boards_licensed_features_spec.rb
ee/spec/features/boards/boards_licensed_features_spec.rb
+1
-1
ee/spec/features/issues/issue_sidebar_spec.rb
ee/spec/features/issues/issue_sidebar_spec.rb
+1
-1
ee/spec/frontend/boards/components/__snapshots__/board_content_sidebar_spec.js.snap
...mponents/__snapshots__/board_content_sidebar_spec.js.snap
+5
-2
ee/spec/frontend/boards/components/board_content_sidebar_spec.js
.../frontend/boards/components/board_content_sidebar_spec.js
+0
-1
ee/spec/frontend/sidebar/components/sidebar_iteration_widget_spec.js
...ntend/sidebar/components/sidebar_iteration_widget_spec.js
+0
-491
ee/spec/support/shared_examples/features/sidebar_shared_examples.rb
...pport/shared_examples/features/sidebar_shared_examples.rb
+5
-5
qa/qa/ee/page/project/issue/show.rb
qa/qa/ee/page/project/issue/show.rb
+0
-5
No files found.
app/assets/javascripts/boards/components/board_content_sidebar.vue
View file @
7ec3d2c2
...
...
@@ -26,8 +26,6 @@ export default {
BoardSidebarMilestoneSelect
,
BoardSidebarWeightInput
:
()
=>
import
(
'
ee_component/boards/components/sidebar/board_sidebar_weight_input.vue
'
),
SidebarIterationWidget
:
()
=>
import
(
'
ee_component/sidebar/components/sidebar_iteration_widget.vue
'
),
SidebarDropdownWidget
:
()
=>
import
(
'
ee_component/sidebar/components/sidebar_dropdown_widget.vue
'
),
},
...
...
@@ -100,13 +98,16 @@ export default {
/>
<div>
<board-sidebar-milestone-select
/>
<sidebar-
iteratio
n-widget
<sidebar-
dropdow
n-widget
v-if=
"iterationFeatureAvailable"
:iid=
"activeBoardItem.iid"
issuable-attribute=
"iteration"
:workspace-path=
"projectPathForActiveIssue"
:
iterations
-workspace-path=
"groupPathForActiveIssue"
:
attr
-workspace-path=
"groupPathForActiveIssue"
:issuable-type=
"issuableType"
class=
"gl-mt-5"
data-testid=
"iteration-edit"
data-qa-selector=
"iteration_container"
/>
</div>
<board-sidebar-time-tracker
class=
"swimlanes-sidebar-time-tracker"
/>
...
...
ee/app/assets/javascripts/sidebar/components/sidebar_iteration_widget.vue
deleted
100644 → 0
View file @
b284e889
<
script
>
import
{
GlLink
,
GlDropdown
,
GlDropdownItem
,
GlDropdownText
,
GlSearchBoxByType
,
GlDropdownDivider
,
GlLoadingIcon
,
GlIcon
,
GlTooltipDirective
,
}
from
'
@gitlab/ui
'
;
import
*
as
Sentry
from
'
@sentry/browser
'
;
import
createFlash
from
'
~/flash
'
;
import
{
IssuableType
}
from
'
~/issue_show/constants
'
;
import
{
__
}
from
'
~/locale
'
;
import
SidebarEditableItem
from
'
~/sidebar/components/sidebar_editable_item.vue
'
;
import
{
iterationSelectTextMap
,
iterationDisplayState
,
noIteration
,
issuableIterationQueries
,
iterationsQueries
,
}
from
'
../constants
'
;
export
default
{
noIteration
,
i18n
:
{
iteration
:
iterationSelectTextMap
.
iteration
,
noIteration
:
iterationSelectTextMap
.
noIteration
,
assignIteration
:
iterationSelectTextMap
.
assignIteration
,
iterationSelectFail
:
iterationSelectTextMap
.
iterationSelectFail
,
noIterationsFound
:
iterationSelectTextMap
.
noIterationsFound
,
currentIterationFetchError
:
iterationSelectTextMap
.
currentIterationFetchError
,
iterationsFetchError
:
iterationSelectTextMap
.
iterationsFetchError
,
edit
:
__
(
'
Edit
'
),
none
:
__
(
'
None
'
),
},
issuableIterationQueries
,
iterationsQueries
,
tracking
:
{
label
:
'
right_sidebar
'
,
property
:
'
iteration
'
,
event
:
'
click_edit_button
'
,
},
directives
:
{
GlTooltip
:
GlTooltipDirective
,
},
components
:
{
SidebarEditableItem
,
GlLink
,
GlDropdown
,
GlDropdownItem
,
GlDropdownText
,
GlDropdownDivider
,
GlSearchBoxByType
,
GlIcon
,
GlLoadingIcon
,
},
inject
:
{
isClassicSidebar
:
{
default
:
false
,
},
},
props
:
{
workspacePath
:
{
required
:
true
,
type
:
String
,
},
iid
:
{
required
:
true
,
type
:
String
,
},
iterationsWorkspacePath
:
{
required
:
true
,
type
:
String
,
},
issuableType
:
{
type
:
String
,
required
:
true
,
validator
(
value
)
{
// Add supported IssuableType here along with graphql queries
// as this widget is used for addtional issuable types.
return
[
IssuableType
.
Issue
].
includes
(
value
);
},
},
},
apollo
:
{
currentIteration
:
{
query
()
{
return
issuableIterationQueries
[
this
.
issuableType
].
query
;
},
variables
()
{
return
{
fullPath
:
this
.
workspacePath
,
iid
:
this
.
iid
,
};
},
update
(
data
)
{
return
data
?.
workspace
?.
issuable
.
iteration
;
},
error
(
error
)
{
createFlash
({
message
:
this
.
$options
.
i18n
.
currentIterationFetchError
});
Sentry
.
captureException
(
error
);
},
},
iterations
:
{
query
()
{
return
iterationsQueries
[
this
.
issuableType
].
query
;
},
skip
()
{
return
!
this
.
editing
;
},
debounce
:
250
,
variables
()
{
return
{
fullPath
:
this
.
iterationsWorkspacePath
,
title
:
this
.
searchTerm
,
state
:
iterationDisplayState
,
};
},
update
(
data
)
{
return
data
?.
workspace
?.
iterations
.
nodes
||
[];
},
error
(
error
)
{
createFlash
({
message
:
this
.
$options
.
i18n
.
iterationsFetchError
});
Sentry
.
captureException
(
error
);
},
},
},
data
()
{
return
{
searchTerm
:
''
,
editing
:
false
,
updating
:
false
,
selectedTitle
:
null
,
currentIteration
:
null
,
iterations
:
[],
};
},
computed
:
{
iteration
()
{
return
this
.
iterations
.
find
(({
id
})
=>
id
===
this
.
currentIteration
);
},
iterationTitle
()
{
return
this
.
currentIteration
?.
title
;
},
iterationUrl
()
{
return
this
.
currentIteration
?.
webUrl
;
},
dropdownText
()
{
return
this
.
currentIteration
?
this
.
currentIteration
?.
title
:
this
.
$options
.
i18n
.
iteration
;
},
loading
()
{
return
this
.
$apollo
.
queries
.
currentIteration
.
loading
;
},
noIterations
()
{
return
this
.
iterations
.
length
===
0
;
},
},
methods
:
{
updateIteration
(
iterationId
)
{
if
(
this
.
currentIteration
===
null
&&
iterationId
===
null
)
return
;
if
(
iterationId
===
this
.
currentIteration
?.
id
)
return
;
this
.
updating
=
true
;
const
selectedIteration
=
this
.
iterations
.
find
((
i
)
=>
i
.
id
===
iterationId
);
this
.
selectedTitle
=
selectedIteration
?
selectedIteration
.
title
:
this
.
$options
.
i18n
.
none
;
this
.
$apollo
.
mutate
({
mutation
:
issuableIterationQueries
[
this
.
issuableType
].
mutation
,
variables
:
{
fullPath
:
this
.
workspacePath
,
iterationId
,
iid
:
this
.
iid
,
},
})
.
then
(({
data
})
=>
{
if
(
data
.
issuableSetIteration
?.
errors
?.
length
)
{
createFlash
(
data
.
issuableSetIteration
.
errors
[
0
]);
Sentry
.
captureException
(
data
.
issuableSetIteration
.
errors
[
0
]);
}
else
{
this
.
$emit
(
'
iteration-updated
'
,
data
);
}
})
.
catch
((
error
)
=>
{
createFlash
(
this
.
$options
.
i18n
.
iterationSelectFail
);
Sentry
.
captureException
(
error
);
})
.
finally
(()
=>
{
this
.
updating
=
false
;
this
.
searchTerm
=
''
;
this
.
selectedTitle
=
null
;
});
},
isIterationChecked
(
iterationId
=
undefined
)
{
return
(
iterationId
===
this
.
currentIteration
?.
id
||
(
!
this
.
currentIteration
?.
id
&&
!
iterationId
)
);
},
showDropdown
()
{
this
.
$refs
.
newDropdown
.
show
();
},
handleOpen
()
{
this
.
editing
=
true
;
this
.
showDropdown
();
},
handleClose
()
{
this
.
editing
=
false
;
},
setFocus
()
{
this
.
$refs
.
search
.
focusInput
();
},
},
};
</
script
>
<
template
>
<div
data-qa-selector=
"iteration_container"
>
<sidebar-editable-item
ref=
"editable"
:title=
"$options.i18n.iteration"
data-testid=
"iteration-edit-link"
:tracking=
"$options.tracking"
:loading=
"updating || loading"
@
open=
"handleOpen"
@
close=
"handleClose"
>
<template
#collapsed
>
<div
v-if=
"isClassicSidebar"
v-gl-tooltip
class=
"sidebar-collapsed-icon"
>
<gl-icon
:size=
"16"
:aria-label=
"$options.i18n.iteration"
name=
"iteration"
/>
<span
class=
"collapse-truncated-title"
>
{{
iterationTitle
}}
</span>
</div>
<div
data-testid=
"select-iteration"
:class=
"isClassicSidebar ? 'hide-collapsed' : 'gl-mt-3'"
>
<strong
v-if=
"updating"
>
{{
selectedTitle
}}
</strong>
<span
v-else-if=
"!updating && !currentIteration"
class=
"gl-text-gray-500"
>
{{
$options
.
i18n
.
none
}}
</span>
<gl-link
v-else
data-qa-selector=
"iteration_link"
class=
"gl-text-gray-900! gl-font-weight-bold"
:href=
"iterationUrl"
><strong>
{{
iterationTitle
}}
</strong></gl-link
>
</div>
</
template
>
<
template
#default
>
<gl-dropdown
ref=
"newDropdown"
lazy
:header-text=
"$options.i18n.assignIteration"
:text=
"dropdownText"
:loading=
"loading"
class=
"gl-w-full"
@
shown=
"setFocus"
>
<gl-search-box-by-type
ref=
"search"
v-model=
"searchTerm"
/>
<gl-dropdown-item
data-testid=
"no-iteration-item"
:is-check-item=
"true"
:is-checked=
"isIterationChecked($options.noIteration)"
@
click=
"updateIteration($options.noIteration)"
>
{{
$options
.
i18n
.
noIteration
}}
</gl-dropdown-item>
<gl-dropdown-divider
/>
<gl-loading-icon
v-if=
"$apollo.queries.iterations.loading"
class=
"gl-py-4"
data-testid=
"loading-icon-dropdown"
/>
<template
v-else
>
<gl-dropdown-text
v-if=
"noIterations"
>
{{
$options
.
i18n
.
noIterationsFound
}}
</gl-dropdown-text>
<gl-dropdown-item
v-for=
"iterationItem in iterations"
:key=
"iterationItem.id"
:is-check-item=
"true"
:is-checked=
"isIterationChecked(iterationItem.id)"
data-testid=
"iteration-items"
@
click=
"updateIteration(iterationItem.id)"
>
{{
iterationItem
.
title
}}
</gl-dropdown-item
>
</
template
>
</gl-dropdown>
</template>
</sidebar-editable-item>
</div>
</template>
ee/app/assets/javascripts/sidebar/mount_sidebar.js
View file @
7ec3d2c2
...
...
@@ -7,7 +7,6 @@ import { apolloProvider } from '~/sidebar/graphql';
import
*
as
CEMountSidebar
from
'
~/sidebar/mount_sidebar
'
;
import
CveIdRequest
from
'
./components/cve_id_request/cve_id_request_sidebar.vue
'
;
import
SidebarDropdownWidget
from
'
./components/sidebar_dropdown_widget.vue
'
;
import
SidebarIterationWidget
from
'
./components/sidebar_iteration_widget.vue
'
;
import
SidebarStatus
from
'
./components/status/sidebar_status.vue
'
;
import
SidebarWeight
from
'
./components/weight/sidebar_weight.vue
'
;
import
{
IssuableAttributeType
}
from
'
./constants
'
;
...
...
@@ -118,19 +117,20 @@ function mountIterationSelect() {
el
,
apolloProvider
,
components
:
{
Sidebar
Iteratio
nWidget
,
Sidebar
Dropdow
nWidget
,
},
provide
:
{
canUpdate
:
parseBoolean
(
canEdit
),
isClassicSidebar
:
true
,
},
render
:
(
createElement
)
=>
createElement
(
'
sidebar-
iteratio
n-widget
'
,
{
createElement
(
'
sidebar-
dropdow
n-widget
'
,
{
props
:
{
iterations
WorkspacePath
:
groupPath
,
attr
WorkspacePath
:
groupPath
,
workspacePath
:
projectPath
,
iid
:
issueIid
,
issuableType
:
IssuableType
.
Issue
,
issuableAttribute
:
IssuableAttributeType
.
Iteration
,
},
}),
});
...
...
ee/app/assets/javascripts/sidebar/queries/group_iterations.query.graphql
View file @
7ec3d2c2
...
...
@@ -3,7 +3,7 @@
query
issueIterations
(
$fullPath
:
ID
!,
$title
:
String
,
$state
:
IterationState
)
{
workspace
:
group
(
fullPath
:
$fullPath
)
{
__typename
iterations
(
title
:
$title
,
state
:
$state
)
{
attributes
:
iterations
(
title
:
$title
,
state
:
$state
)
{
nodes
{
...
IterationFragment
state
...
...
ee/app/assets/javascripts/sidebar/queries/project_issue_iteration.mutation.graphql
View file @
7ec3d2c2
mutation
projectIssueIterationMutation
(
$fullPath
:
ID
!,
$iid
:
String
!,
$
iteration
Id
:
ID
)
{
issuableSet
Iteration
:
issueSetIteration
(
input
:
{
projectPath
:
$fullPath
,
iid
:
$iid
,
iterationId
:
$
iteration
Id
}
mutation
projectIssueIterationMutation
(
$fullPath
:
ID
!,
$iid
:
String
!,
$
attribute
Id
:
ID
)
{
issuableSet
Attribute
:
issueSetIteration
(
input
:
{
projectPath
:
$fullPath
,
iid
:
$iid
,
iterationId
:
$
attribute
Id
}
)
{
__typename
errors
issuable
:
issue
{
__typename
id
iteration
{
attribute
:
iteration
{
title
id
state
...
...
ee/app/assets/javascripts/sidebar/queries/project_issue_iteration.query.graphql
View file @
7ec3d2c2
...
...
@@ -6,7 +6,7 @@ query projectIssueIteration($fullPath: ID!, $iid: String!) {
issuable
:
issue
(
iid
:
$iid
)
{
__typename
id
iteration
{
attribute
:
iteration
{
...
IterationFragment
}
}
...
...
ee/spec/features/boards/boards_licensed_features_spec.rb
View file @
7ec3d2c2
...
...
@@ -27,7 +27,7 @@ RSpec.describe 'Boards licensed features', :js do
end
it
"hides iteration widget"
do
expect
(
page
).
not_to
have_selector
(
'[data-testid="iteration-edit
-link
"]'
)
expect
(
page
).
not_to
have_selector
(
'[data-testid="iteration-edit"]'
)
end
end
...
...
ee/spec/features/issues/issue_sidebar_spec.rb
View file @
7ec3d2c2
...
...
@@ -206,7 +206,7 @@ RSpec.describe 'Issue Sidebar' do
end
def
find_and_click_edit_iteration
page
.
find
(
'[data-testid="iteration-edit
-link
"] [data-testid="edit-button"]'
).
click
page
.
find
(
'[data-testid="iteration-edit"] [data-testid="edit-button"]'
).
click
wait_for_all_requests
end
...
...
ee/spec/frontend/boards/components/__snapshots__/board_content_sidebar_spec.js.snap
View file @
7ec3d2c2
...
...
@@ -24,11 +24,14 @@ exports[`ee/BoardContentSidebar matches the snapshot 1`] = `
<div>
<boardsidebarmilestoneselect-stub />
<sidebariterationwidget-stub
<sidebardropdownwidget-stub
attr-workspace-path="gitlab-org"
class="gl-mt-5"
data-qa-selector="iteration_container"
data-testid="iteration-edit"
iid="27"
issuable-attribute="iteration"
issuable-type="issue"
iterations-workspace-path="gitlab-org"
workspace-path="gitlab-org/gitlab-test"
/>
</div>
...
...
ee/spec/frontend/boards/components/board_content_sidebar_spec.js
View file @
7ec3d2c2
...
...
@@ -63,7 +63,6 @@ describe('ee/BoardContentSidebar', () => {
SidebarSubscriptionsWidget
:
true
,
BoardSidebarMilestoneSelect
:
true
,
BoardSidebarWeightInput
:
true
,
SidebarIterationWidget
:
true
,
SidebarDropdownWidget
:
true
,
},
});
...
...
ee/spec/frontend/sidebar/components/sidebar_iteration_widget_spec.js
deleted
100644 → 0
View file @
b284e889
import
{
GlDropdown
,
GlDropdownItem
,
GlDropdownText
,
GlLink
,
GlSearchBoxByType
,
GlFormInput
,
GlLoadingIcon
,
}
from
'
@gitlab/ui
'
;
import
*
as
Sentry
from
'
@sentry/browser
'
;
import
{
createLocalVue
,
shallowMount
,
mount
}
from
'
@vue/test-utils
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
SidebarIterationWidget
from
'
ee/sidebar/components/sidebar_iteration_widget.vue
'
;
import
{
iterationSelectTextMap
,
iterationDisplayState
}
from
'
ee/sidebar/constants
'
;
import
groupIterationsQuery
from
'
ee/sidebar/queries/group_iterations.query.graphql
'
;
import
projectIssueIterationMutation
from
'
ee/sidebar/queries/project_issue_iteration.mutation.graphql
'
;
import
projectIssueIterationQuery
from
'
ee/sidebar/queries/project_issue_iteration.query.graphql
'
;
import
createMockApollo
from
'
helpers/mock_apollo_helper
'
;
import
{
extendedWrapper
}
from
'
helpers/vue_test_utils_helper
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
createFlash
from
'
~/flash
'
;
import
{
IssuableType
}
from
'
~/issue_show/constants
'
;
import
SidebarEditableItem
from
'
~/sidebar/components/sidebar_editable_item.vue
'
;
import
{
mockIssue
,
mockGroupIterationsResponse
,
mockIteration2
,
mockIterationMutationResponse
,
emptyGroupIterationsResponse
,
noCurrentIterationResponse
,
}
from
'
../mock_data
'
;
jest
.
mock
(
'
~/flash
'
);
const
localVue
=
createLocalVue
();
describe
(
'
SidebarIterationWidget
'
,
()
=>
{
let
wrapper
;
let
mockApollo
;
const
promiseData
=
{
issuableSetIteration
:
{
issue
:
{
iteration
:
{
id
:
'
123
'
}
}
}
};
const
firstErrorMsg
=
'
first error
'
;
const
promiseWithErrors
=
{
...
promiseData
,
issuableSetIteration
:
{
...
promiseData
.
issuableSetIteration
,
errors
:
[
firstErrorMsg
]
},
};
const
mutationSuccess
=
()
=>
jest
.
fn
().
mockResolvedValue
({
data
:
promiseData
});
const
mutationError
=
()
=>
jest
.
fn
().
mockRejectedValue
();
const
mutationSuccessWithErrors
=
()
=>
jest
.
fn
().
mockResolvedValue
({
data
:
promiseWithErrors
});
const
findGlLink
=
()
=>
wrapper
.
find
(
GlLink
);
const
findDropdown
=
()
=>
wrapper
.
find
(
GlDropdown
);
const
findDropdownText
=
()
=>
wrapper
.
find
(
GlDropdownText
);
const
findSearchBox
=
()
=>
wrapper
.
find
(
GlSearchBoxByType
);
const
findAllDropdownItems
=
()
=>
wrapper
.
findAll
(
GlDropdownItem
);
const
findDropdownItemWithText
=
(
text
)
=>
findAllDropdownItems
().
wrappers
.
find
((
x
)
=>
x
.
text
()
===
text
);
const
findSidebarEditableItem
=
()
=>
wrapper
.
findComponent
(
SidebarEditableItem
);
const
findEditButton
=
()
=>
findSidebarEditableItem
().
find
(
'
[data-testid="edit-button"]
'
);
const
findEditableLoadingIcon
=
()
=>
findSidebarEditableItem
().
find
(
GlLoadingIcon
);
const
findIterationItems
=
()
=>
wrapper
.
findByTestId
(
'
iteration-items
'
);
const
findSelectedIteration
=
()
=>
wrapper
.
findByTestId
(
'
select-iteration
'
);
const
findNoIterationItem
=
()
=>
wrapper
.
findByTestId
(
'
no-iteration-item
'
);
const
findLoadingIconDropdown
=
()
=>
wrapper
.
findByTestId
(
'
loading-icon-dropdown
'
);
const
waitForDropdown
=
async
()
=>
{
// BDropdown first changes its `visible` property
// in a requestAnimationFrame callback.
// It then emits `shown` event in a watcher for `visible`
// Hence we need both of these:
await
waitForPromises
();
await
wrapper
.
vm
.
$nextTick
();
};
const
waitForApollo
=
async
()
=>
{
jest
.
runOnlyPendingTimers
();
await
waitForPromises
();
};
// Used with createComponentWithApollo which uses 'mount'
const
clickEdit
=
async
()
=>
{
await
findEditButton
().
trigger
(
'
click
'
);
await
waitForDropdown
();
// We should wait for iterations list to be fetched.
await
waitForApollo
();
};
// Used with createComponent which shallow mounts components
const
toggleDropdown
=
async
()
=>
{
wrapper
.
vm
.
$refs
.
editable
.
expand
();
await
waitForDropdown
();
};
const
createComponentWithApollo
=
async
({
requestHandlers
=
[],
currentIterationSpy
=
jest
.
fn
().
mockResolvedValue
(
noCurrentIterationResponse
),
groupIterationsSpy
=
jest
.
fn
().
mockResolvedValue
(
mockGroupIterationsResponse
),
}
=
{})
=>
{
localVue
.
use
(
VueApollo
);
mockApollo
=
createMockApollo
([
[
projectIssueIterationQuery
,
currentIterationSpy
],
[
groupIterationsQuery
,
groupIterationsSpy
],
...
requestHandlers
,
]);
wrapper
=
extendedWrapper
(
mount
(
SidebarIterationWidget
,
{
localVue
,
provide
:
{
canUpdate
:
true
},
apolloProvider
:
mockApollo
,
propsData
:
{
workspacePath
:
mockIssue
.
projectPath
,
iterationsWorkspacePath
:
mockIssue
.
groupPath
,
iid
:
mockIssue
.
iid
,
issuableType
:
IssuableType
.
Issue
,
},
attachTo
:
document
.
body
,
}),
);
await
waitForApollo
();
};
const
createComponent
=
({
data
=
{},
mutationPromise
=
mutationSuccess
,
queries
=
{}
}
=
{})
=>
{
wrapper
=
extendedWrapper
(
shallowMount
(
SidebarIterationWidget
,
{
provide
:
{
canUpdate
:
true
},
data
()
{
return
data
;
},
propsData
:
{
workspacePath
:
''
,
iterationsWorkspacePath
:
''
,
iid
:
''
,
issuableType
:
IssuableType
.
Issue
,
},
mocks
:
{
$apollo
:
{
mutate
:
mutationPromise
(),
queries
:
{
currentIteration
:
{
loading
:
false
},
iterations
:
{
loading
:
false
},
...
queries
,
},
},
},
stubs
:
{
SidebarEditableItem
,
GlSearchBoxByType
,
GlDropdown
,
},
}),
);
// We need to mock out `showDropdown` which
// invokes `show` method of BDropdown used inside GlDropdown.
jest
.
spyOn
(
wrapper
.
vm
,
'
showDropdown
'
).
mockImplementation
();
};
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
=
null
;
});
describe
(
'
when not editing
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
({
data
:
{
currentIteration
:
{
id
:
'
id
'
,
title
:
'
title
'
,
webUrl
:
'
webUrl
'
},
},
stubs
:
{
GlDropdown
,
SidebarEditableItem
,
},
});
});
it
(
'
shows the current iteration
'
,
()
=>
{
expect
(
findSelectedIteration
().
text
()).
toBe
(
'
title
'
);
});
it
(
'
links to the current iteration
'
,
()
=>
{
expect
(
findGlLink
().
attributes
().
href
).
toBe
(
'
webUrl
'
);
});
it
(
'
does not show a loading spinner next to the iteration heading
'
,
()
=>
{
expect
(
findEditableLoadingIcon
().
exists
()).
toBe
(
false
);
});
it
(
'
shows a loading spinner while fetching the current iteration
'
,
()
=>
{
createComponent
({
queries
:
{
currentIteration
:
{
loading
:
true
},
},
});
expect
(
findEditableLoadingIcon
().
exists
()).
toBe
(
true
);
});
it
(
'
shows the title of the selected iteration while updating
'
,
()
=>
{
createComponent
({
data
:
{
updating
:
true
,
selectedTitle
:
'
Some iteration title
'
,
},
queries
:
{
currentIteration
:
{
loading
:
false
},
},
});
expect
(
findEditableLoadingIcon
().
exists
()).
toBe
(
true
);
expect
(
findSelectedIteration
().
text
()).
toBe
(
'
Some iteration title
'
);
});
describe
(
'
when current iteration does not exist
'
,
()
=>
{
it
(
'
renders "None" as the selected iteration title
'
,
()
=>
{
createComponent
();
expect
(
findSelectedIteration
().
text
()).
toBe
(
'
None
'
);
});
});
});
describe
(
'
when a user can edit
'
,
()
=>
{
describe
(
'
when user is editing
'
,
()
=>
{
describe
(
'
when rendering the dropdown
'
,
()
=>
{
it
(
'
shows a loading spinner while fetching a list of iterations
'
,
async
()
=>
{
createComponent
({
queries
:
{
iterations
:
{
loading
:
true
},
},
});
await
toggleDropdown
();
expect
(
findLoadingIconDropdown
().
exists
()).
toBe
(
true
);
});
describe
(
'
GlDropdownItem with the right title and id
'
,
()
=>
{
const
id
=
'
id
'
;
const
title
=
'
title
'
;
beforeEach
(
async
()
=>
{
createComponent
({
data
:
{
iterations
:
[{
id
,
title
}],
currentIteration
:
{
id
,
title
}
},
});
await
toggleDropdown
();
});
it
(
'
does not show a loading spinner
'
,
()
=>
{
expect
(
findLoadingIconDropdown
().
exists
()).
toBe
(
false
);
});
it
(
'
renders title $title
'
,
()
=>
{
expect
(
findDropdownItemWithText
(
title
).
text
()).
toBe
(
title
);
});
it
(
'
checks the correct dropdown item
'
,
()
=>
{
expect
(
findAllDropdownItems
()
.
filter
((
w
)
=>
w
.
props
(
'
isChecked
'
)
===
true
)
.
at
(
0
)
.
text
(),
).
toBe
(
title
);
});
});
describe
(
'
when no data is assigned
'
,
()
=>
{
beforeEach
(
async
()
=>
{
createComponent
();
await
toggleDropdown
();
});
it
(
'
finds GlDropdownItem with "No iteration"
'
,
()
=>
{
expect
(
findNoIterationItem
().
text
()).
toBe
(
'
No iteration
'
);
});
it
(
'
"No iteration" is checked
'
,
()
=>
{
expect
(
findNoIterationItem
().
props
(
'
isChecked
'
)).
toBe
(
true
);
});
it
(
'
does not render any dropdown item
'
,
()
=>
{
expect
(
findIterationItems
().
exists
()).
toBe
(
false
);
});
});
describe
(
'
when clicking on dropdown item
'
,
()
=>
{
describe
(
'
when currentIteration is equal to iteration id
'
,
()
=>
{
it
(
'
does not call setIssueIteration mutation
'
,
async
()
=>
{
createComponent
({
data
:
{
iterations
:
[{
id
:
'
id
'
,
title
:
'
title
'
}],
currentIteration
:
{
id
:
'
id
'
,
title
:
'
title
'
},
},
});
await
toggleDropdown
();
findDropdownItemWithText
(
'
title
'
).
vm
.
$emit
(
'
click
'
);
expect
(
wrapper
.
vm
.
$apollo
.
mutate
).
toHaveBeenCalledTimes
(
0
);
});
});
describe
(
'
when currentIteration is not equal to iteration id
'
,
()
=>
{
describe
(
'
when error
'
,
()
=>
{
const
bootstrapComponent
=
(
mutationResp
)
=>
{
createComponent
({
data
:
{
iterations
:
[
{
id
:
'
123
'
,
title
:
'
123
'
},
{
id
:
'
id
'
,
title
:
'
title
'
},
],
currentIteration
:
'
123
'
,
},
mutationPromise
:
mutationResp
,
});
};
describe
.
each
`
description | mutationResp | expectedMsg
${
'
top-level error
'
}
|
${
mutationError
}
|
${
iterationSelectTextMap
.
iterationSelectFail
}
${
'
user-recoverable error
'
}
|
${
mutationSuccessWithErrors
}
|
${
firstErrorMsg
}
`
(
`$description`
,
({
mutationResp
,
expectedMsg
})
=>
{
beforeEach
(
async
()
=>
{
bootstrapComponent
(
mutationResp
);
await
toggleDropdown
();
findDropdownItemWithText
(
'
title
'
).
vm
.
$emit
(
'
click
'
);
});
it
(
`calls createFlash with "
${
expectedMsg
}
"`
,
async
()
=>
{
await
wrapper
.
vm
.
$nextTick
();
expect
(
createFlash
).
toHaveBeenCalledWith
(
expectedMsg
);
});
});
});
});
});
});
describe
(
'
when a user is searching
'
,
()
=>
{
describe
(
'
when search result is not found
'
,
()
=>
{
it
(
'
renders "No iterations found"
'
,
async
()
=>
{
createComponent
();
await
toggleDropdown
();
findSearchBox
().
vm
.
$emit
(
'
input
'
,
'
non existing iterations
'
);
await
wrapper
.
vm
.
$nextTick
();
expect
(
findDropdownText
().
text
()).
toBe
(
'
No iterations found
'
);
});
});
});
});
});
describe
(
'
with mock apollo
'
,
()
=>
{
let
error
;
beforeEach
(()
=>
{
jest
.
spyOn
(
Sentry
,
'
captureException
'
);
error
=
new
Error
(
'
mayday
'
);
});
describe
(
"
when issuable type is 'issue'
"
,
()
=>
{
describe
(
'
when dropdown is expanded and user can edit
'
,
()
=>
{
let
iterationMutationSpy
;
beforeEach
(
async
()
=>
{
iterationMutationSpy
=
jest
.
fn
().
mockResolvedValue
(
mockIterationMutationResponse
);
await
createComponentWithApollo
({
requestHandlers
:
[[
projectIssueIterationMutation
,
iterationMutationSpy
]],
});
await
clickEdit
();
});
it
(
'
renders the dropdown on clicking edit
'
,
async
()
=>
{
expect
(
findDropdown
().
isVisible
()).
toBe
(
true
);
});
it
(
'
focuses on the input when dropdown is shown
'
,
async
()
=>
{
expect
(
document
.
activeElement
).
toEqual
(
wrapper
.
find
(
GlFormInput
).
element
);
});
describe
(
'
when currentIteration is not equal to iteration id
'
,
()
=>
{
describe
(
'
when update is successful
'
,
()
=>
{
beforeEach
(()
=>
{
findDropdownItemWithText
(
mockIteration2
.
title
).
vm
.
$emit
(
'
click
'
);
});
it
(
'
calls setIssueIteration mutation
'
,
()
=>
{
expect
(
iterationMutationSpy
).
toHaveBeenCalledWith
({
iid
:
mockIssue
.
iid
,
iterationId
:
mockIteration2
.
id
,
fullPath
:
mockIssue
.
projectPath
,
});
});
it
(
'
sets the value returned from the mutation to currentIteration
'
,
async
()
=>
{
expect
(
findSelectedIteration
().
text
()).
toBe
(
mockIteration2
.
title
);
});
});
});
describe
(
'
iterations
'
,
()
=>
{
let
groupIterationsSpy
;
it
(
'
should call createFlash and Sentry if iterations query fails
'
,
async
()
=>
{
await
createComponentWithApollo
({
groupIterationsSpy
:
jest
.
fn
().
mockRejectedValue
(
error
),
});
await
clickEdit
();
expect
(
createFlash
).
toHaveBeenNthCalledWith
(
1
,
{
message
:
wrapper
.
vm
.
$options
.
i18n
.
iterationsFetchError
,
});
expect
(
Sentry
.
captureException
.
mock
.
calls
[
0
][
0
].
networkError
).
toBe
(
error
);
});
it
(
'
only fetches iterations when dropdown is opened
'
,
async
()
=>
{
groupIterationsSpy
=
jest
.
fn
().
mockResolvedValueOnce
(
emptyGroupIterationsResponse
);
await
createComponentWithApollo
({
groupIterationsSpy
});
expect
(
groupIterationsSpy
).
not
.
toHaveBeenCalled
();
await
clickEdit
();
expect
(
groupIterationsSpy
).
toHaveBeenNthCalledWith
(
1
,
{
fullPath
:
mockIssue
.
groupPath
,
title
:
''
,
state
:
iterationDisplayState
,
});
});
describe
(
'
when a user is searching
'
,
()
=>
{
const
mockSearchTerm
=
'
foobar
'
;
beforeEach
(
async
()
=>
{
groupIterationsSpy
=
jest
.
fn
().
mockResolvedValueOnce
(
emptyGroupIterationsResponse
);
await
createComponentWithApollo
({
groupIterationsSpy
});
await
clickEdit
();
});
it
(
'
sends a groupIterations query with the entered search term "foo"
'
,
async
()
=>
{
findSearchBox
().
vm
.
$emit
(
'
input
'
,
mockSearchTerm
);
await
wrapper
.
vm
.
$nextTick
();
// Account for debouncing
jest
.
runAllTimers
();
expect
(
groupIterationsSpy
).
toHaveBeenNthCalledWith
(
2
,
{
fullPath
:
mockIssue
.
groupPath
,
title
:
mockSearchTerm
,
state
:
iterationDisplayState
,
});
});
});
});
});
describe
(
'
currentIterations
'
,
()
=>
{
it
(
'
should call createFlash and Sentry if currentIterations query fails
'
,
async
()
=>
{
await
createComponentWithApollo
({
currentIterationSpy
:
jest
.
fn
().
mockRejectedValue
(
error
),
});
expect
(
createFlash
).
toHaveBeenNthCalledWith
(
1
,
{
message
:
wrapper
.
vm
.
$options
.
i18n
.
currentIterationFetchError
,
});
expect
(
Sentry
.
captureException
.
mock
.
calls
[
0
][
0
].
networkError
).
toBe
(
error
);
});
});
});
});
});
ee/spec/support/shared_examples/features/sidebar_shared_examples.rb
View file @
7ec3d2c2
...
...
@@ -72,13 +72,13 @@ RSpec.shared_examples 'issue boards sidebar EE' do
select_iteration
(
iteration
.
title
)
expect
(
page
.
find
(
'[data-testid="iteration-edit
-link
"]'
)).
to
have_content
(
'Iteration 1'
)
expect
(
page
.
find
(
'[data-testid="iteration-edit"]'
)).
to
have_content
(
'Iteration 1'
)
find_and_click_edit_iteration
select_iteration
(
'No iteration'
)
expect
(
page
.
find
(
'[data-testid="iteration-edit
-link
"]'
)).
to
have_content
(
'None'
)
expect
(
page
.
find
(
'[data-testid="iteration-edit"]'
)).
to
have_content
(
'None'
)
end
context
'when iteration feature is not available'
do
...
...
@@ -90,15 +90,15 @@ RSpec.shared_examples 'issue boards sidebar EE' do
wait_for_all_requests
end
it
'cannot find the iteration-edit
-link
'
do
expect
(
page
).
not_to
have_selector
(
'[data-testid="iteration-edit
-link
"]'
)
it
'cannot find the iteration-edit'
do
expect
(
page
).
not_to
have_selector
(
'[data-testid="iteration-edit"]'
)
end
end
end
end
def
find_and_click_edit_iteration
page
.
find
(
'[data-testid="iteration-edit
-link
"] [data-testid="edit-button"]'
).
click
page
.
find
(
'[data-testid="iteration-edit"] [data-testid="edit-button"]'
).
click
wait_for_all_requests
end
...
...
qa/qa/ee/page/project/issue/show.rb
View file @
7ec3d2c2
...
...
@@ -16,11 +16,6 @@ module QA
element
:edit_link
end
view
'ee/app/assets/javascripts/sidebar/components/sidebar_iteration_widget.vue'
do
element
:iteration_container
element
:iteration_link
end
view
'ee/app/assets/javascripts/sidebar/components/weight/weight.vue'
do
element
:edit_weight_link
element
:remove_weight_link
...
...
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