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
84e30a1a
Commit
84e30a1a
authored
Sep 21, 2021
by
Zack Cuddy
Committed by
Nicolò Maria Mezzopera
Sep 21, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Global Search - Header Search Autocomplete
parent
f051dd16
Changes
18
Hide whitespace changes
Inline
Side-by-side
Showing
18 changed files
with
477 additions
and
19 deletions
+477
-19
app/assets/javascripts/header_search/components/app.vue
app/assets/javascripts/header_search/components/app.vue
+13
-2
app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue
...er_search/components/header_search_autocomplete_items.vue
+74
-0
app/assets/javascripts/header_search/constants.js
app/assets/javascripts/header_search/constants.js
+8
-0
app/assets/javascripts/header_search/index.js
app/assets/javascripts/header_search/index.js
+2
-2
app/assets/javascripts/header_search/store/actions.js
app/assets/javascripts/header_search/store/actions.js
+14
-0
app/assets/javascripts/header_search/store/getters.js
app/assets/javascripts/header_search/store/getters.js
+32
-0
app/assets/javascripts/header_search/store/index.js
app/assets/javascripts/header_search/store/index.js
+8
-2
app/assets/javascripts/header_search/store/mutation_types.js
app/assets/javascripts/header_search/store/mutation_types.js
+4
-0
app/assets/javascripts/header_search/store/mutations.js
app/assets/javascripts/header_search/store/mutations.js
+12
-0
app/assets/javascripts/header_search/store/state.js
app/assets/javascripts/header_search/store/state.js
+4
-1
app/views/layouts/header/_default.html.haml
app/views/layouts/header/_default.html.haml
+2
-1
locale/gitlab.pot
locale/gitlab.pot
+3
-0
spec/frontend/header_search/components/app_spec.js
spec/frontend/header_search/components/app_spec.js
+23
-9
spec/frontend/header_search/components/header_search_autocomplete_items_spec.js
...earch/components/header_search_autocomplete_items_spec.js
+108
-0
spec/frontend/header_search/mock_data.js
spec/frontend/header_search/mock_data.js
+69
-0
spec/frontend/header_search/store/actions_spec.js
spec/frontend/header_search/store/actions_spec.js
+33
-1
spec/frontend/header_search/store/getters_spec.js
spec/frontend/header_search/store/getters_spec.js
+40
-0
spec/frontend/header_search/store/mutations_spec.js
spec/frontend/header_search/store/mutations_spec.js
+28
-1
No files found.
app/assets/javascripts/header_search/components/app.vue
View file @
84e30a1a
...
...
@@ -3,6 +3,7 @@ import { GlSearchBoxByType, GlOutsideDirective as Outside } from '@gitlab/ui';
import
{
mapState
,
mapActions
,
mapGetters
}
from
'
vuex
'
;
import
{
visitUrl
}
from
'
~/lib/utils/url_utility
'
;
import
{
__
}
from
'
~/locale
'
;
import
HeaderSearchAutocompleteItems
from
'
./header_search_autocomplete_items.vue
'
;
import
HeaderSearchDefaultItems
from
'
./header_search_default_items.vue
'
;
import
HeaderSearchScopedItems
from
'
./header_search_scoped_items.vue
'
;
...
...
@@ -16,6 +17,7 @@ export default {
GlSearchBoxByType
,
HeaderSearchDefaultItems
,
HeaderSearchScopedItems
,
HeaderSearchAutocompleteItems
,
},
data
()
{
return
{
...
...
@@ -41,7 +43,7 @@ export default {
},
},
methods
:
{
...
mapActions
([
'
setSearch
'
]),
...
mapActions
([
'
setSearch
'
,
'
fetchAutocompleteOptions
'
]),
openDropdown
()
{
this
.
showDropdown
=
true
;
},
...
...
@@ -51,6 +53,13 @@ export default {
submitSearch
()
{
return
visitUrl
(
this
.
searchQuery
);
},
getAutocompleteOptions
(
searchTerm
)
{
if
(
!
searchTerm
)
{
return
;
}
this
.
fetchAutocompleteOptions
();
},
},
};
</
script
>
...
...
@@ -64,18 +73,20 @@ export default {
:placeholder=
"$options.i18n.searchPlaceholder"
@
focus=
"openDropdown"
@
click=
"openDropdown"
@
input=
"getAutocompleteOptions"
@
keydown.enter=
"submitSearch"
@
keydown.esc=
"closeDropdown"
/>
<div
v-if=
"showSearchDropdown"
data-testid=
"header-search-dropdown-menu"
class=
"header-search-dropdown-menu gl-
overflow-y-auto gl-absolute gl-left-0 gl-z-index-1
gl-w-full gl-bg-white gl-border-1 gl-rounded-base gl-border-solid gl-border-gray-200 gl-shadow-x0-y2-b4-s0"
class=
"header-search-dropdown-menu gl-
absolute
gl-w-full gl-bg-white gl-border-1 gl-rounded-base gl-border-solid gl-border-gray-200 gl-shadow-x0-y2-b4-s0"
>
<div
class=
"header-search-dropdown-content gl-overflow-y-auto gl-py-2"
>
<header-search-default-items
v-if=
"showDefaultItems"
/>
<template
v-else
>
<header-search-scoped-items
/>
<header-search-autocomplete-items
/>
</
template
>
</div>
</div>
...
...
app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue
0 → 100644
View file @
84e30a1a
<
script
>
import
{
GlDropdownItem
,
GlDropdownSectionHeader
,
GlDropdownDivider
,
GlAvatar
,
GlLoadingIcon
,
GlSafeHtmlDirective
as
SafeHtml
,
}
from
'
@gitlab/ui
'
;
import
{
mapState
,
mapGetters
}
from
'
vuex
'
;
import
highlight
from
'
~/lib/utils/highlight
'
;
import
{
GROUPS_CATEGORY
,
PROJECTS_CATEGORY
,
LARGE_AVATAR_PX
,
SMALL_AVATAR_PX
}
from
'
../constants
'
;
export
default
{
name
:
'
HeaderSearchAutocompleteItems
'
,
components
:
{
GlDropdownItem
,
GlDropdownSectionHeader
,
GlDropdownDivider
,
GlAvatar
,
GlLoadingIcon
,
},
directives
:
{
SafeHtml
,
},
computed
:
{
...
mapState
([
'
search
'
,
'
loading
'
]),
...
mapGetters
([
'
autocompleteGroupedSearchOptions
'
]),
},
methods
:
{
highlightedName
(
val
)
{
return
highlight
(
val
,
this
.
search
);
},
avatarSize
(
data
)
{
if
(
data
.
category
===
GROUPS_CATEGORY
||
data
.
category
===
PROJECTS_CATEGORY
)
{
return
LARGE_AVATAR_PX
;
}
return
SMALL_AVATAR_PX
;
},
},
};
</
script
>
<
template
>
<div>
<template
v-if=
"!loading"
>
<div
v-for=
"option in autocompleteGroupedSearchOptions"
:key=
"option.category"
>
<gl-dropdown-divider
/>
<gl-dropdown-section-header>
{{
option
.
category
}}
</gl-dropdown-section-header>
<gl-dropdown-item
v-for=
"(data, index) in option.data"
:id=
"`autocomplete-$
{option.category}-${index}`"
:key="index"
tabindex="-1"
:href="data.url"
>
<div
class=
"gl-display-flex gl-align-items-center"
>
<gl-avatar
v-if=
"data.avatar_url !== undefined"
:src=
"data.avatar_url"
:entity-id=
"data.id"
:entity-name=
"data.label"
:size=
"avatarSize(data)"
shape=
"square"
/>
<span
v-safe-html=
"highlightedName(data.label)"
></span>
</div>
</gl-dropdown-item>
</div>
</
template
>
<gl-loading-icon
v-else
size=
"lg"
class=
"my-4"
/>
</div>
</template>
app/assets/javascripts/header_search/constants.js
View file @
84e30a1a
...
...
@@ -15,3 +15,11 @@ export const MSG_IN_ALL_GITLAB = __('in all GitLab');
export
const
MSG_IN_GROUP
=
__
(
'
in group
'
);
export
const
MSG_IN_PROJECT
=
__
(
'
in project
'
);
export
const
GROUPS_CATEGORY
=
'
Groups
'
;
export
const
PROJECTS_CATEGORY
=
'
Projects
'
;
export
const
LARGE_AVATAR_PX
=
32
;
export
const
SMALL_AVATAR_PX
=
16
;
app/assets/javascripts/header_search/index.js
View file @
84e30a1a
...
...
@@ -12,13 +12,13 @@ export const initHeaderSearchApp = () => {
return
false
;
}
const
{
searchPath
,
issuesPath
,
mrPath
}
=
el
.
dataset
;
const
{
searchPath
,
issuesPath
,
mrPath
,
autocompletePath
}
=
el
.
dataset
;
let
{
searchContext
}
=
el
.
dataset
;
searchContext
=
JSON
.
parse
(
searchContext
);
return
new
Vue
({
el
,
store
:
createStore
({
searchPath
,
issuesPath
,
mrPath
,
searchContext
}),
store
:
createStore
({
searchPath
,
issuesPath
,
mrPath
,
autocompletePath
,
searchContext
}),
render
(
createElement
)
{
return
createElement
(
HeaderSearchApp
);
},
...
...
app/assets/javascripts/header_search/store/actions.js
View file @
84e30a1a
import
createFlash
from
'
~/flash
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
{
__
}
from
'
~/locale
'
;
import
*
as
types
from
'
./mutation_types
'
;
export
const
fetchAutocompleteOptions
=
({
commit
,
getters
})
=>
{
commit
(
types
.
REQUEST_AUTOCOMPLETE
);
return
axios
.
get
(
getters
.
autocompleteQuery
)
.
then
(({
data
})
=>
commit
(
types
.
RECEIVE_AUTOCOMPLETE_SUCCESS
,
data
))
.
catch
(()
=>
{
commit
(
types
.
RECEIVE_AUTOCOMPLETE_ERROR
);
createFlash
({
message
:
__
(
'
There was an error fetching search autocomplete suggestions
'
)
});
});
};
export
const
setSearch
=
({
commit
},
value
)
=>
{
commit
(
types
.
SET_SEARCH
,
value
);
};
app/assets/javascripts/header_search/store/getters.js
View file @
84e30a1a
...
...
@@ -23,6 +23,16 @@ export const searchQuery = (state) => {
return
`
${
state
.
searchPath
}
?
${
objectToQuery
(
query
)}
`
;
};
export
const
autocompleteQuery
=
(
state
)
=>
{
const
query
=
{
term
:
state
.
search
,
project_id
:
state
.
searchContext
.
project
?.
id
,
project_ref
:
state
.
searchContext
.
ref
,
};
return
`
${
state
.
autocompletePath
}
?
${
objectToQuery
(
query
)}
`
;
};
export
const
scopedIssuesPath
=
(
state
)
=>
{
return
(
state
.
searchContext
.
project_metadata
?.
issues_path
||
...
...
@@ -133,3 +143,25 @@ export const scopedSearchOptions = (state, getters) => {
return
options
;
};
export
const
autocompleteGroupedSearchOptions
=
(
state
)
=>
{
const
groupedOptions
=
{};
const
results
=
[];
state
.
autocompleteOptions
.
forEach
((
option
)
=>
{
const
category
=
groupedOptions
[
option
.
category
];
if
(
category
)
{
category
.
data
.
push
(
option
);
}
else
{
groupedOptions
[
option
.
category
]
=
{
category
:
option
.
category
,
data
:
[
option
],
};
results
.
push
(
groupedOptions
[
option
.
category
]);
}
});
return
results
;
};
app/assets/javascripts/header_search/store/index.js
View file @
84e30a1a
...
...
@@ -7,11 +7,17 @@ import createState from './state';
Vue
.
use
(
Vuex
);
export
const
getStoreConfig
=
({
searchPath
,
issuesPath
,
mrPath
,
searchContext
})
=>
({
export
const
getStoreConfig
=
({
searchPath
,
issuesPath
,
mrPath
,
autocompletePath
,
searchContext
,
})
=>
({
actions
,
getters
,
mutations
,
state
:
createState
({
searchPath
,
issuesPath
,
mrPath
,
searchContext
}),
state
:
createState
({
searchPath
,
issuesPath
,
mrPath
,
autocompletePath
,
searchContext
}),
});
const
createStore
=
(
config
)
=>
new
Vuex
.
Store
(
getStoreConfig
(
config
));
...
...
app/assets/javascripts/header_search/store/mutation_types.js
View file @
84e30a1a
export
const
REQUEST_AUTOCOMPLETE
=
'
REQUEST_AUTOCOMPLETE
'
;
export
const
RECEIVE_AUTOCOMPLETE_SUCCESS
=
'
RECEIVE_AUTOCOMPLETE_SUCCESS
'
;
export
const
RECEIVE_AUTOCOMPLETE_ERROR
=
'
RECEIVE_AUTOCOMPLETE_ERROR
'
;
export
const
SET_SEARCH
=
'
SET_SEARCH
'
;
app/assets/javascripts/header_search/store/mutations.js
View file @
84e30a1a
import
*
as
types
from
'
./mutation_types
'
;
export
default
{
[
types
.
REQUEST_AUTOCOMPLETE
](
state
)
{
state
.
loading
=
true
;
state
.
autocompleteOptions
=
[];
},
[
types
.
RECEIVE_AUTOCOMPLETE_SUCCESS
](
state
,
data
)
{
state
.
loading
=
false
;
state
.
autocompleteOptions
=
data
;
},
[
types
.
RECEIVE_AUTOCOMPLETE_ERROR
](
state
)
{
state
.
loading
=
false
;
state
.
autocompleteOptions
=
[];
},
[
types
.
SET_SEARCH
](
state
,
value
)
{
state
.
search
=
value
;
},
...
...
app/assets/javascripts/header_search/store/state.js
View file @
84e30a1a
const
createState
=
({
searchPath
,
issuesPath
,
mrPath
,
searchContext
})
=>
({
const
createState
=
({
searchPath
,
issuesPath
,
mrPath
,
autocompletePath
,
searchContext
})
=>
({
searchPath
,
issuesPath
,
mrPath
,
autocompletePath
,
searchContext
,
search
:
''
,
autocompleteOptions
:
[],
loading
:
false
,
});
export
default
createState
;
app/views/layouts/header/_default.html.haml
View file @
84e30a1a
...
...
@@ -34,7 +34,8 @@
#js-header-search
.header-search
{
data:
{
'search-context'
=>
search_context
.
to_json
,
'search-path'
=>
search_path
,
'issues-path'
=>
issues_dashboard_path
,
'mr-path'
=>
merge_requests_dashboard_path
}
}
'mr-path'
=>
merge_requests_dashboard_path
,
'autocomplete-path'
=>
search_autocomplete_path
}
}
%input
{
type:
"text"
,
placeholder:
_
(
'Search or jump to...'
),
class:
'form-control gl-form-input'
}
-
else
=
render
'layouts/search'
...
...
locale/gitlab.pot
View file @
84e30a1a
...
...
@@ -34157,6 +34157,9 @@ msgstr ""
msgid "There was an error fetching projects"
msgstr ""
msgid "There was an error fetching search autocomplete suggestions"
msgstr ""
msgid "There was an error fetching stage total counts"
msgstr ""
...
...
spec/frontend/header_search/components/app_spec.js
View file @
84e30a1a
...
...
@@ -3,6 +3,7 @@ import Vue from 'vue';
import
Vuex
from
'
vuex
'
;
import
{
shallowMountExtended
}
from
'
helpers/vue_test_utils_helper
'
;
import
HeaderSearchApp
from
'
~/header_search/components/app.vue
'
;
import
HeaderSearchAutocompleteItems
from
'
~/header_search/components/header_search_autocomplete_items.vue
'
;
import
HeaderSearchDefaultItems
from
'
~/header_search/components/header_search_default_items.vue
'
;
import
HeaderSearchScopedItems
from
'
~/header_search/components/header_search_scoped_items.vue
'
;
import
{
ENTER_KEY
,
ESC_KEY
}
from
'
~/lib/utils/keys
'
;
...
...
@@ -20,6 +21,7 @@ describe('HeaderSearchApp', () => {
const
actionSpies
=
{
setSearch
:
jest
.
fn
(),
fetchAutocompleteOptions
:
jest
.
fn
(),
};
const
createComponent
=
(
initialState
)
=>
{
...
...
@@ -46,6 +48,8 @@ describe('HeaderSearchApp', () => {
const
findHeaderSearchDropdown
=
()
=>
wrapper
.
findByTestId
(
'
header-search-dropdown-menu
'
);
const
findHeaderSearchDefaultItems
=
()
=>
wrapper
.
findComponent
(
HeaderSearchDefaultItems
);
const
findHeaderSearchScopedItems
=
()
=>
wrapper
.
findComponent
(
HeaderSearchScopedItems
);
const
findHeaderSearchAutocompleteItems
=
()
=>
wrapper
.
findComponent
(
HeaderSearchAutocompleteItems
);
describe
(
'
template
'
,
()
=>
{
it
(
'
always renders Header Search Input
'
,
()
=>
{
...
...
@@ -74,11 +78,11 @@ describe('HeaderSearchApp', () => {
});
describe
.
each
`
search | showDefault | showScoped
${
null
}
|
${
true
}
|
${
false
}
${
''
}
|
${
true
}
|
${
false
}
${
MOCK_SEARCH
}
|
${
false
}
|
${
true
}
`
(
'
Header Search Dropdown Items
'
,
({
search
,
showDefault
,
showScoped
})
=>
{
search | showDefault | showScoped
| showAutocomplete
${
null
}
|
${
true
}
|
${
false
}
|
${
false
}
${
''
}
|
${
true
}
|
${
false
}
|
${
false
}
${
MOCK_SEARCH
}
|
${
false
}
|
${
true
}
|
${
true
}
`
(
'
Header Search Dropdown Items
'
,
({
search
,
showDefault
,
showScoped
,
showAutocomplete
})
=>
{
describe
(
`when search is
${
search
}
`
,
()
=>
{
beforeEach
(()
=>
{
createComponent
({
search
});
...
...
@@ -93,6 +97,10 @@ describe('HeaderSearchApp', () => {
it
(
`should
${
showScoped
?
''
:
'
not
'
}
render the Scoped Dropdown Items`
,
()
=>
{
expect
(
findHeaderSearchScopedItems
().
exists
()).
toBe
(
showScoped
);
});
it
(
`should
${
showAutocomplete
?
''
:
'
not
'
}
render the Autocomplete Dropdown Items`
,
()
=>
{
expect
(
findHeaderSearchAutocompleteItems
().
exists
()).
toBe
(
showAutocomplete
);
});
});
});
});
...
...
@@ -139,12 +147,18 @@ describe('HeaderSearchApp', () => {
});
});
it
(
'
calls setSearch when search input event is fired
'
,
async
()
=>
{
findHeaderSearchInput
().
vm
.
$emit
(
'
input
'
,
MOCK_SEARCH
);
describe
(
'
onInput
'
,
()
=>
{
beforeEach
(()
=>
{
findHeaderSearchInput
().
vm
.
$emit
(
'
input
'
,
MOCK_SEARCH
);
});
await
wrapper
.
vm
.
$nextTick
();
it
(
'
calls setSearch with search term
'
,
()
=>
{
expect
(
actionSpies
.
setSearch
).
toHaveBeenCalledWith
(
expect
.
any
(
Object
),
MOCK_SEARCH
);
});
expect
(
actionSpies
.
setSearch
).
toHaveBeenCalledWith
(
expect
.
any
(
Object
),
MOCK_SEARCH
);
it
(
'
calls fetchAutocompleteOptions
'
,
()
=>
{
expect
(
actionSpies
.
fetchAutocompleteOptions
).
toHaveBeenCalled
();
});
});
it
(
'
submits a search onKey-Enter
'
,
async
()
=>
{
...
...
spec/frontend/header_search/components/header_search_autocomplete_items_spec.js
0 → 100644
View file @
84e30a1a
import
{
GlDropdownItem
,
GlLoadingIcon
,
GlAvatar
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
Vue
from
'
vue
'
;
import
Vuex
from
'
vuex
'
;
import
HeaderSearchAutocompleteItems
from
'
~/header_search/components/header_search_autocomplete_items.vue
'
;
import
{
GROUPS_CATEGORY
,
LARGE_AVATAR_PX
,
PROJECTS_CATEGORY
,
SMALL_AVATAR_PX
,
}
from
'
~/header_search/constants
'
;
import
{
MOCK_GROUPED_AUTOCOMPLETE_OPTIONS
,
MOCK_AUTOCOMPLETE_OPTIONS
}
from
'
../mock_data
'
;
Vue
.
use
(
Vuex
);
describe
(
'
HeaderSearchAutocompleteItems
'
,
()
=>
{
let
wrapper
;
const
createComponent
=
(
initialState
,
mockGetters
)
=>
{
const
store
=
new
Vuex
.
Store
({
state
:
{
loading
:
false
,
...
initialState
,
},
getters
:
{
autocompleteGroupedSearchOptions
:
()
=>
MOCK_GROUPED_AUTOCOMPLETE_OPTIONS
,
...
mockGetters
,
},
});
wrapper
=
shallowMount
(
HeaderSearchAutocompleteItems
,
{
store
,
});
};
afterEach
(()
=>
{
wrapper
.
destroy
();
});
const
findDropdownItems
=
()
=>
wrapper
.
findAllComponents
(
GlDropdownItem
);
const
findDropdownItemTitles
=
()
=>
findDropdownItems
().
wrappers
.
map
((
w
)
=>
w
.
text
());
const
findDropdownItemLinks
=
()
=>
findDropdownItems
().
wrappers
.
map
((
w
)
=>
w
.
attributes
(
'
href
'
));
const
findGlLoadingIcon
=
()
=>
wrapper
.
findComponent
(
GlLoadingIcon
);
const
findGlAvatar
=
()
=>
wrapper
.
findComponent
(
GlAvatar
);
describe
(
'
template
'
,
()
=>
{
describe
(
'
when loading is true
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
({
loading
:
true
});
});
it
(
'
renders GlLoadingIcon
'
,
()
=>
{
expect
(
findGlLoadingIcon
().
exists
()).
toBe
(
true
);
});
it
(
'
does not render autocomplete options
'
,
()
=>
{
expect
(
findDropdownItems
()).
toHaveLength
(
0
);
});
});
describe
(
'
when loading is false
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
({
loading
:
false
});
});
it
(
'
does not render GlLoadingIcon
'
,
()
=>
{
expect
(
findGlLoadingIcon
().
exists
()).
toBe
(
false
);
});
describe
(
'
Dropdown items
'
,
()
=>
{
it
(
'
renders item for each option in autocomplete option
'
,
()
=>
{
expect
(
findDropdownItems
()).
toHaveLength
(
MOCK_AUTOCOMPLETE_OPTIONS
.
length
);
});
it
(
'
renders titles correctly
'
,
()
=>
{
const
expectedTitles
=
MOCK_AUTOCOMPLETE_OPTIONS
.
map
((
o
)
=>
o
.
label
);
expect
(
findDropdownItemTitles
()).
toStrictEqual
(
expectedTitles
);
});
it
(
'
renders links correctly
'
,
()
=>
{
const
expectedLinks
=
MOCK_AUTOCOMPLETE_OPTIONS
.
map
((
o
)
=>
o
.
url
);
expect
(
findDropdownItemLinks
()).
toStrictEqual
(
expectedLinks
);
});
});
describe
.
each
`
item | showAvatar | avatarSize
${{
data
:
[{
category
:
PROJECTS_CATEGORY
,
avatar_url
:
null
}]
}
} |
${
true
}
|
${
String
(
LARGE_AVATAR_PX
)}
${{
data
:
[{
category
:
GROUPS_CATEGORY
,
avatar_url
:
'
/123
'
}]
}
} |
${
true
}
|
${
String
(
LARGE_AVATAR_PX
)}
${{
data
:
[{
category
:
'
Help
'
,
avatar_url
:
''
}]
}
} |
${
true
}
|
${
String
(
SMALL_AVATAR_PX
)}
${{
data
:
[{
category
:
'
Settings
'
}]
}
} |
${
false
}
|
${
false
}
`
(
'
GlAvatar
'
,
({
item
,
showAvatar
,
avatarSize
})
=>
{
describe
(
`when category is
${
item
.
data
[
0
].
category
}
and avatar_url is
${
item
.
data
[
0
].
avatar_url
}
`
,
()
=>
{
beforeEach
(()
=>
{
createComponent
({},
{
autocompleteGroupedSearchOptions
:
()
=>
[
item
]
});
});
it
(
`should
${
showAvatar
?
''
:
'
not
'
}
render`
,
()
=>
{
expect
(
findGlAvatar
().
exists
()).
toBe
(
showAvatar
);
});
it
(
`should set avatarSize to
${
avatarSize
}
`
,
()
=>
{
expect
(
findGlAvatar
().
exists
()
&&
findGlAvatar
().
attributes
(
'
size
'
)).
toBe
(
avatarSize
);
});
});
});
});
});
});
spec/frontend/header_search/mock_data.js
View file @
84e30a1a
...
...
@@ -19,6 +19,8 @@ export const MOCK_MR_PATH = '/dashboard/merge_requests';
export
const
MOCK_ALL_PATH
=
'
/
'
;
export
const
MOCK_AUTOCOMPLETE_PATH
=
'
/autocomplete
'
;
export
const
MOCK_PROJECT
=
{
id
:
123
,
name
:
'
MockProject
'
,
...
...
@@ -81,3 +83,70 @@ export const MOCK_SCOPED_SEARCH_OPTIONS = [
url
:
MOCK_ALL_PATH
,
},
];
export
const
MOCK_AUTOCOMPLETE_OPTIONS
=
[
{
category
:
'
Projects
'
,
id
:
1
,
label
:
'
MockProject1
'
,
url
:
'
project/1
'
,
},
{
category
:
'
Projects
'
,
id
:
2
,
label
:
'
MockProject2
'
,
url
:
'
project/2
'
,
},
{
category
:
'
Groups
'
,
id
:
1
,
label
:
'
MockGroup1
'
,
url
:
'
group/1
'
,
},
{
category
:
'
Help
'
,
label
:
'
GitLab Help
'
,
url
:
'
help/gitlab
'
,
},
];
export
const
MOCK_GROUPED_AUTOCOMPLETE_OPTIONS
=
[
{
category
:
'
Projects
'
,
data
:
[
{
category
:
'
Projects
'
,
id
:
1
,
label
:
'
MockProject1
'
,
url
:
'
project/1
'
,
},
{
category
:
'
Projects
'
,
id
:
2
,
label
:
'
MockProject2
'
,
url
:
'
project/2
'
,
},
],
},
{
category
:
'
Groups
'
,
data
:
[
{
category
:
'
Groups
'
,
id
:
1
,
label
:
'
MockGroup1
'
,
url
:
'
group/1
'
,
},
],
},
{
category
:
'
Help
'
,
data
:
[
{
category
:
'
Help
'
,
label
:
'
GitLab Help
'
,
url
:
'
help/gitlab
'
,
},
],
},
];
spec/frontend/header_search/store/actions_spec.js
View file @
84e30a1a
import
MockAdapter
from
'
axios-mock-adapter
'
;
import
testAction
from
'
helpers/vuex_action_helper
'
;
import
createFlash
from
'
~/flash
'
;
import
*
as
actions
from
'
~/header_search/store/actions
'
;
import
*
as
types
from
'
~/header_search/store/mutation_types
'
;
import
createState
from
'
~/header_search/store/state
'
;
import
{
MOCK_SEARCH
}
from
'
../mock_data
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
{
MOCK_SEARCH
,
MOCK_AUTOCOMPLETE_OPTIONS
}
from
'
../mock_data
'
;
jest
.
mock
(
'
~/flash
'
);
describe
(
'
Header Search Store Actions
'
,
()
=>
{
let
state
;
let
mock
;
const
flashCallback
=
(
callCount
)
=>
{
expect
(
createFlash
).
toHaveBeenCalledTimes
(
callCount
);
createFlash
.
mockClear
();
};
beforeEach
(()
=>
{
state
=
createState
({});
mock
=
new
MockAdapter
(
axios
);
});
afterEach
(()
=>
{
state
=
null
;
mock
.
restore
();
});
describe
.
each
`
axiosMock | type | expectedMutations | flashCallCount
${{
method
:
'
onGet
'
,
code
:
200
,
res
:
MOCK_AUTOCOMPLETE_OPTIONS
}
} |
${
'
success
'
}
|
${[{
type
:
types
.
REQUEST_AUTOCOMPLETE
},
{
type
:
types
.
RECEIVE_AUTOCOMPLETE_SUCCESS
,
payload
:
MOCK_AUTOCOMPLETE_OPTIONS
}]}
|
${
0
}
${{
method
:
'
onGet
'
,
code
:
500
,
res
:
null
}
} |
${
'
error
'
}
|
${[{
type
:
types
.
REQUEST_AUTOCOMPLETE
},
{
type
:
types
.
RECEIVE_AUTOCOMPLETE_ERROR
}]}
|
${
1
}
`
(
'
fetchAutocompleteOptions
'
,
({
axiosMock
,
type
,
expectedMutations
,
flashCallCount
})
=>
{
describe
(
`on
${
type
}
`
,
()
=>
{
beforeEach
(()
=>
{
mock
[
axiosMock
.
method
]().
replyOnce
(
axiosMock
.
code
,
axiosMock
.
res
);
});
it
(
`should dispatch the correct mutations`
,
()
=>
{
return
testAction
({
action
:
actions
.
fetchAutocompleteOptions
,
state
,
expectedMutations
,
}).
then
(()
=>
flashCallback
(
flashCallCount
));
});
});
});
describe
(
'
setSearch
'
,
()
=>
{
...
...
spec/frontend/header_search/store/getters_spec.js
View file @
84e30a1a
...
...
@@ -5,6 +5,7 @@ import {
MOCK_SEARCH_PATH
,
MOCK_ISSUE_PATH
,
MOCK_MR_PATH
,
MOCK_AUTOCOMPLETE_PATH
,
MOCK_SEARCH_CONTEXT
,
MOCK_DEFAULT_SEARCH_OPTIONS
,
MOCK_SCOPED_SEARCH_OPTIONS
,
...
...
@@ -12,6 +13,8 @@ import {
MOCK_GROUP
,
MOCK_ALL_PATH
,
MOCK_SEARCH
,
MOCK_AUTOCOMPLETE_OPTIONS
,
MOCK_GROUPED_AUTOCOMPLETE_OPTIONS
,
}
from
'
../mock_data
'
;
describe
(
'
Header Search Store Getters
'
,
()
=>
{
...
...
@@ -22,6 +25,7 @@ describe('Header Search Store Getters', () => {
searchPath
:
MOCK_SEARCH_PATH
,
issuesPath
:
MOCK_ISSUE_PATH
,
mrPath
:
MOCK_MR_PATH
,
autocompletePath
:
MOCK_AUTOCOMPLETE_PATH
,
searchContext
:
MOCK_SEARCH_CONTEXT
,
...
initialState
,
});
...
...
@@ -55,6 +59,29 @@ describe('Header Search Store Getters', () => {
});
});
describe.each`
project
|
ref
|
expectedPath
$
{
null
}
|
$
{
null
}
|
$
{
`
${
MOCK_AUTOCOMPLETE_PATH
}
?term=
${
MOCK_SEARCH
}
&project_id=undefined&project_ref=null`
}
$
{
MOCK_PROJECT
}
|
$
{
null
}
|
$
{
`
${
MOCK_AUTOCOMPLETE_PATH
}
?term=
${
MOCK_SEARCH
}
&project_id=
${
MOCK_PROJECT
.
id
}
&project_ref=null`
}
$
{
MOCK_PROJECT
}
|
$
{
MOCK_PROJECT
.
id
}
|
$
{
`
${
MOCK_AUTOCOMPLETE_PATH
}
?term=
${
MOCK_SEARCH
}
&project_id=
${
MOCK_PROJECT
.
id
}
&project_ref=
${
MOCK_PROJECT
.
id
}
`
}
`('autocompleteQuery', ({ project, ref, expectedPath }) => {
describe(`
when
project
is
$
{
project
?.
name
}
and
project
ref
is
$
{
ref
}
`, () => {
beforeEach(() => {
createState({
searchContext: {
project,
ref,
},
});
state.search = MOCK_SEARCH;
});
it(`
should
return
$
{
expectedPath
}
`, () => {
expect(getters.autocompleteQuery(state)).toBe(expectedPath);
});
});
});
describe.each`
group
|
group_metadata
|
project
|
project_metadata
|
expectedPath
$
{
null
}
|
$
{
null
}
|
$
{
null
}
|
$
{
null
}
|
$
{
MOCK_ISSUE_PATH
}
...
...
@@ -208,4 +235,17 @@ describe('Header Search Store Getters', () => {
);
});
});
describe('autocompleteGroupedSearchOptions', () => {
beforeEach(() => {
createState();
state.autocompleteOptions = MOCK_AUTOCOMPLETE_OPTIONS;
});
it('returns the correct grouped array', () => {
expect(getters.autocompleteGroupedSearchOptions(state)).toStrictEqual(
MOCK_GROUPED_AUTOCOMPLETE_OPTIONS,
);
});
});
});
spec/frontend/header_search/store/mutations_spec.js
View file @
84e30a1a
import
*
as
types
from
'
~/header_search/store/mutation_types
'
;
import
mutations
from
'
~/header_search/store/mutations
'
;
import
createState
from
'
~/header_search/store/state
'
;
import
{
MOCK_SEARCH
}
from
'
../mock_data
'
;
import
{
MOCK_SEARCH
,
MOCK_AUTOCOMPLETE_OPTIONS
}
from
'
../mock_data
'
;
describe
(
'
Header Search Store Mutations
'
,
()
=>
{
let
state
;
...
...
@@ -10,6 +10,33 @@ describe('Header Search Store Mutations', () => {
state
=
createState
({});
});
describe
(
'
REQUEST_AUTOCOMPLETE
'
,
()
=>
{
it
(
'
sets loading to true and empties autocompleteOptions array
'
,
()
=>
{
mutations
[
types
.
REQUEST_AUTOCOMPLETE
](
state
);
expect
(
state
.
loading
).
toBe
(
true
);
expect
(
state
.
autocompleteOptions
).
toStrictEqual
([]);
});
});
describe
(
'
RECEIVE_AUTOCOMPLETE_SUCCESS
'
,
()
=>
{
it
(
'
sets loading to false and sets autocompleteOptions array
'
,
()
=>
{
mutations
[
types
.
RECEIVE_AUTOCOMPLETE_SUCCESS
](
state
,
MOCK_AUTOCOMPLETE_OPTIONS
);
expect
(
state
.
loading
).
toBe
(
false
);
expect
(
state
.
autocompleteOptions
).
toStrictEqual
(
MOCK_AUTOCOMPLETE_OPTIONS
);
});
});
describe
(
'
RECEIVE_AUTOCOMPLETE_ERROR
'
,
()
=>
{
it
(
'
sets loading to false and empties autocompleteOptions array
'
,
()
=>
{
mutations
[
types
.
RECEIVE_AUTOCOMPLETE_ERROR
](
state
);
expect
(
state
.
loading
).
toBe
(
false
);
expect
(
state
.
autocompleteOptions
).
toStrictEqual
([]);
});
});
describe
(
'
SET_SEARCH
'
,
()
=>
{
it
(
'
sets search to value
'
,
()
=>
{
mutations
[
types
.
SET_SEARCH
](
state
,
MOCK_SEARCH
);
...
...
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