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
0
Merge Requests
0
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
Léo-Paul Géneau
gitlab-ce
Commits
f44fb5cf
Commit
f44fb5cf
authored
Jan 30, 2017
by
Clement Ho
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add filtered search visual tokens
parent
b5cb1115
Changes
32
Hide whitespace changes
Inline
Side-by-side
Showing
32 changed files
with
2244 additions
and
376 deletions
+2244
-376
app/assets/javascripts/droplab/droplab_ajax.js
app/assets/javascripts/droplab/droplab_ajax.js
+5
-1
app/assets/javascripts/filtered_search/dropdown_hint.js
app/assets/javascripts/filtered_search/dropdown_hint.js
+18
-1
app/assets/javascripts/filtered_search/dropdown_user.js
app/assets/javascripts/filtered_search/dropdown_user.js
+6
-1
app/assets/javascripts/filtered_search/dropdown_utils.js
app/assets/javascripts/filtered_search/dropdown_utils.js
+50
-21
app/assets/javascripts/filtered_search/filtered_search_bundle.js
...ets/javascripts/filtered_search/filtered_search_bundle.js
+1
-0
app/assets/javascripts/filtered_search/filtered_search_dropdown.js
...s/javascripts/filtered_search/filtered_search_dropdown.js
+1
-1
app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
...ripts/filtered_search/filtered_search_dropdown_manager.js
+13
-38
app/assets/javascripts/filtered_search/filtered_search_manager.js
...ts/javascripts/filtered_search/filtered_search_manager.js
+158
-16
app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
...ascripts/filtered_search/filtered_search_visual_tokens.js
+200
-0
app/assets/stylesheets/framework/filters.scss
app/assets/stylesheets/framework/filters.scss
+99
-2
app/assets/stylesheets/framework/variables.scss
app/assets/stylesheets/framework/variables.scss
+9
-0
app/views/shared/issuable/_search_bar.html.haml
app/views/shared/issuable/_search_bar.html.haml
+7
-4
spec/features/issues/filtered_search/dropdown_assignee_spec.rb
...features/issues/filtered_search/dropdown_assignee_spec.rb
+9
-3
spec/features/issues/filtered_search/dropdown_author_spec.rb
spec/features/issues/filtered_search/dropdown_author_spec.rb
+5
-2
spec/features/issues/filtered_search/dropdown_hint_spec.rb
spec/features/issues/filtered_search/dropdown_hint_spec.rb
+55
-8
spec/features/issues/filtered_search/dropdown_label_spec.rb
spec/features/issues/filtered_search/dropdown_label_spec.rb
+20
-11
spec/features/issues/filtered_search/dropdown_milestone_spec.rb
...eatures/issues/filtered_search/dropdown_milestone_spec.rb
+19
-9
spec/features/issues/filtered_search/filter_issues_spec.rb
spec/features/issues/filtered_search/filter_issues_spec.rb
+207
-160
spec/features/issues/filtered_search/search_bar_spec.rb
spec/features/issues/filtered_search/search_bar_spec.rb
+3
-1
spec/features/issues/filtered_search/visual_tokens_spec.rb
spec/features/issues/filtered_search/visual_tokens_spec.rb
+306
-0
spec/features/merge_requests/filter_by_labels_spec.rb
spec/features/merge_requests/filter_by_labels_spec.rb
+1
-1
spec/features/merge_requests/filter_by_milestone_spec.rb
spec/features/merge_requests/filter_by_milestone_spec.rb
+3
-0
spec/features/merge_requests/filter_merge_requests_spec.rb
spec/features/merge_requests/filter_merge_requests_spec.rb
+64
-24
spec/features/merge_requests/reset_filters_spec.rb
spec/features/merge_requests/reset_filters_spec.rb
+29
-3
spec/features/search_spec.rb
spec/features/search_spec.rb
+9
-4
spec/javascripts/filtered_search/dropdown_user_spec.js
spec/javascripts/filtered_search/dropdown_user_spec.js
+2
-6
spec/javascripts/filtered_search/dropdown_utils_spec.js
spec/javascripts/filtered_search/dropdown_utils_spec.js
+9
-16
spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js
.../filtered_search/filtered_search_dropdown_manager_spec.js
+57
-15
spec/javascripts/filtered_search/filtered_search_manager_spec.js
...vascripts/filtered_search/filtered_search_manager_spec.js
+206
-26
spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
...pts/filtered_search/filtered_search_visual_tokens_spec.js
+587
-0
spec/javascripts/helpers/filtered_search_spec_helper.js
spec/javascripts/helpers/filtered_search_spec_helper.js
+52
-0
spec/support/filtered_search_helpers.rb
spec/support/filtered_search_helpers.rb
+34
-2
No files found.
app/assets/javascripts/droplab/droplab_ajax.js
View file @
f44fb5cf
...
...
@@ -37,11 +37,14 @@ require('../window')(function(w){
}
}
self
.
hook
.
list
[
config
.
method
].
call
(
self
.
hook
.
list
,
data
);
if
(
!
self
.
destroyed
)
{
self
.
hook
.
list
[
config
.
method
].
call
(
self
.
hook
.
list
,
data
);
}
},
init
:
function
init
(
hook
)
{
var
self
=
this
;
self
.
destroyed
=
false
;
self
.
cache
=
self
.
cache
||
{};
var
config
=
hook
.
config
.
droplabAjax
;
this
.
hook
=
hook
;
...
...
@@ -79,6 +82,7 @@ require('../window')(function(w){
destroy
:
function
()
{
var
dynamicList
=
this
.
hook
.
list
.
list
.
querySelector
(
'
[data-dynamic]
'
);
this
.
destroyed
=
true
;
if
(
this
.
listTemplate
&&
dynamicList
)
{
dynamicList
.
outerHTML
=
this
.
listTemplate
;
}
...
...
app/assets/javascripts/filtered_search/dropdown_hint.js
View file @
f44fb5cf
...
...
@@ -28,6 +28,23 @@ require('./filtered_search_dropdown');
const
tag
=
selected
.
querySelector
(
'
.js-filter-tag
'
).
innerText
.
trim
();
if
(
tag
.
length
)
{
// Get previous input values in the input field and convert them into visual tokens
const
previousInputValues
=
this
.
input
.
value
.
split
(
'
'
);
const
searchTerms
=
[];
previousInputValues
.
forEach
((
value
,
index
)
=>
{
searchTerms
.
push
(
value
);
if
(
index
===
previousInputValues
.
length
-
1
&&
token
.
indexOf
(
value
.
toLowerCase
())
!==
-
1
)
{
searchTerms
.
pop
();
}
});
if
(
searchTerms
.
length
>
0
)
{
gl
.
FilteredSearchVisualTokens
.
addSearchVisualToken
(
searchTerms
.
join
(
'
'
));
}
gl
.
FilteredSearchDropdownManager
.
addWordToInput
(
token
.
replace
(
'
:
'
,
''
));
}
this
.
dismissDropdown
();
...
...
@@ -39,7 +56,7 @@ require('./filtered_search_dropdown');
renderContent
()
{
const
dropdownData
=
[];
[].
forEach
.
call
(
this
.
input
.
parentElement
.
querySelectorAll
(
'
.dropdown-menu
'
),
(
dropdownMenu
)
=>
{
[].
forEach
.
call
(
this
.
input
.
closest
(
'
.filtered-search-input-container
'
)
.
querySelectorAll
(
'
.dropdown-menu
'
),
(
dropdownMenu
)
=>
{
const
{
icon
,
hint
,
tag
}
=
dropdownMenu
.
dataset
;
if
(
icon
&&
hint
&&
tag
)
{
dropdownData
.
push
({
...
...
app/assets/javascripts/filtered_search/dropdown_user.js
View file @
f44fb5cf
...
...
@@ -39,7 +39,12 @@ require('./filtered_search_dropdown');
getSearchInput
()
{
const
query
=
gl
.
DropdownUtils
.
getSearchInput
(
this
.
input
);
const
{
lastToken
}
=
gl
.
FilteredSearchTokenizer
.
processTokens
(
query
);
let
value
=
lastToken
.
value
||
''
;
let
value
=
lastToken
||
''
;
if
(
value
[
0
]
===
'
@
'
)
{
value
=
value
.
slice
(
1
);
}
// Removes the first character if it is a quotation so that we can search
// with multiple words
...
...
app/assets/javascripts/filtered_search/dropdown_utils.js
View file @
f44fb5cf
...
...
@@ -22,38 +22,40 @@
static
filterWithSymbol
(
filterSymbol
,
input
,
item
)
{
const
updatedItem
=
item
;
const
query
=
gl
.
DropdownUtils
.
getSearchInput
(
input
);
const
{
lastToken
,
searchToken
}
=
gl
.
FilteredSearchTokenizer
.
processTokens
(
query
);
const
searchInput
=
gl
.
DropdownUtils
.
getSearchInput
(
input
);
if
(
lastToken
!==
searchToken
)
{
const
title
=
updatedItem
.
title
.
toLowerCase
();
let
value
=
lastToken
.
value
.
toLowerCase
()
;
const
title
=
updatedItem
.
title
.
toLowerCase
();
let
value
=
searchInput
.
toLowerCase
();
let
symbol
=
''
;
// Removes the first character if it is a quotation so that we can search
// with multiple words
if
((
value
[
0
]
===
'
"
'
||
value
[
0
]
===
'
\'
'
)
&&
title
.
indexOf
(
'
'
)
!==
-
1
)
{
value
=
value
.
slice
(
1
);
}
// Eg. filterSymbol = ~ for labels
const
matchWithoutSymbol
=
lastToken
.
symbol
===
filterSymbol
&&
title
.
indexOf
(
value
)
!==
-
1
;
const
match
=
title
.
indexOf
(
`
${
lastToken
.
symbol
}${
value
}
`
)
!==
-
1
;
// Remove the symbol for filter
if
(
value
[
0
]
===
filterSymbol
)
{
symbol
=
value
[
0
];
value
=
value
.
slice
(
1
);
}
updatedItem
.
droplab_hidden
=
!
match
&&
!
matchWithoutSymbol
;
}
else
{
updatedItem
.
droplab_hidden
=
false
;
// Removes the first character if it is a quotation so that we can search
// with multiple words
if
((
value
[
0
]
===
'
"
'
||
value
[
0
]
===
'
\'
'
)
&&
title
.
indexOf
(
'
'
)
!==
-
1
)
{
value
=
value
.
slice
(
1
);
}
// Eg. filterSymbol = ~ for labels
const
matchWithoutSymbol
=
symbol
===
filterSymbol
&&
title
.
indexOf
(
value
)
!==
-
1
;
const
match
=
title
.
indexOf
(
`
${
symbol
}${
value
}
`
)
!==
-
1
;
updatedItem
.
droplab_hidden
=
!
match
&&
!
matchWithoutSymbol
;
return
updatedItem
;
}
static
filterHint
(
input
,
item
)
{
const
updatedItem
=
item
;
const
query
=
gl
.
DropdownUtils
.
getSearchInput
(
input
);
let
{
lastToken
}
=
gl
.
FilteredSearchTokenizer
.
processTokens
(
query
);
const
searchInput
=
gl
.
DropdownUtils
.
getSearchInput
(
input
);
let
{
lastToken
}
=
gl
.
FilteredSearchTokenizer
.
processTokens
(
searchInput
);
lastToken
=
lastToken
.
key
||
lastToken
||
''
;
if
(
!
lastToken
||
query
.
split
(
''
).
last
()
===
'
'
)
{
if
(
!
lastToken
||
searchInput
.
split
(
''
).
last
()
===
'
'
)
{
updatedItem
.
droplab_hidden
=
false
;
}
else
if
(
lastToken
)
{
const
split
=
lastToken
.
split
(
'
:
'
);
...
...
@@ -70,13 +72,40 @@
const
dataValue
=
selected
.
getAttribute
(
'
data-value
'
);
if
(
dataValue
)
{
gl
.
FilteredSearchDropdownManager
.
addWordToInput
(
filter
,
dataValue
);
gl
.
FilteredSearchDropdownManager
.
addWordToInput
(
filter
,
dataValue
,
true
);
}
// Return boolean based on whether it was set
return
dataValue
!==
null
;
}
static
getSearchQuery
()
{
const
tokensContainer
=
document
.
querySelector
(
'
.tokens-container
'
);
const
values
=
[];
[].
forEach
.
call
(
tokensContainer
.
querySelectorAll
(
'
.js-visual-token
'
),
(
token
)
=>
{
const
name
=
token
.
querySelector
(
'
.name
'
);
const
value
=
token
.
querySelector
(
'
.value
'
);
const
symbol
=
value
&&
value
.
dataset
.
symbol
?
value
.
dataset
.
symbol
:
''
;
let
valueText
=
''
;
if
(
value
&&
value
.
innerText
)
{
valueText
=
value
.
innerText
;
}
if
(
token
.
className
.
indexOf
(
'
filtered-search-token
'
)
!==
-
1
)
{
values
.
push
(
`
${
name
.
innerText
.
toLowerCase
()}
:
${
symbol
}${
valueText
}
`
);
}
else
{
values
.
push
(
name
.
innerText
);
}
});
const
input
=
document
.
querySelector
(
'
.filtered-search
'
);
values
.
push
(
input
&&
input
.
value
);
return
values
.
join
(
'
'
);
}
static
getSearchInput
(
filteredSearchInput
)
{
const
inputValue
=
filteredSearchInput
.
value
;
const
{
right
}
=
gl
.
DropdownUtils
.
getInputSelectionPosition
(
filteredSearchInput
);
...
...
app/assets/javascripts/filtered_search/filtered_search_bundle.js
View file @
f44fb5cf
...
...
@@ -7,3 +7,4 @@ require('./filtered_search_dropdown');
require
(
'
./filtered_search_manager
'
);
require
(
'
./filtered_search_token_keys
'
);
require
(
'
./filtered_search_tokenizer
'
);
require
(
'
./filtered_search_visual_tokens
'
);
app/assets/javascripts/filtered_search/filtered_search_dropdown.js
View file @
f44fb5cf
...
...
@@ -35,7 +35,7 @@
if
(
!
dataValueSet
)
{
const
value
=
getValueFunction
(
selected
);
gl
.
FilteredSearchDropdownManager
.
addWordToInput
(
this
.
filter
,
value
);
gl
.
FilteredSearchDropdownManager
.
addWordToInput
(
this
.
filter
,
value
,
true
);
}
this
.
dismissDropdown
();
...
...
app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
View file @
f44fb5cf
...
...
@@ -58,35 +58,15 @@
};
}
static
addWordToInput
(
tokenName
,
tokenValue
=
''
)
{
static
addWordToInput
(
tokenName
,
tokenValue
=
''
,
clicked
=
false
)
{
const
input
=
document
.
querySelector
(
'
.filtered-search
'
);
const
inputValue
=
input
.
value
;
const
word
=
`
${
tokenName
}
:
${
tokenValue
}
`
;
// Get the string to replace
let
newCaretPosition
=
input
.
selectionStart
;
const
{
left
,
right
}
=
gl
.
DropdownUtils
.
getInputSelectionPosition
(
input
);
gl
.
FilteredSearchVisualTokens
.
addFilterVisualToken
(
tokenName
,
tokenValue
);
input
.
value
=
''
;
input
.
value
=
`
${
inputValue
.
substr
(
0
,
left
)}${
word
}${
inputValue
.
substr
(
right
)}
`
;
// If we have added a tokenValue at the end of the input,
// add a space and set selection to the end
if
(
right
>=
inputValue
.
length
&&
tokenValue
!==
''
)
{
input
.
value
+=
'
'
;
newCaretPosition
=
input
.
value
.
length
;
if
(
clicked
)
{
gl
.
FilteredSearchVisualTokens
.
moveInputToTheRight
();
}
gl
.
FilteredSearchDropdownManager
.
updateInputCaretPosition
(
newCaretPosition
,
input
);
}
static
updateInputCaretPosition
(
selectionStart
,
input
)
{
// Reset the position
// Sometimes can end up at end of input
input
.
setSelectionRange
(
selectionStart
,
selectionStart
);
const
{
right
}
=
gl
.
DropdownUtils
.
getInputSelectionPosition
(
input
);
input
.
setSelectionRange
(
right
,
right
);
}
updateCurrentDropdownOffset
()
{
...
...
@@ -94,19 +74,14 @@
}
updateDropdownOffset
(
key
)
{
if
(
!
this
.
font
)
{
this
.
font
=
window
.
getComputedStyle
(
this
.
filteredSearchInput
).
font
;
}
const
input
=
this
.
filteredSearchInput
;
const
inputText
=
input
.
value
.
slice
(
0
,
input
.
selectionStart
);
const
filterIconPadding
=
27
;
let
offset
=
gl
.
text
.
getTextWidth
(
inputText
,
this
.
font
)
+
filterIconPadding
;
// Always align dropdown with the input field
let
offset
=
this
.
filteredSearchInput
.
getBoundingClientRect
().
left
-
document
.
querySelector
(
'
.scroll-container
'
).
getBoundingClientRect
().
left
;
const
currentDropdownWidth
=
this
.
mapping
[
key
].
element
.
clientWidth
===
0
?
200
:
this
.
mapping
[
key
].
element
.
clientWidth
;
const
offsetMaxWidth
=
this
.
filteredSearchInput
.
clientWidth
-
currentDropdownWidth
;
const
maxInputWidth
=
240
;
const
currentDropdownWidth
=
this
.
mapping
[
key
].
element
.
clientWidth
||
maxInputWidth
;
// Make sure offset never exceeds the input container
const
offsetMaxWidth
=
document
.
querySelector
(
'
.scroll-container
'
).
clientWidth
-
currentDropdownWidth
;
if
(
offsetMaxWidth
<
offset
)
{
offset
=
offsetMaxWidth
;
}
...
...
@@ -164,8 +139,8 @@
}
setDropdown
()
{
const
{
lastToken
,
searchToken
}
=
this
.
tokenizer
.
processTokens
(
gl
.
DropdownUtils
.
getSearchInput
(
this
.
filteredSearchInput
)
);
const
query
=
gl
.
DropdownUtils
.
getSearchQuery
();
const
{
lastToken
,
searchToken
}
=
this
.
tokenizer
.
processTokens
(
query
);
if
(
this
.
currentDropdown
)
{
this
.
updateCurrentDropdownOffset
();
...
...
app/assets/javascripts/filtered_search/filtered_search_manager.js
View file @
f44fb5cf
...
...
@@ -3,6 +3,7 @@
constructor
(
page
)
{
this
.
filteredSearchInput
=
document
.
querySelector
(
'
.filtered-search
'
);
this
.
clearSearchButton
=
document
.
querySelector
(
'
.clear-search
'
);
this
.
tokensContainer
=
document
.
querySelector
(
'
.tokens-container
'
);
this
.
filteredSearchTokenKeys
=
gl
.
FilteredSearchTokenKeys
;
if
(
this
.
filteredSearchInput
)
{
...
...
@@ -27,36 +28,61 @@
this
.
handleFormSubmit
=
this
.
handleFormSubmit
.
bind
(
this
);
this
.
setDropdownWrapper
=
this
.
dropdownManager
.
setDropdown
.
bind
(
this
.
dropdownManager
);
this
.
toggleClearSearchButtonWrapper
=
this
.
toggleClearSearchButton
.
bind
(
this
);
this
.
handleInputPlaceholderWrapper
=
this
.
handleInputPlaceholder
.
bind
(
this
);
this
.
handleInputVisualTokenWrapper
=
this
.
handleInputVisualToken
.
bind
(
this
);
this
.
checkForEnterWrapper
=
this
.
checkForEnter
.
bind
(
this
);
this
.
clearSearchWrapper
=
this
.
clearSearch
.
bind
(
this
);
this
.
checkForBackspaceWrapper
=
this
.
checkForBackspace
.
bind
(
this
);
this
.
removeSelectedTokenWrapper
=
this
.
removeSelectedToken
.
bind
(
this
);
this
.
unselectEditTokensWrapper
=
this
.
unselectEditTokens
.
bind
(
this
);
this
.
tokenChange
=
this
.
tokenChange
.
bind
(
this
);
this
.
filteredSearchInput
.
form
.
addEventListener
(
'
submit
'
,
this
.
handleFormSubmit
);
this
.
filteredSearchInput
.
addEventListener
(
'
input
'
,
this
.
setDropdownWrapper
);
this
.
filteredSearchInput
.
addEventListener
(
'
input
'
,
this
.
toggleClearSearchButtonWrapper
);
this
.
filteredSearchInput
.
addEventListener
(
'
input
'
,
this
.
handleInputPlaceholderWrapper
);
this
.
filteredSearchInput
.
addEventListener
(
'
input
'
,
this
.
handleInputVisualTokenWrapper
);
this
.
filteredSearchInput
.
addEventListener
(
'
keydown
'
,
this
.
checkForEnterWrapper
);
this
.
filteredSearchInput
.
addEventListener
(
'
keyup
'
,
this
.
checkForBackspaceWrapper
);
this
.
filteredSearchInput
.
addEventListener
(
'
click
'
,
this
.
tokenChange
);
this
.
filteredSearchInput
.
addEventListener
(
'
keyup
'
,
this
.
tokenChange
);
this
.
tokensContainer
.
addEventListener
(
'
click
'
,
FilteredSearchManager
.
selectToken
);
this
.
tokensContainer
.
addEventListener
(
'
dblclick
'
,
FilteredSearchManager
.
editToken
);
this
.
clearSearchButton
.
addEventListener
(
'
click
'
,
this
.
clearSearchWrapper
);
document
.
addEventListener
(
'
click
'
,
gl
.
FilteredSearchVisualTokens
.
unselectTokens
);
document
.
addEventListener
(
'
click
'
,
this
.
unselectEditTokensWrapper
);
document
.
addEventListener
(
'
keydown
'
,
this
.
removeSelectedTokenWrapper
);
}
unbindEvents
()
{
this
.
filteredSearchInput
.
form
.
removeEventListener
(
'
submit
'
,
this
.
handleFormSubmit
);
this
.
filteredSearchInput
.
removeEventListener
(
'
input
'
,
this
.
setDropdownWrapper
);
this
.
filteredSearchInput
.
removeEventListener
(
'
input
'
,
this
.
toggleClearSearchButtonWrapper
);
this
.
filteredSearchInput
.
removeEventListener
(
'
input
'
,
this
.
handleInputPlaceholderWrapper
);
this
.
filteredSearchInput
.
removeEventListener
(
'
input
'
,
this
.
handleInputVisualTokenWrapper
);
this
.
filteredSearchInput
.
removeEventListener
(
'
keydown
'
,
this
.
checkForEnterWrapper
);
this
.
filteredSearchInput
.
removeEventListener
(
'
keyup
'
,
this
.
checkForBackspaceWrapper
);
this
.
filteredSearchInput
.
removeEventListener
(
'
click
'
,
this
.
tokenChange
);
this
.
filteredSearchInput
.
removeEventListener
(
'
keyup
'
,
this
.
tokenChange
);
this
.
tokensContainer
.
removeEventListener
(
'
click
'
,
FilteredSearchManager
.
selectToken
);
this
.
tokensContainer
.
removeEventListener
(
'
dblclick
'
,
FilteredSearchManager
.
editToken
);
this
.
clearSearchButton
.
removeEventListener
(
'
click
'
,
this
.
clearSearchWrapper
);
document
.
removeEventListener
(
'
click
'
,
gl
.
FilteredSearchVisualTokens
.
unselectTokens
);
document
.
removeEventListener
(
'
click
'
,
this
.
unselectEditTokensWrapper
);
document
.
removeEventListener
(
'
keydown
'
,
this
.
removeSelectedTokenWrapper
);
}
checkForBackspace
(
e
)
{
// 8 = Backspace Key
// 46 = Delete Key
if
(
e
.
keyCode
===
8
||
e
.
keyCode
===
46
)
{
const
{
lastVisualToken
}
=
gl
.
FilteredSearchVisualTokens
.
getLastVisualTokenBeforeInput
();
if
(
this
.
filteredSearchInput
.
value
===
''
&&
lastVisualToken
)
{
this
.
filteredSearchInput
.
value
=
gl
.
FilteredSearchVisualTokens
.
getLastTokenPartial
();
gl
.
FilteredSearchVisualTokens
.
removeLastTokenPartial
();
}
// Reposition dropdown so that it is aligned with cursor
this
.
dropdownManager
.
updateCurrentDropdownOffset
();
}
...
...
@@ -86,11 +112,67 @@
}
}
toggleClearSearchButton
(
e
)
{
if
(
e
.
target
.
value
)
{
this
.
clearSearchButton
.
classList
.
remove
(
'
hidden
'
);
}
else
{
this
.
clearSearchButton
.
classList
.
add
(
'
hidden
'
);
static
selectToken
(
e
)
{
const
button
=
e
.
target
.
closest
(
'
.selectable
'
);
if
(
button
)
{
e
.
preventDefault
();
e
.
stopPropagation
();
gl
.
FilteredSearchVisualTokens
.
selectToken
(
button
);
}
}
unselectEditTokens
(
e
)
{
const
inputContainer
=
document
.
querySelector
(
'
.filtered-search-input-container
'
);
const
isElementInFilteredSearch
=
inputContainer
&&
inputContainer
.
contains
(
e
.
target
);
const
isElementInFilterDropdown
=
e
.
target
.
closest
(
'
.filter-dropdown
'
)
!==
null
;
const
isElementTokensContainer
=
e
.
target
.
classList
.
contains
(
'
tokens-container
'
);
if
((
!
isElementInFilteredSearch
&&
!
isElementInFilterDropdown
)
||
isElementTokensContainer
)
{
gl
.
FilteredSearchVisualTokens
.
moveInputToTheRight
();
this
.
dropdownManager
.
resetDropdowns
();
}
}
static
editToken
(
e
)
{
const
token
=
e
.
target
.
closest
(
'
.js-visual-token
'
);
if
(
token
)
{
gl
.
FilteredSearchVisualTokens
.
editToken
(
token
);
}
}
toggleClearSearchButton
()
{
const
query
=
gl
.
DropdownUtils
.
getSearchQuery
();
const
hidden
=
'
hidden
'
;
const
hasHidden
=
this
.
clearSearchButton
.
classList
.
contains
(
hidden
);
if
(
query
.
length
===
0
&&
!
hasHidden
)
{
this
.
clearSearchButton
.
classList
.
add
(
hidden
);
}
else
if
(
query
.
length
&&
hasHidden
)
{
this
.
clearSearchButton
.
classList
.
remove
(
hidden
);
}
}
handleInputPlaceholder
()
{
const
query
=
gl
.
DropdownUtils
.
getSearchQuery
();
const
placeholder
=
'
Search or filter results...
'
;
const
currentPlaceholder
=
this
.
filteredSearchInput
.
placeholder
;
if
(
query
.
length
===
0
&&
currentPlaceholder
!==
placeholder
)
{
this
.
filteredSearchInput
.
placeholder
=
placeholder
;
}
else
if
(
query
.
length
>
0
&&
currentPlaceholder
!==
''
)
{
this
.
filteredSearchInput
.
placeholder
=
''
;
}
}
removeSelectedToken
(
e
)
{
// 8 = Backspace Key
// 46 = Delete Key
if
(
e
.
keyCode
===
8
||
e
.
keyCode
===
46
)
{
gl
.
FilteredSearchVisualTokens
.
removeSelectedToken
();
this
.
handleInputPlaceholder
();
this
.
toggleClearSearchButton
();
}
}
...
...
@@ -98,11 +180,67 @@
e
.
preventDefault
();
this
.
filteredSearchInput
.
value
=
''
;
const
removeElements
=
[];
[].
forEach
.
call
(
this
.
tokensContainer
.
children
,
(
t
)
=>
{
if
(
t
.
classList
.
contains
(
'
js-visual-token
'
))
{
removeElements
.
push
(
t
);
}
});
removeElements
.
forEach
((
el
)
=>
{
el
.
parentElement
.
removeChild
(
el
);
});
this
.
clearSearchButton
.
classList
.
add
(
'
hidden
'
);
this
.
handleInputPlaceholder
();
this
.
dropdownManager
.
resetDropdowns
();
}
handleInputVisualToken
()
{
const
input
=
this
.
filteredSearchInput
;
const
{
tokens
,
searchToken
}
=
gl
.
FilteredSearchTokenizer
.
processTokens
(
input
.
value
);
const
{
isLastVisualTokenValid
}
=
gl
.
FilteredSearchVisualTokens
.
getLastVisualTokenBeforeInput
();
if
(
isLastVisualTokenValid
)
{
tokens
.
forEach
((
t
)
=>
{
input
.
value
=
input
.
value
.
replace
(
`
${
t
.
key
}
:
${
t
.
symbol
}${
t
.
value
}
`
,
''
);
gl
.
FilteredSearchVisualTokens
.
addFilterVisualToken
(
t
.
key
,
`
${
t
.
symbol
}${
t
.
value
}
`
);
});
const
fragments
=
searchToken
.
split
(
'
:
'
);
if
(
fragments
.
length
>
1
)
{
const
inputValues
=
fragments
[
0
].
split
(
'
'
);
const
tokenKey
=
inputValues
.
last
();
if
(
inputValues
.
length
>
1
)
{
inputValues
.
pop
();
const
searchTerms
=
inputValues
.
join
(
'
'
);
input
.
value
=
input
.
value
.
replace
(
searchTerms
,
''
);
gl
.
FilteredSearchVisualTokens
.
addSearchVisualToken
(
searchTerms
);
}
gl
.
FilteredSearchVisualTokens
.
addFilterVisualToken
(
tokenKey
);
input
.
value
=
input
.
value
.
replace
(
`
${
tokenKey
}
:`
,
''
);
}
}
else
{
// Keep listening to token until we determine that the user is done typing the token value
const
valueCompletedRegex
=
/
([
~%@
]{0,1}
".+"
)
|
([
~%@
]{0,1}
'.+'
)
|^
((?![
~%@
]
'
)(?![
~%@
]
"
)(?!
'
)(?!
"
))
.*/g
;
if
(
searchToken
.
match
(
valueCompletedRegex
)
&&
input
.
value
[
input
.
value
.
length
-
1
]
===
'
'
)
{
gl
.
FilteredSearchVisualTokens
.
addFilterVisualToken
(
searchToken
);
// Trim the last space as seen in the if statement above
input
.
value
=
input
.
value
.
replace
(
searchToken
,
''
).
trim
();
}
}
}
handleFormSubmit
(
e
)
{
e
.
preventDefault
();
this
.
search
();
...
...
@@ -111,7 +249,7 @@
loadSearchParamsFromURL
()
{
const
params
=
gl
.
utils
.
getUrlParamsArray
();
const
usernameParams
=
this
.
getUsernameParams
();
const
inputValues
=
[]
;
let
hasFilteredSearch
=
false
;
params
.
forEach
((
p
)
=>
{
const
split
=
p
.
split
(
'
=
'
);
...
...
@@ -122,7 +260,8 @@
const
condition
=
this
.
filteredSearchTokenKeys
.
searchByConditionUrl
(
p
);
if
(
condition
)
{
inputValues
.
push
(
`
${
condition
.
tokenKey
}
:
${
condition
.
value
}
`
);
hasFilteredSearch
=
true
;
gl
.
FilteredSearchVisualTokens
.
addFilterVisualToken
(
condition
.
tokenKey
,
condition
.
value
);
}
else
{
// Sanitize value since URL converts spaces into +
// Replace before decode so that we know what was originally + versus the encoded +
...
...
@@ -140,34 +279,37 @@
quotationsToUse
=
sanitizedValue
.
indexOf
(
'
"
'
)
===
-
1
?
'
"
'
:
'
\'
'
;
}
inputValues
.
push
(
`
${
sanitizedKey
}
:
${
symbol
}${
quotationsToUse
}${
sanitizedValue
}${
quotationsToUse
}
`
);
hasFilteredSearch
=
true
;
gl
.
FilteredSearchVisualTokens
.
addFilterVisualToken
(
sanitizedKey
,
`
${
symbol
}${
quotationsToUse
}${
sanitizedValue
}${
quotationsToUse
}
`
);
}
else
if
(
!
match
&&
keyParam
===
'
assignee_id
'
)
{
const
id
=
parseInt
(
value
,
10
);
if
(
usernameParams
[
id
])
{
inputValues
.
push
(
`assignee:@
${
usernameParams
[
id
]}
`
);
hasFilteredSearch
=
true
;
gl
.
FilteredSearchVisualTokens
.
addFilterVisualToken
(
'
assignee
'
,
`@
${
usernameParams
[
id
]}
`
);
}
}
else
if
(
!
match
&&
keyParam
===
'
author_id
'
)
{
const
id
=
parseInt
(
value
,
10
);
if
(
usernameParams
[
id
])
{
inputValues
.
push
(
`author:@
${
usernameParams
[
id
]}
`
);
hasFilteredSearch
=
true
;
gl
.
FilteredSearchVisualTokens
.
addFilterVisualToken
(
'
author
'
,
`@
${
usernameParams
[
id
]}
`
);
}
}
else
if
(
!
match
&&
keyParam
===
'
search
'
)
{
inputValues
.
push
(
sanitizedValue
);
hasFilteredSearch
=
true
;
this
.
filteredSearchInput
.
value
=
sanitizedValue
;
}
}
});
// Trim the last space value
this
.
filteredSearchInput
.
value
=
inputValues
.
join
(
'
'
);
if
(
inputValues
.
length
>
0
)
{
if
(
hasFilteredSearch
)
{
this
.
clearSearchButton
.
classList
.
remove
(
'
hidden
'
);
this
.
handleInputPlaceholder
();
}
}
search
()
{
const
paths
=
[];
const
{
tokens
,
searchToken
}
=
this
.
tokenizer
.
processTokens
(
this
.
filteredSearchInput
.
value
);
const
{
tokens
,
searchToken
}
=
this
.
tokenizer
.
processTokens
(
gl
.
DropdownUtils
.
getSearchQuery
());
const
currentState
=
gl
.
utils
.
getParameterByName
(
'
state
'
)
||
'
opened
'
;
paths
.
push
(
`state=
${
currentState
}
`
);
...
...
app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
0 → 100644
View file @
f44fb5cf
class
FilteredSearchVisualTokens
{
static
getLastVisualTokenBeforeInput
()
{
const
inputLi
=
document
.
querySelector
(
'
.input-token
'
);
const
lastVisualToken
=
inputLi
&&
inputLi
.
previousElementSibling
;
return
{
lastVisualToken
,
isLastVisualTokenValid
:
lastVisualToken
===
null
||
lastVisualToken
.
className
.
indexOf
(
'
filtered-search-term
'
)
!==
-
1
||
(
lastVisualToken
&&
lastVisualToken
.
querySelector
(
'
.value
'
)
!==
null
),
};
}
static
unselectTokens
()
{
const
otherTokens
=
document
.
querySelectorAll
(
'
.js-visual-token .selectable.selected
'
);
[].
forEach
.
call
(
otherTokens
,
t
=>
t
.
classList
.
remove
(
'
selected
'
));
}
static
selectToken
(
tokenButton
)
{
const
selected
=
tokenButton
.
classList
.
contains
(
'
selected
'
);
FilteredSearchVisualTokens
.
unselectTokens
();
if
(
!
selected
)
{
tokenButton
.
classList
.
add
(
'
selected
'
);
}
}
static
removeSelectedToken
()
{
const
selected
=
document
.
querySelector
(
'
.js-visual-token .selected
'
);
if
(
selected
)
{
const
li
=
selected
.
closest
(
'
.js-visual-token
'
);
li
.
parentElement
.
removeChild
(
li
);
}
}
static
createVisualTokenElementHTML
()
{
return
`
<div class="selectable" role="button">
<div class="name"></div>
<div class="value"></div>
</div>
`
;
}
static
addVisualTokenElement
(
name
,
value
,
isSearchTerm
)
{
const
li
=
document
.
createElement
(
'
li
'
);
li
.
classList
.
add
(
'
js-visual-token
'
);
li
.
classList
.
add
(
isSearchTerm
?
'
filtered-search-term
'
:
'
filtered-search-token
'
);
if
(
value
)
{
li
.
innerHTML
=
FilteredSearchVisualTokens
.
createVisualTokenElementHTML
();
li
.
querySelector
(
'
.value
'
).
innerText
=
value
;
}
else
{
li
.
innerHTML
=
'
<div class="name"></div>
'
;
}
li
.
querySelector
(
'
.name
'
).
innerText
=
name
;
const
tokensContainer
=
document
.
querySelector
(
'
.tokens-container
'
);
const
input
=
document
.
querySelector
(
'
.filtered-search
'
);
tokensContainer
.
insertBefore
(
li
,
input
.
parentElement
);
}
static
addValueToPreviousVisualTokenElement
(
value
)
{
const
{
lastVisualToken
,
isLastVisualTokenValid
}
=
FilteredSearchVisualTokens
.
getLastVisualTokenBeforeInput
();
if
(
!
isLastVisualTokenValid
&&
lastVisualToken
.
classList
.
contains
(
'
filtered-search-token
'
))
{
const
name
=
FilteredSearchVisualTokens
.
getLastTokenPartial
();
lastVisualToken
.
innerHTML
=
FilteredSearchVisualTokens
.
createVisualTokenElementHTML
();
lastVisualToken
.
querySelector
(
'
.name
'
).
innerText
=
name
;
lastVisualToken
.
querySelector
(
'
.value
'
).
innerText
=
value
;
}
}
static
addFilterVisualToken
(
tokenName
,
tokenValue
)
{
const
{
lastVisualToken
,
isLastVisualTokenValid
}
=
FilteredSearchVisualTokens
.
getLastVisualTokenBeforeInput
();
const
addVisualTokenElement
=
FilteredSearchVisualTokens
.
addVisualTokenElement
;
if
(
isLastVisualTokenValid
)
{
addVisualTokenElement
(
tokenName
,
tokenValue
);
}
else
{
const
previousTokenName
=
lastVisualToken
.
querySelector
(
'
.name
'
).
innerText
;
const
tokensContainer
=
document
.
querySelector
(
'
.tokens-container
'
);
tokensContainer
.
removeChild
(
lastVisualToken
);
const
value
=
tokenValue
||
tokenName
;
addVisualTokenElement
(
previousTokenName
,
value
);
}
}
static
addSearchVisualToken
(
searchTerm
)
{
const
{
lastVisualToken
}
=
FilteredSearchVisualTokens
.
getLastVisualTokenBeforeInput
();
if
(
lastVisualToken
&&
lastVisualToken
.
classList
.
contains
(
'
filtered-search-term
'
))
{
lastVisualToken
.
querySelector
(
'
.name
'
).
innerText
+=
`
${
searchTerm
}
`
;
}
else
{
FilteredSearchVisualTokens
.
addVisualTokenElement
(
searchTerm
,
null
,
true
);
}
}
static
getLastTokenPartial
()
{
const
{
lastVisualToken
}
=
FilteredSearchVisualTokens
.
getLastVisualTokenBeforeInput
();
if
(
!
lastVisualToken
)
return
''
;
const
value
=
lastVisualToken
.
querySelector
(
'
.value
'
);
const
name
=
lastVisualToken
.
querySelector
(
'
.name
'
);
const
valueText
=
value
?
value
.
innerText
:
''
;
const
nameText
=
name
?
name
.
innerText
:
''
;
return
valueText
||
nameText
;
}
static
removeLastTokenPartial
()
{
const
{
lastVisualToken
}
=
FilteredSearchVisualTokens
.
getLastVisualTokenBeforeInput
();
if
(
lastVisualToken
)
{
const
value
=
lastVisualToken
.
querySelector
(
'
.value
'
);
if
(
value
)
{
const
button
=
lastVisualToken
.
querySelector
(
'
.selectable
'
);
button
.
removeChild
(
value
);
lastVisualToken
.
innerHTML
=
button
.
innerHTML
;
}
else
{
lastVisualToken
.
closest
(
'
.tokens-container
'
).
removeChild
(
lastVisualToken
);
}
}
}
static
tokenizeInput
()
{
const
input
=
document
.
querySelector
(
'
.filtered-search
'
);
const
{
isLastVisualTokenValid
}
=
gl
.
FilteredSearchVisualTokens
.
getLastVisualTokenBeforeInput
();
if
(
input
.
value
)
{
if
(
isLastVisualTokenValid
)
{
gl
.
FilteredSearchVisualTokens
.
addSearchVisualToken
(
input
.
value
);
}
else
{
FilteredSearchVisualTokens
.
addValueToPreviousVisualTokenElement
(
input
.
value
);
}
input
.
value
=
''
;
}
}
static
editToken
(
token
)
{
const
input
=
document
.
querySelector
(
'
.filtered-search
'
);
FilteredSearchVisualTokens
.
tokenizeInput
();
// Replace token with input field
const
tokenContainer
=
token
.
parentElement
;
const
inputLi
=
input
.
parentElement
;
tokenContainer
.
replaceChild
(
inputLi
,
token
);
const
name
=
token
.
querySelector
(
'
.name
'
);
const
value
=
token
.
querySelector
(
'
.value
'
);
if
(
token
.
classList
.
contains
(
'
filtered-search-token
'
))
{
FilteredSearchVisualTokens
.
addFilterVisualToken
(
name
.
innerText
);
input
.
value
=
value
.
innerText
;
}
else
{
// token is a search term
input
.
value
=
name
.
innerText
;
}
// Opens dropdown
const
inputEvent
=
new
Event
(
'
input
'
);
input
.
dispatchEvent
(
inputEvent
);
// Adds cursor to input
input
.
focus
();
}
static
moveInputToTheRight
()
{
const
input
=
document
.
querySelector
(
'
.filtered-search
'
);
const
inputLi
=
input
.
parentElement
;
const
tokenContainer
=
document
.
querySelector
(
'
.tokens-container
'
);
if
(
!
tokenContainer
.
lastElementChild
.
isEqualNode
(
inputLi
))
{
FilteredSearchVisualTokens
.
tokenizeInput
();
const
{
isLastVisualTokenValid
}
=
gl
.
FilteredSearchVisualTokens
.
getLastVisualTokenBeforeInput
();
if
(
!
isLastVisualTokenValid
)
{
const
lastPartial
=
gl
.
FilteredSearchVisualTokens
.
getLastTokenPartial
();
gl
.
FilteredSearchVisualTokens
.
removeLastTokenPartial
();
gl
.
FilteredSearchVisualTokens
.
addSearchVisualToken
(
lastPartial
);
}
tokenContainer
.
removeChild
(
inputLi
);
tokenContainer
.
appendChild
(
inputLi
);
}
}
}
window
.
gl
=
window
.
gl
||
{};
gl
.
FilteredSearchVisualTokens
=
FilteredSearchVisualTokens
;
app/assets/stylesheets/framework/filters.scss
View file @
f44fb5cf
...
...
@@ -64,6 +64,89 @@
-webkit-flex-direction
:
column
;
flex-direction
:
column
;
}
.tokens-container
{
display
:
-
webkit-flex
;
display
:
flex
;
flex
:
1
;
-webkit-flex
:
1
;
padding-left
:
30px
;
position
:
relative
;
margin-bottom
:
0
;
}
.input-token
{
flex
:
1
;
-webkit-flex
:
1
;
}
.filtered-search-token
+
.input-token
:not
(
:last-child
)
{
max-width
:
200px
;
}
}
.filtered-search-token
,
.filtered-search-term
{
display
:
-
webkit-flex
;
display
:
flex
;
margin-top
:
5px
;
margin-bottom
:
5px
;
.selectable
{
display
:
-
webkit-flex
;
display
:
flex
;
}
.name
,
.value
{
display
:
inline-block
;
padding
:
2px
7px
;
}
.name
{
background-color
:
$filter-name-resting-color
;
color
:
$filter-name-text-color
;
border-radius
:
2px
0
0
2px
;
margin-right
:
1px
;
text-transform
:
capitalize
;
}
.value
{
background-color
:
$white-normal
;
color
:
$filter-value-text-color
;
border-radius
:
0
2px
2px
0
;
margin-right
:
5px
;
}
.selected
{
.name
{
background-color
:
$filter-name-selected-color
;
}
.value
{
background-color
:
$filter-value-selected-color
;
}
}
}
.filtered-search-term
{
.name
{
background-color
:
inherit
;
color
:
$black
;
text-transform
:
none
;
}
.selectable
{
cursor
:
text
;
}
}
.scroll-container
{
display
:
-
webkit-flex
;
display
:
flex
;
overflow-x
:
scroll
;
white-space
:
nowrap
;
width
:
100%
;
}
.filtered-search-input-container
{
...
...
@@ -71,6 +154,9 @@
display
:
flex
;
position
:
relative
;
width
:
100%
;
border
:
1px
solid
$border-color
;
background-color
:
$white-light
;
max-width
:
87%
;
@media
(
max-width
:
$screen-xs-min
)
{
-webkit-flex
:
1
1
100%
;
...
...
@@ -87,12 +173,22 @@
}
.form-control
{
padding-left
:
25px
;
position
:
relative
;
min-width
:
200px
;
padding-left
:
0
;
padding-right
:
25px
;
border-color
:
transparent
;
&
:focus
~
.fa-filter
{
color
:
$common-gray-dark
;
}
&
:focus
,
&
:hover
{
outline
:
none
;
border-color
:
transparent
;
box-shadow
:
none
;
}
}
.fa-filter
{
...
...
@@ -109,12 +205,13 @@
.clear-search
{
width
:
35px
;
background-color
:
transparen
t
;
background-color
:
$white-ligh
t
;
border
:
none
;
position
:
absolute
;
right
:
0
;
height
:
100%
;
outline
:
none
;
z-index
:
1
;
&
:hover
.fa-times
{
color
:
$common-gray-dark
;
...
...
app/assets/stylesheets/framework/variables.scss
View file @
f44fb5cf
...
...
@@ -540,3 +540,12 @@ Pipeline Graph
$stage-hover-bg
:
#eaf3fc
;
$stage-hover-border
:
#d1e7fc
;
$action-icon-color
:
#d6d6d6
;
/*
Filtered Search
*/
$filter-name-resting-color
:
#f8f8f8
;
$filter-name-text-color
:
rgba
(
0
,
0
,
0
,
0
.55
);
$filter-value-text-color
:
rgba
(
0
,
0
,
0
,
0
.85
);
$filter-name-selected-color
:
#ebebeb
;
$filter-value-selected-color
:
#d7d7d7
;
app/views/shared/issuable/_search_bar.html.haml
View file @
f44fb5cf
...
...
@@ -11,10 +11,13 @@
class:
"check_all_issues left"
.issues-other-filters.filtered-search-container
.filtered-search-input-container
%input
.form-control.filtered-search
{
placeholder:
'Search or filter results...'
,
'data-id'
=>
'filtered-search'
,
'data-project-id'
=>
@project
.
id
,
'data-username-params'
=>
@users
.
to_json
(
only:
[
:id
,
:username
]),
'data-base-endpoint'
=>
namespace_project_path
(
@project
.
namespace
,
@project
)
}
=
icon
(
'filter'
)
%button
.clear-search.hidden
{
type:
'button'
}
=
icon
(
'times'
)
.scroll-container
%ul
.tokens-container.list-unstyled
%li
.input-token
%input
.form-control.filtered-search
{
placeholder:
'Search or filter results...'
,
'data-id'
=>
'filtered-search'
,
'data-project-id'
=>
@project
.
id
,
'data-username-params'
=>
@users
.
to_json
(
only:
[
:id
,
:username
]),
'data-base-endpoint'
=>
namespace_project_path
(
@project
.
namespace
,
@project
)
}
=
icon
(
'filter'
)
%button
.clear-search.hidden
{
type:
'button'
}
=
icon
(
'times'
)
#js-dropdown-hint
.dropdown-menu.hint-dropdown
%ul
{
'data-dropdown'
=>
true
}
%li
.filter-dropdown-item
{
'data-action'
=>
'submit'
}
...
...
spec/features/issues/filtered_search/dropdown_assignee_spec.rb
View file @
f44fb5cf
require
'rails_helper'
describe
'Dropdown assignee'
,
:feature
,
:js
do
include
FilteredSearchHelpers
include
WaitForAjax
let!
(
:project
)
{
create
(
:empty_project
)
}
let!
(
:user
)
{
create
(
:user
,
name:
'administrator'
,
username:
'root'
)
}
let!
(
:user_john
)
{
create
(
:user
,
name:
'John'
,
username:
'th0mas'
)
}
...
...
@@ -133,7 +136,8 @@ describe 'Dropdown assignee', :feature, :js do
click_assignee
(
user_jacob
.
name
)
expect
(
page
).
to
have_css
(
js_dropdown_assignee
,
visible:
false
)
expect
(
filtered_search
.
value
).
to
eq
(
"assignee:@
#{
user_jacob
.
username
}
"
)
expect_tokens
([{
name:
'assignee'
,
value:
"@
#{
user_jacob
.
username
}
"
}])
expect_filtered_search_input_empty
end
it
'fills in the assignee username when the assignee has been filtered'
do
...
...
@@ -141,14 +145,16 @@ describe 'Dropdown assignee', :feature, :js do
click_assignee
(
user
.
name
)
expect
(
page
).
to
have_css
(
js_dropdown_assignee
,
visible:
false
)
expect
(
filtered_search
.
value
).
to
eq
(
"assignee:@
#{
user
.
username
}
"
)
expect_tokens
([{
name:
'assignee'
,
value:
"@
#{
user
.
username
}
"
}])
expect_filtered_search_input_empty
end
it
'selects `no assignee`'
do
find
(
'#js-dropdown-assignee .filter-dropdown-item'
,
text:
'No Assignee'
).
click
expect
(
page
).
to
have_css
(
js_dropdown_assignee
,
visible:
false
)
expect
(
filtered_search
.
value
).
to
eq
(
"assignee:none "
)
expect_tokens
([{
name:
'assignee'
,
value:
'none'
}])
expect_filtered_search_input_empty
end
end
...
...
spec/features/issues/filtered_search/dropdown_author_spec.rb
View file @
f44fb5cf
require
'rails_helper'
describe
'Dropdown author'
,
js:
true
,
feature:
true
do
include
FilteredSearchHelpers
include
WaitForAjax
let!
(
:project
)
{
create
(
:empty_project
)
}
...
...
@@ -121,14 +122,16 @@ describe 'Dropdown author', js: true, feature: true do
click_author
(
user_jacob
.
name
)
expect
(
page
).
to
have_css
(
js_dropdown_author
,
visible:
false
)
expect
(
filtered_search
.
value
).
to
eq
(
"author:@
#{
user_jacob
.
username
}
"
)
expect_tokens
([{
name:
'author'
,
value:
"@
#{
user_jacob
.
username
}
"
}])
expect_filtered_search_input_empty
end
it
'fills in the author username when the author has been filtered'
do
click_author
(
user
.
name
)
expect
(
page
).
to
have_css
(
js_dropdown_author
,
visible:
false
)
expect
(
filtered_search
.
value
).
to
eq
(
"author:@
#{
user
.
username
}
"
)
expect_tokens
([{
name:
'author'
,
value:
"@
#{
user
.
username
}
"
}])
expect_filtered_search_input_empty
end
end
...
...
spec/features/issues/filtered_search/dropdown_hint_spec.rb
View file @
f44fb5cf
require
'rails_helper'
describe
'Dropdown hint'
,
js:
true
,
feature:
true
do
include
FilteredSearchHelpers
include
WaitForAjax
let!
(
:project
)
{
create
(
:empty_project
)
}
...
...
@@ -66,7 +67,8 @@ describe 'Dropdown hint', js: true, feature: true do
expect
(
page
).
to
have_css
(
js_dropdown_hint
,
visible:
false
)
expect
(
page
).
to
have_css
(
'#js-dropdown-author'
,
visible:
true
)
expect
(
filtered_search
.
value
).
to
eq
(
'author:'
)
expect_tokens
([{
name:
'author'
}])
expect_filtered_search_input_empty
end
it
'opens the assignee dropdown when you click on assignee'
do
...
...
@@ -74,7 +76,8 @@ describe 'Dropdown hint', js: true, feature: true do
expect
(
page
).
to
have_css
(
js_dropdown_hint
,
visible:
false
)
expect
(
page
).
to
have_css
(
'#js-dropdown-assignee'
,
visible:
true
)
expect
(
filtered_search
.
value
).
to
eq
(
'assignee:'
)
expect_tokens
([{
name:
'assignee'
}])
expect_filtered_search_input_empty
end
it
'opens the milestone dropdown when you click on milestone'
do
...
...
@@ -82,7 +85,8 @@ describe 'Dropdown hint', js: true, feature: true do
expect
(
page
).
to
have_css
(
js_dropdown_hint
,
visible:
false
)
expect
(
page
).
to
have_css
(
'#js-dropdown-milestone'
,
visible:
true
)
expect
(
filtered_search
.
value
).
to
eq
(
'milestone:'
)
expect_tokens
([{
name:
'milestone'
}])
expect_filtered_search_input_empty
end
it
'opens the label dropdown when you click on label'
do
...
...
@@ -90,7 +94,8 @@ describe 'Dropdown hint', js: true, feature: true do
expect
(
page
).
to
have_css
(
js_dropdown_hint
,
visible:
false
)
expect
(
page
).
to
have_css
(
'#js-dropdown-label'
,
visible:
true
)
expect
(
filtered_search
.
value
).
to
eq
(
'label:'
)
expect_tokens
([{
name:
'label'
}])
expect_filtered_search_input_empty
end
end
...
...
@@ -101,7 +106,8 @@ describe 'Dropdown hint', js: true, feature: true do
expect
(
page
).
to
have_css
(
js_dropdown_hint
,
visible:
false
)
expect
(
page
).
to
have_css
(
'#js-dropdown-author'
,
visible:
true
)
expect
(
filtered_search
.
value
).
to
eq
(
'author:'
)
expect_tokens
([{
name:
'author'
}])
expect_filtered_search_input_empty
end
it
'opens the assignee dropdown when you click on assignee'
do
...
...
@@ -110,7 +116,8 @@ describe 'Dropdown hint', js: true, feature: true do
expect
(
page
).
to
have_css
(
js_dropdown_hint
,
visible:
false
)
expect
(
page
).
to
have_css
(
'#js-dropdown-assignee'
,
visible:
true
)
expect
(
filtered_search
.
value
).
to
eq
(
'assignee:'
)
expect_tokens
([{
name:
'assignee'
}])
expect_filtered_search_input_empty
end
it
'opens the milestone dropdown when you click on milestone'
do
...
...
@@ -119,7 +126,8 @@ describe 'Dropdown hint', js: true, feature: true do
expect
(
page
).
to
have_css
(
js_dropdown_hint
,
visible:
false
)
expect
(
page
).
to
have_css
(
'#js-dropdown-milestone'
,
visible:
true
)
expect
(
filtered_search
.
value
).
to
eq
(
'milestone:'
)
expect_tokens
([{
name:
'milestone'
}])
expect_filtered_search_input_empty
end
it
'opens the label dropdown when you click on label'
do
...
...
@@ -128,7 +136,46 @@ describe 'Dropdown hint', js: true, feature: true do
expect
(
page
).
to
have_css
(
js_dropdown_hint
,
visible:
false
)
expect
(
page
).
to
have_css
(
'#js-dropdown-label'
,
visible:
true
)
expect
(
filtered_search
.
value
).
to
eq
(
'label:'
)
expect_tokens
([{
name:
'label'
}])
expect_filtered_search_input_empty
end
end
describe
'reselecting from dropdown'
do
it
'reuses existing author text'
do
filtered_search
.
send_keys
(
'author:'
)
filtered_search
.
send_keys
(
:backspace
)
click_hint
(
'author'
)
expect_tokens
([{
name:
'author'
}])
expect_filtered_search_input_empty
end
it
'reuses existing assignee text'
do
filtered_search
.
send_keys
(
'assignee:'
)
filtered_search
.
send_keys
(
:backspace
)
click_hint
(
'assignee'
)
expect_tokens
([{
name:
'assignee'
}])
expect_filtered_search_input_empty
end
it
'reuses existing milestone text'
do
filtered_search
.
send_keys
(
'milestone:'
)
filtered_search
.
send_keys
(
:backspace
)
click_hint
(
'milestone'
)
expect_tokens
([{
name:
'milestone'
}])
expect_filtered_search_input_empty
end
it
'reuses existing label text'
do
filtered_search
.
send_keys
(
'label:'
)
filtered_search
.
send_keys
(
:backspace
)
click_hint
(
'label'
)
expect_tokens
([{
name:
'label'
}])
expect_filtered_search_input_empty
end
end
end
spec/features/issues/filtered_search/dropdown_label_spec.rb
View file @
f44fb5cf
...
...
@@ -51,7 +51,8 @@ describe 'Dropdown label', js: true, feature: true do
filtered_search
.
native
.
send_keys
(
:down
,
:down
,
:enter
)
expect
(
filtered_search
.
value
).
to
eq
(
"label:~
#{
bug_label
.
title
}
"
)
expect_tokens
([{
name:
'label'
,
value:
"~
#{
bug_label
.
title
}
"
}])
expect_filtered_search_input_empty
end
end
...
...
@@ -92,7 +93,7 @@ describe 'Dropdown label', js: true, feature: true do
end
it
'filters by case-insensitive name with or without symbol'
do
search_for_label
(
'b'
)
filtered_search
.
send_keys
(
'b'
)
expect
(
filter_dropdown
.
find
(
'.filter-dropdown-item'
,
text:
bug_label
.
title
)).
to
be_visible
expect
(
filter_dropdown
.
find
(
'.filter-dropdown-item'
,
text:
uppercase_label
.
title
)).
to
be_visible
...
...
@@ -101,7 +102,7 @@ describe 'Dropdown label', js: true, feature: true do
clear_search_field
init_label_search
search_for_label
(
'~bu'
)
filtered_search
.
send_keys
(
'~bu'
)
expect
(
filter_dropdown
.
find
(
'.filter-dropdown-item'
,
text:
bug_label
.
title
)).
to
be_visible
expect
(
filter_dropdown
.
find
(
'.filter-dropdown-item'
,
text:
uppercase_label
.
title
)).
to
be_visible
...
...
@@ -180,7 +181,8 @@ describe 'Dropdown label', js: true, feature: true do
click_label
(
bug_label
.
title
)
expect
(
page
).
not_to
have_css
(
js_dropdown_label
)
expect
(
filtered_search
.
value
).
to
eq
(
"label:~
#{
bug_label
.
title
}
"
)
expect_tokens
([{
name:
'label'
,
value:
"~
#{
bug_label
.
title
}
"
}])
expect_filtered_search_input_empty
end
it
'fills in the label name when the label is partially filled'
do
...
...
@@ -188,49 +190,56 @@ describe 'Dropdown label', js: true, feature: true do
click_label
(
bug_label
.
title
)
expect
(
page
).
not_to
have_css
(
js_dropdown_label
)
expect
(
filtered_search
.
value
).
to
eq
(
"label:~
#{
bug_label
.
title
}
"
)
expect_tokens
([{
name:
'label'
,
value:
"~
#{
bug_label
.
title
}
"
}])
expect_filtered_search_input_empty
end
it
'fills in the label name that contains multiple words'
do
click_label
(
two_words_label
.
title
)
expect
(
page
).
not_to
have_css
(
js_dropdown_label
)
expect
(
filtered_search
.
value
).
to
eq
(
"label:~
\"
#{
two_words_label
.
title
}
\"
"
)
expect_tokens
([{
name:
'label'
,
value:
"
\"
#{
two_words_label
.
title
}
\"
"
}])
expect_filtered_search_input_empty
end
it
'fills in the label name that contains multiple words and is very long'
do
click_label
(
long_label
.
title
)
expect
(
page
).
not_to
have_css
(
js_dropdown_label
)
expect
(
filtered_search
.
value
).
to
eq
(
"label:~
\"
#{
long_label
.
title
}
\"
"
)
expect_tokens
([{
name:
'label'
,
value:
"
\"
#{
long_label
.
title
}
\"
"
}])
expect_filtered_search_input_empty
end
it
'fills in the label name that contains double quotes'
do
click_label
(
wont_fix_label
.
title
)
expect
(
page
).
not_to
have_css
(
js_dropdown_label
)
expect
(
filtered_search
.
value
).
to
eq
(
"label:~'
#{
wont_fix_label
.
title
}
' "
)
expect_tokens
([{
name:
'label'
,
value:
"~'
#{
wont_fix_label
.
title
}
'"
}])
expect_filtered_search_input_empty
end
it
'fills in the label name with the correct capitalization'
do
click_label
(
uppercase_label
.
title
)
expect
(
page
).
not_to
have_css
(
js_dropdown_label
)
expect
(
filtered_search
.
value
).
to
eq
(
"label:~
#{
uppercase_label
.
title
}
"
)
expect_tokens
([{
name:
'label'
,
value:
"~
#{
uppercase_label
.
title
}
"
}])
expect_filtered_search_input_empty
end
it
'fills in the label name with special characters'
do
click_label
(
special_label
.
title
)
expect
(
page
).
not_to
have_css
(
js_dropdown_label
)
expect
(
filtered_search
.
value
).
to
eq
(
"label:~
#{
special_label
.
title
}
"
)
expect_tokens
([{
name:
'label'
,
value:
"~
#{
special_label
.
title
}
"
}])
expect_filtered_search_input_empty
end
it
'selects `no label`'
do
find
(
"
#{
js_dropdown_label
}
.filter-dropdown-item"
,
text:
'No Label'
).
click
expect
(
page
).
not_to
have_css
(
js_dropdown_label
)
expect
(
filtered_search
.
value
).
to
eq
(
"label:none "
)
expect_tokens
([{
name:
'label'
,
value:
'none'
}])
expect_filtered_search_input_empty
end
end
...
...
spec/features/issues/filtered_search/dropdown_milestone_spec.rb
View file @
f44fb5cf
require
'rails_helper'
describe
'Dropdown milestone'
,
js:
true
,
feature:
true
do
include
FilteredSearchHelpers
include
WaitForAjax
let!
(
:project
)
{
create
(
:empty_project
)
}
...
...
@@ -127,7 +128,8 @@ describe 'Dropdown milestone', js: true, feature: true do
click_milestone
(
milestone
.
title
)
expect
(
page
).
to
have_css
(
js_dropdown_milestone
,
visible:
false
)
expect
(
filtered_search
.
value
).
to
eq
(
"milestone:%
#{
milestone
.
title
}
"
)
expect_tokens
([{
name:
'milestone'
,
value:
"%
#{
milestone
.
title
}
"
}])
expect_filtered_search_input_empty
end
it
'fills in the milestone name when the milestone is partially filled'
do
...
...
@@ -135,56 +137,64 @@ describe 'Dropdown milestone', js: true, feature: true do
click_milestone
(
milestone
.
title
)
expect
(
page
).
to
have_css
(
js_dropdown_milestone
,
visible:
false
)
expect
(
filtered_search
.
value
).
to
eq
(
"milestone:%
#{
milestone
.
title
}
"
)
expect_tokens
([{
name:
'milestone'
,
value:
"%
#{
milestone
.
title
}
"
}])
expect_filtered_search_input_empty
end
it
'fills in the milestone name that contains multiple words'
do
click_milestone
(
two_words_milestone
.
title
)
expect
(
page
).
to
have_css
(
js_dropdown_milestone
,
visible:
false
)
expect
(
filtered_search
.
value
).
to
eq
(
"milestone:%
\"
#{
two_words_milestone
.
title
}
\"
"
)
expect_tokens
([{
name:
'milestone'
,
value:
"%
\"
#{
two_words_milestone
.
title
}
\"
"
}])
expect_filtered_search_input_empty
end
it
'fills in the milestone name that contains multiple words and is very long'
do
click_milestone
(
long_milestone
.
title
)
expect
(
page
).
to
have_css
(
js_dropdown_milestone
,
visible:
false
)
expect
(
filtered_search
.
value
).
to
eq
(
"milestone:%
\"
#{
long_milestone
.
title
}
\"
"
)
expect_tokens
([{
name:
'milestone'
,
value:
"%
\"
#{
long_milestone
.
title
}
\"
"
}])
expect_filtered_search_input_empty
end
it
'fills in the milestone name that contains double quotes'
do
click_milestone
(
wont_fix_milestone
.
title
)
expect
(
page
).
to
have_css
(
js_dropdown_milestone
,
visible:
false
)
expect
(
filtered_search
.
value
).
to
eq
(
"milestone:%'
#{
wont_fix_milestone
.
title
}
' "
)
expect_tokens
([{
name:
'milestone'
,
value:
"%'
#{
wont_fix_milestone
.
title
}
'"
}])
expect_filtered_search_input_empty
end
it
'fills in the milestone name with the correct capitalization'
do
click_milestone
(
uppercase_milestone
.
title
)
expect
(
page
).
to
have_css
(
js_dropdown_milestone
,
visible:
false
)
expect
(
filtered_search
.
value
).
to
eq
(
"milestone:%
#{
uppercase_milestone
.
title
}
"
)
expect_tokens
([{
name:
'milestone'
,
value:
"%
#{
uppercase_milestone
.
title
}
"
}])
expect_filtered_search_input_empty
end
it
'fills in the milestone name with special characters'
do
click_milestone
(
special_milestone
.
title
)
expect
(
page
).
to
have_css
(
js_dropdown_milestone
,
visible:
false
)
expect
(
filtered_search
.
value
).
to
eq
(
"milestone:%
#{
special_milestone
.
title
}
"
)
expect_tokens
([{
name:
'milestone'
,
value:
"%
#{
special_milestone
.
title
}
"
}])
expect_filtered_search_input_empty
end
it
'selects `no milestone`'
do
click_static_milestone
(
'No Milestone'
)
expect
(
page
).
to
have_css
(
js_dropdown_milestone
,
visible:
false
)
expect
(
filtered_search
.
value
).
to
eq
(
"milestone:none "
)
expect_tokens
([{
name:
'milestone'
,
value:
'none'
}])
expect_filtered_search_input_empty
end
it
'selects `upcoming milestone`'
do
click_static_milestone
(
'Upcoming'
)
expect
(
page
).
to
have_css
(
js_dropdown_milestone
,
visible:
false
)
expect
(
filtered_search
.
value
).
to
eq
(
"milestone:upcoming "
)
expect_tokens
([{
name:
'milestone'
,
value:
'upcoming'
}])
expect_filtered_search_input_empty
end
end
...
...
spec/features/issues/filtered_search/filter_issues_spec.rb
View file @
f44fb5cf
require
'
rails
_helper'
require
'
spec
_helper'
describe
'Filter issues'
,
js:
true
,
feature:
true
do
include
FilteredSearchHelpers
...
...
@@ -97,7 +97,9 @@ describe 'Filter issues', js: true, feature: true do
it
'filters issues by searched author'
do
input_filtered_search
(
"author:@
#{
user
.
username
}
"
)
expect_tokens
([{
name:
'author'
,
value:
user
.
username
}])
expect_issues_list_count
(
5
)
expect_filtered_search_input_empty
end
it
'filters issues by invalid author'
do
...
...
@@ -110,36 +112,50 @@ describe 'Filter issues', js: true, feature: true do
end
context
'author with other filters'
do
search_term
=
'issue'
it
'filters issues by searched author and text'
do
search
=
"author:@
#{
user
.
username
}
issue"
input_filtered_search
(
search
)
input_filtered_search
(
"author:@
#{
user
.
username
}
#{
search_term
}
"
)
expect_tokens
([{
name:
'author'
,
value:
user
.
username
}])
expect_issues_list_count
(
3
)
expect_filtered_search_input
(
search
)
expect_filtered_search_input
(
search
_term
)
end
it
'filters issues by searched author, assignee and text'
do
search
=
"author:@
#{
user
.
username
}
assignee:@
#{
user
.
username
}
issue"
input_filtered_search
(
search
)
input_filtered_search
(
"author:@
#{
user
.
username
}
assignee:@
#{
user
.
username
}
#{
search_term
}
"
)
expect_tokens
([
{
name:
'author'
,
value:
user
.
username
},
{
name:
'assignee'
,
value:
user
.
username
}
])
expect_issues_list_count
(
3
)
expect_filtered_search_input
(
search
)
expect_filtered_search_input
(
search
_term
)
end
it
'filters issues by searched author, assignee, label, and text'
do
search
=
"author:@
#{
user
.
username
}
assignee:@
#{
user
.
username
}
label:~
#{
caps_sensitive_label
.
title
}
issue"
input_filtered_search
(
search
)
input_filtered_search
(
"author:@
#{
user
.
username
}
assignee:@
#{
user
.
username
}
label:~
#{
caps_sensitive_label
.
title
}
#{
search_term
}
"
)
expect_tokens
([
{
name:
'author'
,
value:
user
.
username
},
{
name:
'assignee'
,
value:
user
.
username
},
{
name:
'label'
,
value:
caps_sensitive_label
.
title
}
])
expect_issues_list_count
(
1
)
expect_filtered_search_input
(
search
)
expect_filtered_search_input
(
search
_term
)
end
it
'filters issues by searched author, assignee, label, milestone and text'
do
search
=
"author:@
#{
user
.
username
}
assignee:@
#{
user
.
username
}
label:~
#{
caps_sensitive_label
.
title
}
milestone:%
#{
milestone
.
title
}
issue"
input_filtered_search
(
search
)
input_filtered_search
(
"author:@
#{
user
.
username
}
assignee:@
#{
user
.
username
}
label:~
#{
caps_sensitive_label
.
title
}
milestone:%
#{
milestone
.
title
}
#{
search_term
}
"
)
expect_tokens
([
{
name:
'author'
,
value:
user
.
username
},
{
name:
'assignee'
,
value:
user
.
username
},
{
name:
'label'
,
value:
caps_sensitive_label
.
title
},
{
name:
'milestone'
,
value:
milestone
.
title
}
])
expect_issues_list_count
(
1
)
expect_filtered_search_input
(
search
)
expect_filtered_search_input
(
search
_term
)
end
end
...
...
@@ -151,19 +167,19 @@ describe 'Filter issues', js: true, feature: true do
describe
'filter issues by assignee'
do
context
'only assignee'
do
it
'filters issues by searched assignee'
do
search
=
"assignee:@
#{
user
.
username
}
"
input_filtered_search
(
search
)
input_filtered_search
(
"assignee:@
#{
user
.
username
}
"
)
expect_tokens
([{
name:
'assignee'
,
value:
user
.
username
}])
expect_issues_list_count
(
5
)
expect_filtered_search_input
(
search
)
expect_filtered_search_input
_empty
end
it
'filters issues by no assignee'
do
search
=
"assignee:none"
input_filtered_search
(
search
)
input_filtered_search
(
'assignee:none'
)
expect_tokens
([{
name:
'assignee'
,
value:
'none'
}])
expect_issues_list_count
(
8
,
1
)
expect_filtered_search_input
(
search
)
expect_filtered_search_input
_empty
end
it
'filters issues by invalid assignee'
do
...
...
@@ -176,36 +192,50 @@ describe 'Filter issues', js: true, feature: true do
end
context
'assignee with other filters'
do
let
(
:search_term
)
{
'searchTerm'
}
it
'filters issues by searched assignee and text'
do
search
=
"assignee:@
#{
user
.
username
}
searchTerm"
input_filtered_search
(
search
)
input_filtered_search
(
"assignee:@
#{
user
.
username
}
#{
search_term
}
"
)
expect_tokens
([{
name:
'assignee'
,
value:
user
.
username
}])
expect_issues_list_count
(
2
)
expect_filtered_search_input
(
search
)
expect_filtered_search_input
(
search
_term
)
end
it
'filters issues by searched assignee, author and text'
do
search
=
"assignee:@
#{
user
.
username
}
author:@
#{
user
.
username
}
searchTerm"
input_filtered_search
(
search
)
input_filtered_search
(
"assignee:@
#{
user
.
username
}
author:@
#{
user
.
username
}
#{
search_term
}
"
)
expect_tokens
([
{
name:
'assignee'
,
value:
user
.
username
},
{
name:
'author'
,
value:
user
.
username
}
])
expect_issues_list_count
(
2
)
expect_filtered_search_input
(
search
)
expect_filtered_search_input
(
search
_term
)
end
it
'filters issues by searched assignee, author, label, text'
do
search
=
"assignee:@
#{
user
.
username
}
author:@
#{
user
.
username
}
label:~
#{
caps_sensitive_label
.
title
}
searchTerm"
input_filtered_search
(
search
)
input_filtered_search
(
"assignee:@
#{
user
.
username
}
author:@
#{
user
.
username
}
label:~
#{
caps_sensitive_label
.
title
}
#{
search_term
}
"
)
expect_tokens
([
{
name:
'assignee'
,
value:
user
.
username
},
{
name:
'author'
,
value:
user
.
username
},
{
name:
'label'
,
value:
caps_sensitive_label
.
title
}
])
expect_issues_list_count
(
1
)
expect_filtered_search_input
(
search
)
expect_filtered_search_input
(
search
_term
)
end
it
'filters issues by searched assignee, author, label, milestone and text'
do
search
=
"assignee:@
#{
user
.
username
}
author:@
#{
user
.
username
}
label:~
#{
caps_sensitive_label
.
title
}
milestone:%
#{
milestone
.
title
}
searchTerm"
input_filtered_search
(
search
)
input_filtered_search
(
"assignee:@
#{
user
.
username
}
author:@
#{
user
.
username
}
label:~
#{
caps_sensitive_label
.
title
}
milestone:%
#{
milestone
.
title
}
#{
search_term
}
"
)
expect_tokens
([
{
name:
'assignee'
,
value:
user
.
username
},
{
name:
'author'
,
value:
user
.
username
},
{
name:
'label'
,
value:
caps_sensitive_label
.
title
},
{
name:
'milestone'
,
value:
milestone
.
title
}
])
expect_issues_list_count
(
1
)
expect_filtered_search_input
(
search
)
expect_filtered_search_input
(
search
_term
)
end
end
...
...
@@ -217,21 +247,23 @@ describe 'Filter issues', js: true, feature: true do
end
describe
'filter issues by label'
do
let
(
:search_term
)
{
'bug'
}
context
'only label'
do
it
'filters issues by searched label'
do
search
=
"label:~
#{
bug_label
.
title
}
"
input_filtered_search
(
search
)
input_filtered_search
(
"label:~
#{
bug_label
.
title
}
"
)
expect_tokens
([{
name:
'label'
,
value:
bug_label
.
title
}])
expect_issues_list_count
(
2
)
expect_filtered_search_input
(
search
)
expect_filtered_search_input
_empty
end
it
'filters issues by no label'
do
search
=
"label:none"
input_filtered_search
(
search
)
input_filtered_search
(
'label:none'
)
expect_tokens
([{
name:
'label'
,
value:
'none'
}])
expect_issues_list_count
(
9
,
1
)
expect_filtered_search_input
(
search
)
expect_filtered_search_input
_empty
end
it
'filters issues by invalid label'
do
...
...
@@ -239,11 +271,14 @@ describe 'Filter issues', js: true, feature: true do
end
it
'filters issues by multiple labels'
do
search
=
"label:~
#{
bug_label
.
title
}
label:~
#{
caps_sensitive_label
.
title
}
"
input_filtered_search
(
search
)
input_filtered_search
(
"label:~
#{
bug_label
.
title
}
label:~
#{
caps_sensitive_label
.
title
}
"
)
expect_tokens
([
{
name:
'label'
,
value:
bug_label
.
title
},
{
name:
'label'
,
value:
caps_sensitive_label
.
title
}
])
expect_issues_list_count
(
1
)
expect_filtered_search_input
(
search
)
expect_filtered_search_input
_empty
end
it
'filters issues by label containing special characters'
do
...
...
@@ -251,21 +286,20 @@ describe 'Filter issues', js: true, feature: true do
special_issue
=
create
(
:issue
,
title:
"Issue with special character label"
,
project:
project
)
special_issue
.
labels
<<
special_label
search
=
"label:~
#{
special_label
.
title
}
"
input_filtered_search
(
search
)
input_filtered_search
(
"label:~
#{
special_label
.
title
}
"
)
expect_tokens
([{
name:
'label'
,
value:
special_label
.
title
}])
expect_issues_list_count
(
1
)
expect_filtered_search_input
(
search
)
expect_filtered_search_input
_empty
end
it
'does not show issues'
do
new_label
=
create
(
:label
,
project:
project
,
title:
"new_label"
)
new_label
=
create
(
:label
,
project:
project
,
title:
'new_label'
)
search
=
"label:~
#{
new_label
.
title
}
"
input_filtered_search
(
search
)
input_filtered_search
(
"label:~
#{
new_label
.
title
}
"
)
expect_tokens
([{
name:
'label'
,
value:
new_label
.
title
}])
expect_no_issues_list
()
expect_filtered_search_input
(
search
)
expect_filtered_search_input
_empty
end
end
...
...
@@ -275,29 +309,29 @@ describe 'Filter issues', js: true, feature: true do
special_multiple_issue
=
create
(
:issue
,
title:
"Issue with special character multiple words label"
,
project:
project
)
special_multiple_issue
.
labels
<<
special_multiple_label
search
=
"label:~'
#{
special_multiple_label
.
title
}
'"
input_filtered_search
(
search
)
input_filtered_search
(
"label:~'
#{
special_multiple_label
.
title
}
'"
)
# filtered search defaults quotations to double quotes
expect_tokens
([{
name:
'label'
,
value:
"
\"
#{
special_multiple_label
.
title
}
\"
"
}])
expect_issues_list_count
(
1
)
# filtered search defaults quotations to double quotes
expect_filtered_search_input
(
"label:~
\"
#{
special_multiple_label
.
title
}
\"
"
)
expect_filtered_search_input_empty
end
it
'single quotes'
do
search
=
"label:~'
#{
multiple_words_label
.
title
}
'"
input_filtered_search
(
search
)
input_filtered_search
(
"label:~'
#{
multiple_words_label
.
title
}
'"
)
expect_tokens
([{
name:
'label'
,
value:
"
\"
#{
multiple_words_label
.
title
}
\"
"
}])
expect_issues_list_count
(
1
)
expect_filtered_search_input
(
"label:~
\"
#{
multiple_words_label
.
title
}
\"
"
)
expect_filtered_search_input
_empty
end
it
'double quotes'
do
search
=
"label:~
\"
#{
multiple_words_label
.
title
}
\"
"
input_filtered_search
(
search
)
input_filtered_search
(
"label:~
\"
#{
multiple_words_label
.
title
}
\"
"
)
expect_tokens
([{
name:
'label'
,
value:
"
\"
#{
multiple_words_label
.
title
}
\"
"
}])
expect_issues_list_count
(
1
)
expect_filtered_search_input
(
search
)
expect_filtered_search_input
_empty
end
it
'single quotes containing double quotes'
do
...
...
@@ -305,11 +339,11 @@ describe 'Filter issues', js: true, feature: true do
double_quotes_label_issue
=
create
(
:issue
,
title:
"Issue with double quotes label"
,
project:
project
)
double_quotes_label_issue
.
labels
<<
double_quotes_label
search
=
"label:~'
#{
double_quotes_label
.
title
}
'"
input_filtered_search
(
search
)
input_filtered_search
(
"label:~'
#{
double_quotes_label
.
title
}
'"
)
expect_tokens
([{
name:
'label'
,
value:
"'
#{
double_quotes_label
.
title
}
'"
}])
expect_issues_list_count
(
1
)
expect_filtered_search_input
(
search
)
expect_filtered_search_input
_empty
end
it
'double quotes containing single quotes'
do
...
...
@@ -317,86 +351,115 @@ describe 'Filter issues', js: true, feature: true do
single_quotes_label_issue
=
create
(
:issue
,
title:
"Issue with single quotes label"
,
project:
project
)
single_quotes_label_issue
.
labels
<<
single_quotes_label
search
=
"label:~
\"
#{
single_quotes_label
.
title
}
\"
"
input_filtered_search
(
search
)
input_filtered_search
(
"label:~
\"
#{
single_quotes_label
.
title
}
\"
"
)
expect_tokens
([{
name:
'label'
,
value:
"
\"
#{
single_quotes_label
.
title
}
\"
"
}])
expect_issues_list_count
(
1
)
expect_filtered_search_input
(
search
)
expect_filtered_search_input
_empty
end
end
context
'label with other filters'
do
it
'filters issues by searched label and text'
do
search
=
"label:~
#{
caps_sensitive_label
.
title
}
bug"
input_filtered_search
(
search
)
input_filtered_search
(
"label:~
#{
caps_sensitive_label
.
title
}
#{
search_term
}
"
)
expect_tokens
([{
name:
'label'
,
value:
caps_sensitive_label
.
title
}])
expect_issues_list_count
(
1
)
expect_filtered_search_input
(
search
)
expect_filtered_search_input
(
search
_term
)
end
it
'filters issues by searched label, author and text'
do
search
=
"label:~
#{
caps_sensitive_label
.
title
}
author:@
#{
user
.
username
}
bug"
input_filtered_search
(
search
)
input_filtered_search
(
"label:~
#{
caps_sensitive_label
.
title
}
author:@
#{
user
.
username
}
#{
search_term
}
"
)
expect_tokens
([
{
name:
'label'
,
value:
caps_sensitive_label
.
title
},
{
name:
'author'
,
value:
user
.
username
}
])
expect_issues_list_count
(
1
)
expect_filtered_search_input
(
search
)
expect_filtered_search_input
(
search
_term
)
end
it
'filters issues by searched label, author, assignee and text'
do
search
=
"label:~
#{
caps_sensitive_label
.
title
}
author:@
#{
user
.
username
}
assignee:@
#{
user
.
username
}
bug"
input_filtered_search
(
search
)
input_filtered_search
(
"label:~
#{
caps_sensitive_label
.
title
}
author:@
#{
user
.
username
}
assignee:@
#{
user
.
username
}
#{
search_term
}
"
)
expect_tokens
([
{
name:
'label'
,
value:
caps_sensitive_label
.
title
},
{
name:
'author'
,
value:
user
.
username
},
{
name:
'assignee'
,
value:
user
.
username
}
])
expect_issues_list_count
(
1
)
expect_filtered_search_input
(
search
)
expect_filtered_search_input
(
search
_term
)
end
it
'filters issues by searched label, author, assignee, milestone and text'
do
search
=
"label:~
#{
caps_sensitive_label
.
title
}
author:@
#{
user
.
username
}
assignee:@
#{
user
.
username
}
milestone:%
#{
milestone
.
title
}
bug"
input_filtered_search
(
search
)
input_filtered_search
(
"label:~
#{
caps_sensitive_label
.
title
}
author:@
#{
user
.
username
}
assignee:@
#{
user
.
username
}
milestone:%
#{
milestone
.
title
}
#{
search_term
}
"
)
expect_tokens
([
{
name:
'label'
,
value:
caps_sensitive_label
.
title
},
{
name:
'author'
,
value:
user
.
username
},
{
name:
'assignee'
,
value:
user
.
username
},
{
name:
'milestone'
,
value:
milestone
.
title
}
])
expect_issues_list_count
(
1
)
expect_filtered_search_input
(
search
)
expect_filtered_search_input
(
search
_term
)
end
end
context
'multiple labels with other filters'
do
it
'filters issues by searched label, label2, and text'
do
search
=
"label:~
#{
bug_label
.
title
}
label:~
#{
caps_sensitive_label
.
title
}
bug"
input_filtered_search
(
search
)
input_filtered_search
(
"label:~
#{
bug_label
.
title
}
label:~
#{
caps_sensitive_label
.
title
}
#{
search_term
}
"
)
expect_tokens
([
{
name:
'label'
,
value:
bug_label
.
title
},
{
name:
'label'
,
value:
caps_sensitive_label
.
title
}
])
expect_issues_list_count
(
1
)
expect_filtered_search_input
(
search
)
expect_filtered_search_input
(
search
_term
)
end
it
'filters issues by searched label, label2, author and text'
do
search
=
"label:~
#{
bug_label
.
title
}
label:~
#{
caps_sensitive_label
.
title
}
author:@
#{
user
.
username
}
bug"
input_filtered_search
(
search
)
input_filtered_search
(
"label:~
#{
bug_label
.
title
}
label:~
#{
caps_sensitive_label
.
title
}
author:@
#{
user
.
username
}
#{
search_term
}
"
)
expect_tokens
([
{
name:
'label'
,
value:
bug_label
.
title
},
{
name:
'label'
,
value:
caps_sensitive_label
.
title
},
{
name:
'author'
,
value:
user
.
username
}
])
expect_issues_list_count
(
1
)
expect_filtered_search_input
(
search
)
expect_filtered_search_input
(
search
_term
)
end
it
'filters issues by searched label, label2, author, assignee and text'
do
search
=
"label:~
#{
bug_label
.
title
}
label:~
#{
caps_sensitive_label
.
title
}
author:@
#{
user
.
username
}
assignee:@
#{
user
.
username
}
bug"
input_filtered_search
(
search
)
input_filtered_search
(
"label:~
#{
bug_label
.
title
}
label:~
#{
caps_sensitive_label
.
title
}
author:@
#{
user
.
username
}
assignee:@
#{
user
.
username
}
#{
search_term
}
"
)
expect_tokens
([
{
name:
'label'
,
value:
bug_label
.
title
},
{
name:
'label'
,
value:
caps_sensitive_label
.
title
},
{
name:
'author'
,
value:
user
.
username
},
{
name:
'assignee'
,
value:
user
.
username
}
])
expect_issues_list_count
(
1
)
expect_filtered_search_input
(
search
)
expect_filtered_search_input
(
search
_term
)
end
it
'filters issues by searched label, label2, author, assignee, milestone and text'
do
search
=
"label:~
#{
bug_label
.
title
}
label:~
#{
caps_sensitive_label
.
title
}
author:@
#{
user
.
username
}
assignee:@
#{
user
.
username
}
milestone:%
#{
milestone
.
title
}
bug"
input_filtered_search
(
search
)
input_filtered_search
(
"label:~
#{
bug_label
.
title
}
label:~
#{
caps_sensitive_label
.
title
}
author:@
#{
user
.
username
}
assignee:@
#{
user
.
username
}
milestone:%
#{
milestone
.
title
}
#{
search_term
}
"
)
expect_tokens
([
{
name:
'label'
,
value:
bug_label
.
title
},
{
name:
'label'
,
value:
caps_sensitive_label
.
title
},
{
name:
'author'
,
value:
user
.
username
},
{
name:
'assignee'
,
value:
user
.
username
},
{
name:
'milestone'
,
value:
milestone
.
title
}
])
expect_issues_list_count
(
1
)
expect_filtered_search_input
(
search
)
expect_filtered_search_input
(
search
_term
)
end
end
context
'issue label clicked'
do
before
do
find
(
'.issues-list .issue .issue-info a .label'
,
text:
multiple_words_label
.
title
).
click
sleep
1
end
it
'filters'
do
...
...
@@ -404,7 +467,8 @@ describe 'Filter issues', js: true, feature: true do
end
it
'displays in search bar'
do
expect
(
find
(
'.filtered-search'
).
value
).
to
eq
(
"label:~
\"
#{
multiple_words_label
.
title
}
\"
"
)
expect_tokens
([{
name:
'label'
,
value:
"
\"
#{
multiple_words_label
.
title
}
\"
"
}])
expect_filtered_search_input_empty
end
end
...
...
@@ -420,19 +484,25 @@ describe 'Filter issues', js: true, feature: true do
it
'filters issues by searched milestone'
do
input_filtered_search
(
"milestone:%
#{
milestone
.
title
}
"
)
expect_tokens
([{
name:
'milestone'
,
value:
milestone
.
title
}])
expect_issues_list_count
(
5
)
expect_filtered_search_input_empty
end
it
'filters issues by no milestone'
do
input_filtered_search
(
"milestone:none"
)
expect_tokens
([{
name:
'milestone'
,
value:
'none'
}])
expect_issues_list_count
(
7
,
1
)
expect_filtered_search_input_empty
end
it
'filters issues by upcoming milestones'
do
input_filtered_search
(
"milestone:upcoming"
)
expect_tokens
([{
name:
'milestone'
,
value:
'upcoming'
}])
expect_issues_list_count
(
1
)
expect_filtered_search_input_empty
end
it
'filters issues by invalid milestones'
do
...
...
@@ -447,55 +517,69 @@ describe 'Filter issues', js: true, feature: true do
special_milestone
=
create
(
:milestone
,
title:
'!@\#{$%^&*()}'
,
project:
project
)
create
(
:issue
,
title:
"Issue with special character milestone"
,
project:
project
,
milestone:
special_milestone
)
search
=
"milestone:%
#{
special_milestone
.
title
}
"
input_filtered_search
(
search
)
input_filtered_search
(
"milestone:%
#{
special_milestone
.
title
}
"
)
expect_tokens
([{
name:
'milestone'
,
value:
special_milestone
.
title
}])
expect_issues_list_count
(
1
)
expect_filtered_search_input
(
search
)
expect_filtered_search_input
_empty
end
it
'does not show issues'
do
new_milestone
=
create
(
:milestone
,
title:
"new"
,
project:
project
)
search
=
"milestone:%
#{
new_milestone
.
title
}
"
input_filtered_search
(
search
)
input_filtered_search
(
"milestone:%
#{
new_milestone
.
title
}
"
)
expect_tokens
([{
name:
'milestone'
,
value:
new_milestone
.
title
}])
expect_no_issues_list
()
expect_filtered_search_input
(
search
)
expect_filtered_search_input
_empty
end
end
context
'milestone with other filters'
do
search_term
=
'bug'
it
'filters issues by searched milestone and text'
do
search
=
"milestone:%
#{
milestone
.
title
}
bug"
input_filtered_search
(
search
)
input_filtered_search
(
"milestone:%
#{
milestone
.
title
}
#{
search_term
}
"
)
expect_tokens
([{
name:
'milestone'
,
value:
milestone
.
title
}])
expect_issues_list_count
(
2
)
expect_filtered_search_input
(
search
)
expect_filtered_search_input
(
search
_term
)
end
it
'filters issues by searched milestone, author and text'
do
search
=
"milestone:%
#{
milestone
.
title
}
author:@
#{
user
.
username
}
bug"
input_filtered_search
(
search
)
input_filtered_search
(
"milestone:%
#{
milestone
.
title
}
author:@
#{
user
.
username
}
#{
search_term
}
"
)
expect_tokens
([
{
name:
'milestone'
,
value:
milestone
.
title
},
{
name:
'author'
,
value:
user
.
username
}
])
expect_issues_list_count
(
2
)
expect_filtered_search_input
(
search
)
expect_filtered_search_input
(
search
_term
)
end
it
'filters issues by searched milestone, author, assignee and text'
do
search
=
"milestone:%
#{
milestone
.
title
}
author:@
#{
user
.
username
}
assignee:@
#{
user
.
username
}
bug"
input_filtered_search
(
search
)
input_filtered_search
(
"milestone:%
#{
milestone
.
title
}
author:@
#{
user
.
username
}
assignee:@
#{
user
.
username
}
#{
search_term
}
"
)
expect_tokens
([
{
name:
'milestone'
,
value:
milestone
.
title
},
{
name:
'author'
,
value:
user
.
username
},
{
name:
'assignee'
,
value:
user
.
username
}
])
expect_issues_list_count
(
2
)
expect_filtered_search_input
(
search
)
expect_filtered_search_input
(
search
_term
)
end
it
'filters issues by searched milestone, author, assignee, label and text'
do
search
=
"milestone:%
#{
milestone
.
title
}
author:@
#{
user
.
username
}
assignee:@
#{
user
.
username
}
label:~
#{
bug_label
.
title
}
bug"
input_filtered_search
(
search
)
input_filtered_search
(
"milestone:%
#{
milestone
.
title
}
author:@
#{
user
.
username
}
assignee:@
#{
user
.
username
}
label:~
#{
bug_label
.
title
}
#{
search_term
}
"
)
expect_tokens
([
{
name:
'milestone'
,
value:
milestone
.
title
},
{
name:
'author'
,
value:
user
.
username
},
{
name:
'assignee'
,
value:
user
.
username
},
{
name:
'label'
,
value:
bug_label
.
title
}
])
expect_issues_list_count
(
2
)
expect_filtered_search_input
(
search
)
expect_filtered_search_input
(
search
_term
)
end
end
...
...
@@ -506,44 +590,6 @@ describe 'Filter issues', js: true, feature: true do
end
end
describe
'overwrites selected filter'
do
it
'changes author'
do
input_filtered_search
(
"author:@
#{
user
.
username
}
"
,
submit:
false
)
select_search_at_index
(
3
)
page
.
within
'#js-dropdown-author'
do
click_button
user2
.
username
end
expect
(
filtered_search
.
value
).
to
eq
(
"author:@
#{
user2
.
username
}
"
)
end
it
'changes label'
do
input_filtered_search
(
"author:@
#{
user
.
username
}
label:~
#{
bug_label
.
title
}
"
,
submit:
false
)
select_search_at_index
(
27
)
page
.
within
'#js-dropdown-label'
do
click_button
label
.
name
end
expect
(
filtered_search
.
value
).
to
eq
(
"author:@
#{
user
.
username
}
label:~
#{
label
.
name
}
"
)
end
it
'changes label correctly space is in previous label'
do
input_filtered_search
(
"label:~
\"
#{
multiple_words_label
.
title
}
\"
"
,
submit:
false
)
select_search_at_index
(
0
)
page
.
within
'#js-dropdown-label'
do
click_button
label
.
name
end
expect
(
filtered_search
.
value
).
to
eq
(
"label:~
#{
label
.
name
}
"
)
end
end
describe
'filter issues by text'
do
context
'only text'
do
it
'filters issues by searched text'
do
...
...
@@ -605,80 +651,81 @@ describe 'Filter issues', js: true, feature: true do
context
'searched text with other filters'
do
it
'filters issues by searched text and author'
do
# After searching, all search terms are placed at the end
input_filtered_search
(
"bug author:@
#{
user
.
username
}
"
)
expect_issues_list_count
(
2
)
expect_filtered_search_input
(
"author:@
#{
user
.
username
}
bug"
)
expect_filtered_search_input
(
'bug'
)
end
it
'filters issues by searched text, author and more text'
do
input_filtered_search
(
"bug author:@
#{
user
.
username
}
report"
)
expect_issues_list_count
(
1
)
expect_filtered_search_input
(
"author:@
#{
user
.
username
}
bug report"
)
expect_filtered_search_input
(
'bug report'
)
end
it
'filters issues by searched text, author and assignee'
do
input_filtered_search
(
"bug author:@
#{
user
.
username
}
assignee:@
#{
user
.
username
}
"
)
expect_issues_list_count
(
2
)
expect_filtered_search_input
(
"author:@
#{
user
.
username
}
assignee:@
#{
user
.
username
}
bug"
)
expect_filtered_search_input
(
'bug'
)
end
it
'filters issues by searched text, author, more text and assignee'
do
input_filtered_search
(
"bug author:@
#{
user
.
username
}
report assignee:@
#{
user
.
username
}
"
)
expect_issues_list_count
(
1
)
expect_filtered_search_input
(
"author:@
#{
user
.
username
}
assignee:@
#{
user
.
username
}
bug report"
)
expect_filtered_search_input
(
'bug report'
)
end
it
'filters issues by searched text, author, more text, assignee and even more text'
do
input_filtered_search
(
"bug author:@
#{
user
.
username
}
report assignee:@
#{
user
.
username
}
with"
)
expect_issues_list_count
(
1
)
expect_filtered_search_input
(
"author:@
#{
user
.
username
}
assignee:@
#{
user
.
username
}
bug report with"
)
expect_filtered_search_input
(
'bug report with'
)
end
it
'filters issues by searched text, author, assignee and label'
do
input_filtered_search
(
"bug author:@
#{
user
.
username
}
assignee:@
#{
user
.
username
}
label:~
#{
bug_label
.
title
}
"
)
expect_issues_list_count
(
2
)
expect_filtered_search_input
(
"author:@
#{
user
.
username
}
assignee:@
#{
user
.
username
}
label:~
#{
bug_label
.
title
}
bug"
)
expect_filtered_search_input
(
'bug'
)
end
it
'filters issues by searched text, author, text, assignee, text, label and text'
do
input_filtered_search
(
"bug author:@
#{
user
.
username
}
report assignee:@
#{
user
.
username
}
with label:~
#{
bug_label
.
title
}
everything"
)
expect_issues_list_count
(
1
)
expect_filtered_search_input
(
"author:@
#{
user
.
username
}
assignee:@
#{
user
.
username
}
label:~
#{
bug_label
.
title
}
bug report with everything"
)
expect_filtered_search_input
(
'bug report with everything'
)
end
it
'filters issues by searched text, author, assignee, label and milestone'
do
input_filtered_search
(
"bug author:@
#{
user
.
username
}
assignee:@
#{
user
.
username
}
label:~
#{
bug_label
.
title
}
milestone:%
#{
milestone
.
title
}
"
)
expect_issues_list_count
(
2
)
expect_filtered_search_input
(
"author:@
#{
user
.
username
}
assignee:@
#{
user
.
username
}
label:~
#{
bug_label
.
title
}
milestone:%
#{
milestone
.
title
}
bug"
)
expect_filtered_search_input
(
'bug'
)
end
it
'filters issues by searched text, author, text, assignee, text, label, text, milestone and text'
do
input_filtered_search
(
"bug author:@
#{
user
.
username
}
report assignee:@
#{
user
.
username
}
with label:~
#{
bug_label
.
title
}
everything milestone:%
#{
milestone
.
title
}
you"
)
expect_issues_list_count
(
1
)
expect_filtered_search_input
(
"author:@
#{
user
.
username
}
assignee:@
#{
user
.
username
}
label:~
#{
bug_label
.
title
}
milestone:%
#{
milestone
.
title
}
bug report with everything you"
)
expect_filtered_search_input
(
'bug report with everything you'
)
end
it
'filters issues by searched text, author, assignee, multiple labels and milestone'
do
input_filtered_search
(
"bug author:@
#{
user
.
username
}
assignee:@
#{
user
.
username
}
label:~
#{
bug_label
.
title
}
label:~
#{
caps_sensitive_label
.
title
}
milestone:%
#{
milestone
.
title
}
"
)
expect_issues_list_count
(
1
)
expect_filtered_search_input
(
"author:@
#{
user
.
username
}
assignee:@
#{
user
.
username
}
label:~
#{
bug_label
.
title
}
label:~
#{
caps_sensitive_label
.
title
}
milestone:%
#{
milestone
.
title
}
bug"
)
expect_filtered_search_input
(
'bug'
)
end
it
'filters issues by searched text, author, text, assignee, text, label1, text, label2, text, milestone and text'
do
input_filtered_search
(
"bug author:@
#{
user
.
username
}
report assignee:@
#{
user
.
username
}
with label:~
#{
bug_label
.
title
}
everything label:~
#{
caps_sensitive_label
.
title
}
you milestone:%
#{
milestone
.
title
}
thought"
)
expect_issues_list_count
(
1
)
expect_filtered_search_input
(
"author:@
#{
user
.
username
}
assignee:@
#{
user
.
username
}
label:~
#{
bug_label
.
title
}
label:~
#{
caps_sensitive_label
.
title
}
milestone:%
#{
milestone
.
title
}
bug report with everything you thought"
)
expect_filtered_search_input
(
'bug report with everything you thought'
)
end
end
...
...
@@ -717,8 +764,8 @@ describe 'Filter issues', js: true, feature: true do
before
do
input_filtered_search
(
'bug'
)
#
Wait for search results to loa
d
sleep
2
#
This ensures that the search is performe
d
expect_issues_list_count
(
4
,
1
)
end
it
'open state'
do
...
...
spec/features/issues/filtered_search/search_bar_spec.rb
View file @
f44fb5cf
require
'rails_helper'
describe
'Search bar'
,
js:
true
,
feature:
true
do
include
FilteredSearchHelpers
include
WaitForAjax
let!
(
:project
)
{
create
(
:empty_project
)
}
...
...
@@ -32,7 +33,8 @@ describe 'Search bar', js: true, feature: true do
it
'selects item'
do
filtered_search
.
native
.
send_keys
(
:down
,
:down
,
:enter
)
expect
(
filtered_search
.
value
).
to
eq
(
'author:'
)
expect_tokens
([{
name:
'author'
}])
expect_filtered_search_input_empty
end
end
...
...
spec/features/issues/filtered_search/visual_tokens_spec.rb
0 → 100644
View file @
f44fb5cf
require
'rails_helper'
describe
'Visual tokens'
,
js:
true
,
feature:
true
do
include
FilteredSearchHelpers
let!
(
:project
)
{
create
(
:empty_project
)
}
let!
(
:user
)
{
create
(
:user
,
name:
'administrator'
,
username:
'root'
)
}
let!
(
:user_rock
)
{
create
(
:user
,
name:
'The Rock'
,
username:
'rock'
)
}
let!
(
:milestone_nine
)
{
create
(
:milestone
,
title:
'9.0'
,
project:
project
)
}
let!
(
:milestone_ten
)
{
create
(
:milestone
,
title:
'10.0'
,
project:
project
)
}
let!
(
:label
)
{
create
(
:label
,
project:
project
,
title:
'abc'
)
}
let!
(
:cc_label
)
{
create
(
:label
,
project:
project
,
title:
'Community Contribution'
)
}
let
(
:filtered_search
)
{
find
(
'.filtered-search'
)
}
let
(
:filter_author_dropdown
)
{
find
(
"#js-dropdown-author .filter-dropdown"
)
}
let
(
:filter_assignee_dropdown
)
{
find
(
"#js-dropdown-assignee .filter-dropdown"
)
}
let
(
:filter_milestone_dropdown
)
{
find
(
"#js-dropdown-milestone .filter-dropdown"
)
}
let
(
:filter_label_dropdown
)
{
find
(
"#js-dropdown-label .filter-dropdown"
)
}
def
is_input_focused
page
.
evaluate_script
(
"document.activeElement.classList.contains('filtered-search')"
)
end
before
do
project
.
add_user
(
user
,
:master
)
project
.
add_user
(
user_rock
,
:master
)
login_as
(
user
)
create
(
:issue
,
project:
project
)
visit
namespace_project_issues_path
(
project
.
namespace
,
project
)
end
describe
'editing author token'
do
before
do
input_filtered_search
(
'author:@root assignee:none'
,
submit:
false
)
first
(
'.tokens-container .filtered-search-token'
).
double_click
end
it
'opens author dropdown'
do
expect
(
page
).
to
have_css
(
'#js-dropdown-author'
,
visible:
true
)
end
it
'makes value editable'
do
expect_filtered_search_input
(
'@root'
)
end
it
'filters value'
do
filtered_search
.
send_keys
(
:backspace
)
expect
(
page
).
to
have_css
(
'#js-dropdown-author .filter-dropdown .filter-dropdown-item'
,
count:
1
)
end
it
'ends editing mode when document is clicked'
do
find
(
'#content-body'
).
click
expect_filtered_search_input_empty
expect
(
page
).
to
have_css
(
'#js-dropdown-author'
,
visible:
false
)
end
it
'ends editing mode when scroll container is clicked'
do
find
(
'.scroll-container'
).
click
expect_filtered_search_input_empty
expect
(
page
).
to
have_css
(
'#js-dropdown-author'
,
visible:
false
)
end
describe
'selecting different author from dropdown'
do
before
do
filter_author_dropdown
.
find
(
'.filter-dropdown-item .dropdown-light-content'
,
text:
"@
#{
user_rock
.
username
}
"
).
click
end
it
'changes value in visual token'
do
expect
(
first
(
'.tokens-container .filtered-search-token .value'
).
text
).
to
eq
(
"@
#{
user_rock
.
username
}
"
)
end
it
'moves input to the right'
do
expect
(
is_input_focused
).
to
eq
(
true
)
end
end
end
describe
'editing assignee token'
do
before
do
input_filtered_search
(
'assignee:@root author:none'
,
submit:
false
)
first
(
'.tokens-container .filtered-search-token'
).
double_click
end
it
'opens assignee dropdown'
do
expect
(
page
).
to
have_css
(
'#js-dropdown-assignee'
,
visible:
true
)
end
it
'makes value editable'
do
expect_filtered_search_input
(
'@root'
)
end
it
'filters value'
do
filtered_search
.
send_keys
(
:backspace
)
expect
(
page
).
to
have_css
(
'#js-dropdown-assignee .filter-dropdown .filter-dropdown-item'
,
count:
1
)
end
it
'ends editing mode when document is clicked'
do
find
(
'#content-body'
).
click
expect_filtered_search_input_empty
expect
(
page
).
to
have_css
(
'#js-dropdown-assignee'
,
visible:
false
)
end
it
'ends editing mode when scroll container is clicked'
do
find
(
'.scroll-container'
).
click
expect_filtered_search_input_empty
expect
(
page
).
to
have_css
(
'#js-dropdown-assignee'
,
visible:
false
)
end
describe
'selecting static option from dropdown'
do
before
do
find
(
"#js-dropdown-assignee"
).
find
(
'.filter-dropdown-item'
,
text:
'No Assignee'
).
click
end
it
'changes value in visual token'
do
expect
(
first
(
'.tokens-container .filtered-search-token .value'
).
text
).
to
eq
(
'none'
)
end
it
'moves input to the right'
do
expect
(
is_input_focused
).
to
eq
(
true
)
end
end
end
describe
'editing milestone token'
do
before
do
input_filtered_search
(
'milestone:%10.0 author:none'
,
submit:
false
)
first
(
'.tokens-container .filtered-search-token'
).
double_click
first
(
'#js-dropdown-milestone .filter-dropdown .filter-dropdown-item'
)
end
it
'opens milestone dropdown'
do
expect
(
filter_milestone_dropdown
.
find
(
'.filter-dropdown-item'
,
text:
milestone_ten
.
title
)).
to
be_visible
expect
(
filter_milestone_dropdown
.
find
(
'.filter-dropdown-item'
,
text:
milestone_nine
.
title
)).
to
be_visible
expect
(
page
).
to
have_css
(
'#js-dropdown-milestone'
,
visible:
true
)
end
it
'selects static option from dropdown'
do
find
(
"#js-dropdown-milestone"
).
find
(
'.filter-dropdown-item'
,
text:
'Upcoming'
).
click
expect
(
first
(
'.tokens-container .filtered-search-token .value'
).
text
).
to
eq
(
'upcoming'
)
expect
(
is_input_focused
).
to
eq
(
true
)
end
it
'makes value editable'
do
expect_filtered_search_input
(
'%10.0'
)
end
it
'filters value'
do
filtered_search
.
send_keys
(
:backspace
)
expect
(
page
).
to
have_css
(
'#js-dropdown-milestone .filter-dropdown .filter-dropdown-item'
,
count:
1
)
end
it
'ends editing mode when document is clicked'
do
find
(
'#content-body'
).
click
expect_filtered_search_input_empty
expect
(
page
).
to
have_css
(
'#js-dropdown-milestone'
,
visible:
false
)
end
it
'ends editing mode when scroll container is clicked'
do
find
(
'.scroll-container'
).
click
expect_filtered_search_input_empty
expect
(
page
).
to
have_css
(
'#js-dropdown-milestone'
,
visible:
false
)
end
end
describe
'editing label token'
do
before
do
input_filtered_search
(
"label:~
#{
label
.
title
}
author:none"
,
submit:
false
)
first
(
'.tokens-container .filtered-search-token'
).
double_click
first
(
'#js-dropdown-label .filter-dropdown .filter-dropdown-item'
)
end
it
'opens label dropdown'
do
expect
(
filter_label_dropdown
.
find
(
'.filter-dropdown-item'
,
text:
label
.
title
)).
to
be_visible
expect
(
filter_label_dropdown
.
find
(
'.filter-dropdown-item'
,
text:
cc_label
.
title
)).
to
be_visible
expect
(
page
).
to
have_css
(
'#js-dropdown-label'
,
visible:
true
)
end
it
'selects option from dropdown'
do
expect
(
filter_label_dropdown
.
find
(
'.filter-dropdown-item'
,
text:
label
.
title
)).
to
be_visible
expect
(
filter_label_dropdown
.
find
(
'.filter-dropdown-item'
,
text:
cc_label
.
title
)).
to
be_visible
find
(
"#js-dropdown-label"
).
find
(
'.filter-dropdown-item'
,
text:
cc_label
.
title
).
click
expect
(
first
(
'.tokens-container .filtered-search-token .value'
).
text
).
to
eq
(
"~
\"
#{
cc_label
.
title
}
\"
"
)
expect
(
is_input_focused
).
to
eq
(
true
)
end
it
'makes value editable'
do
expect_filtered_search_input
(
"~
#{
label
.
title
}
"
)
end
it
'filters value'
do
expect
(
filter_label_dropdown
.
find
(
'.filter-dropdown-item'
,
text:
label
.
title
)).
to
be_visible
expect
(
filter_label_dropdown
.
find
(
'.filter-dropdown-item'
,
text:
cc_label
.
title
)).
to
be_visible
filtered_search
.
send_keys
(
:backspace
)
filter_label_dropdown
.
find
(
'.filter-dropdown-item'
)
expect
(
page
.
all
(
'#js-dropdown-label .filter-dropdown .filter-dropdown-item'
).
size
).
to
eq
(
1
)
end
it
'ends editing mode when document is clicked'
do
find
(
'#content-body'
).
click
expect_filtered_search_input_empty
expect
(
page
).
to
have_css
(
'#js-dropdown-label'
,
visible:
false
)
end
it
'ends editing mode when scroll container is clicked'
do
find
(
'.scroll-container'
).
click
expect_filtered_search_input_empty
expect
(
page
).
to
have_css
(
'#js-dropdown-label'
,
visible:
false
)
end
end
describe
'add new token after editing existing token'
do
before
do
input_filtered_search
(
'author:@root assignee:none'
,
submit:
false
)
first
(
'.tokens-container .filtered-search-token'
).
double_click
filtered_search
.
send_keys
(
' '
)
end
describe
'opens dropdowns'
do
it
'opens hint dropdown'
do
expect
(
page
).
to
have_css
(
'#js-dropdown-hint'
,
visible:
true
)
end
it
'opens author dropdown'
do
filtered_search
.
send_keys
(
'author:'
)
expect
(
page
).
to
have_css
(
'#js-dropdown-author'
,
visible:
true
)
end
it
'opens assignee dropdown'
do
filtered_search
.
send_keys
(
'assignee:'
)
expect
(
page
).
to
have_css
(
'#js-dropdown-assignee'
,
visible:
true
)
end
it
'opens milestone dropdown'
do
filtered_search
.
send_keys
(
'milestone:'
)
expect
(
page
).
to
have_css
(
'#js-dropdown-milestone'
,
visible:
true
)
end
it
'opens label dropdown'
do
filtered_search
.
send_keys
(
'label:'
)
expect
(
page
).
to
have_css
(
'#js-dropdown-label'
,
visible:
true
)
end
end
describe
'creates visual tokens'
do
it
'creates author token'
do
filtered_search
.
send_keys
(
'author:@thomas '
)
token
=
page
.
all
(
'.tokens-container .filtered-search-token'
)[
1
]
expect
(
token
.
find
(
'.name'
).
text
).
to
eq
(
'Author'
)
expect
(
token
.
find
(
'.value'
).
text
).
to
eq
(
'@thomas'
)
end
it
'creates assignee token'
do
filtered_search
.
send_keys
(
'assignee:@thomas '
)
token
=
page
.
all
(
'.tokens-container .filtered-search-token'
)[
1
]
expect
(
token
.
find
(
'.name'
).
text
).
to
eq
(
'Assignee'
)
expect
(
token
.
find
(
'.value'
).
text
).
to
eq
(
'@thomas'
)
end
it
'creates milestone token'
do
filtered_search
.
send_keys
(
'milestone:none '
)
token
=
page
.
all
(
'.tokens-container .filtered-search-token'
)[
1
]
expect
(
token
.
find
(
'.name'
).
text
).
to
eq
(
'Milestone'
)
expect
(
token
.
find
(
'.value'
).
text
).
to
eq
(
'none'
)
end
it
'creates label token'
do
filtered_search
.
send_keys
(
'label:~Backend '
)
token
=
page
.
all
(
'.tokens-container .filtered-search-token'
)[
1
]
expect
(
token
.
find
(
'.name'
).
text
).
to
eq
(
'Label'
)
expect
(
token
.
find
(
'.value'
).
text
).
to
eq
(
'~Backend'
)
end
end
it
'does not tokenize incomplete token'
do
filtered_search
.
send_keys
(
'author:'
)
find
(
'#content-body'
).
click
token
=
page
.
all
(
'.tokens-container .js-visual-token'
)[
1
]
expect_filtered_search_input_empty
expect
(
token
.
find
(
'.name'
).
text
).
to
eq
(
'Author'
)
end
end
end
spec/features/merge_requests/filter_by_labels_spec.rb
View file @
f44fb5cf
...
...
@@ -70,7 +70,7 @@ feature 'Issue filtering by Labels', feature: true, js: true do
context
'filter by label enhancement and bug in issues list'
do
before
do
input_filtered_search
(
'label:~bug label:~enhancement'
)
input_filtered_search
(
'label:~bug label:~enhancement
'
)
end
it
'applies the filters'
do
...
...
spec/features/merge_requests/filter_by_milestone_spec.rb
View file @
f44fb5cf
...
...
@@ -25,6 +25,9 @@ feature 'Merge Request filtering by Milestone', feature: true do
visit_merge_requests
(
project
)
input_filtered_search
(
'milestone:none'
)
expect_tokens
([{
name:
'milestone'
,
value:
'none'
}])
expect_filtered_search_input_empty
expect
(
page
).
to
have_issuable_counts
(
open:
1
,
closed:
0
,
all:
1
)
expect
(
page
).
to
have_css
(
'.merge-request'
,
count:
1
)
end
...
...
spec/features/merge_requests/filter_merge_requests_spec.rb
View file @
f44fb5cf
...
...
@@ -24,6 +24,11 @@ describe 'Filter merge requests', feature: true do
describe
'for assignee from mr#index'
do
let
(
:search_query
)
{
"assignee:@
#{
user
.
username
}
"
}
def
expect_assignee_visual_tokens
expect_tokens
([{
name:
'assignee'
,
value:
"@
#{
user
.
username
}
"
}])
expect_filtered_search_input_empty
end
before
do
input_filtered_search
(
search_query
)
...
...
@@ -32,25 +37,30 @@ describe 'Filter merge requests', feature: true do
context
'assignee'
,
js:
true
do
it
'updates to current user'
do
expect_
filtered_search_input
(
search_query
)
expect_
assignee_visual_tokens
(
)
end
it
'does not change when closed link is clicked'
do
find
(
'.issues-state-filters a'
,
text:
"Closed"
).
click
expect_
filtered_search_input
(
search_query
)
expect_
assignee_visual_tokens
(
)
end
it
'does not change when all link is clicked'
do
find
(
'.issues-state-filters a'
,
text:
"All"
).
click
expect_
filtered_search_input
(
search_query
)
expect_
assignee_visual_tokens
(
)
end
end
end
describe
'for milestone from mr#index'
do
let
(
:search_query
)
{
"milestone:%
#{
milestone
.
title
}
"
}
let
(
:search_query
)
{
"milestone:%
\"
#{
milestone
.
title
}
\"
"
}
def
expect_milestone_visual_tokens
expect_tokens
([{
name:
'milestone'
,
value:
"%
\"
#{
milestone
.
title
}
\"
"
}])
expect_filtered_search_input_empty
end
before
do
input_filtered_search
(
search_query
)
...
...
@@ -60,19 +70,19 @@ describe 'Filter merge requests', feature: true do
context
'milestone'
,
js:
true
do
it
'updates to current milestone'
do
expect_
filtered_search_input
(
search_query
)
expect_
milestone_visual_tokens
(
)
end
it
'does not change when closed link is clicked'
do
find
(
'.issues-state-filters a'
,
text:
"Closed"
).
click
expect_
filtered_search_input
(
search_query
)
expect_
milestone_visual_tokens
(
)
end
it
'does not change when all link is clicked'
do
find
(
'.issues-state-filters a'
,
text:
"All"
).
click
expect_
filtered_search_input
(
search_query
)
expect_
milestone_visual_tokens
(
)
end
end
end
...
...
@@ -82,35 +92,44 @@ describe 'Filter merge requests', feature: true do
input_filtered_search
(
'label:none'
)
expect_mr_list_count
(
1
)
expect_filtered_search_input
(
'label:none'
)
expect_tokens
([{
name:
'label'
,
value:
'none'
}])
expect_filtered_search_input_empty
end
it
'filters by a label'
do
input_filtered_search
(
"label:~
#{
label
.
title
}
"
)
expect_mr_list_count
(
0
)
expect_filtered_search_input
(
"label:~
#{
label
.
title
}
"
)
expect_tokens
([{
name:
'label'
,
value:
"~
#{
label
.
title
}
"
}])
expect_filtered_search_input_empty
end
it
"filters by `won't fix` and another label"
do
input_filtered_search
(
"label:~
\"
#{
wontfix
.
title
}
\"
label:~
#{
label
.
title
}
"
)
expect_mr_list_count
(
0
)
expect_filtered_search_input
(
"label:~
\"
#{
wontfix
.
title
}
\"
label:~
#{
label
.
title
}
"
)
expect_tokens
([
{
name:
'label'
,
value:
"~
\"
#{
wontfix
.
title
}
\"
"
},
{
name:
'label'
,
value:
"~
#{
label
.
title
}
"
}
])
expect_filtered_search_input_empty
end
it
"filters by `won't fix` label followed by another label after page load"
do
input_filtered_search
(
"label:~
\"
#{
wontfix
.
title
}
\"
"
)
expect_mr_list_count
(
0
)
expect_filtered_search_input
(
"label:~
\"
#{
wontfix
.
title
}
\"
"
)
input_filtered_search_keys
(
" label:~
#{
label
.
title
}
"
)
expect_tokens
([{
name:
'label'
,
value:
"~
\"
#{
wontfix
.
title
}
\"
"
}])
expect_filtered_search_input_empty
expect_filtered_search_input
(
"label:~
\"
#{
wontfix
.
title
}
\"
label:~
#{
label
.
title
}
"
)
input_filtered_search_keys
(
"
label:~
#{
label
.
title
}
"
)
expect_mr_list_count
(
0
)
expect_filtered_search_input
(
"label:~
\"
#{
wontfix
.
title
}
\"
label:~
#{
label
.
title
}
"
)
expect_tokens
([
{
name:
'label'
,
value:
"~
\"
#{
wontfix
.
title
}
\"
"
},
{
name:
'label'
,
value:
"~
#{
label
.
title
}
"
}
])
expect_filtered_search_input_empty
end
end
...
...
@@ -121,9 +140,10 @@ describe 'Filter merge requests', feature: true do
input_filtered_search
(
"assignee:@
#{
user
.
username
}
"
)
expect_mr_list_count
(
1
)
expect_filtered_search_input
(
"assignee:@
#{
user
.
username
}
"
)
expect_tokens
([{
name:
'assignee'
,
value:
"@
#{
user
.
username
}
"
}])
expect_filtered_search_input_empty
input_filtered_search_keys
(
"
label:~
#{
label
.
title
}
"
)
input_filtered_search_keys
(
"
label:~
#{
label
.
title
}
"
)
expect_mr_list_count
(
1
)
...
...
@@ -131,20 +151,28 @@ describe 'Filter merge requests', feature: true do
end
context
'assignee and label'
,
js:
true
do
def
expect_assignee_label_visual_tokens
expect_tokens
([
{
name:
'assignee'
,
value:
"@
#{
user
.
username
}
"
},
{
name:
'label'
,
value:
"~
#{
label
.
title
}
"
}
])
expect_filtered_search_input_empty
end
it
'updates to current assignee and label'
do
expect_
filtered_search_input
(
search_query
)
expect_
assignee_label_visual_tokens
(
)
end
it
'does not change when closed link is clicked'
do
find
(
'.issues-state-filters a'
,
text:
"Closed"
).
click
expect_
filtered_search_input
(
search_query
)
expect_
assignee_label_visual_tokens
(
)
end
it
'does not change when all link is clicked'
do
find
(
'.issues-state-filters a'
,
text:
"All"
).
click
expect_
filtered_search_input
(
search_query
)
expect_
assignee_label_visual_tokens
(
)
end
end
end
...
...
@@ -195,6 +223,8 @@ describe 'Filter merge requests', feature: true do
input_filtered_search_keys
(
' label:~bug'
)
expect_mr_list_count
(
1
)
expect_tokens
([{
name:
'label'
,
value:
'~bug'
}])
expect_filtered_search_input
(
'Bug'
)
end
it
'filters by text and milestone'
do
...
...
@@ -206,6 +236,8 @@ describe 'Filter merge requests', feature: true do
input_filtered_search_keys
(
' milestone:%8'
)
expect_mr_list_count
(
1
)
expect_tokens
([{
name:
'milestone'
,
value:
'%8'
}])
expect_filtered_search_input
(
'Bug'
)
end
it
'filters by text and assignee'
do
...
...
@@ -217,6 +249,8 @@ describe 'Filter merge requests', feature: true do
input_filtered_search_keys
(
" assignee:@
#{
user
.
username
}
"
)
expect_mr_list_count
(
1
)
expect_tokens
([{
name:
'assignee'
,
value:
"@
#{
user
.
username
}
"
}])
expect_filtered_search_input
(
'Bug'
)
end
it
'filters by text and author'
do
...
...
@@ -228,6 +262,8 @@ describe 'Filter merge requests', feature: true do
input_filtered_search_keys
(
" author:@
#{
user
.
username
}
"
)
expect_mr_list_count
(
1
)
expect_tokens
([{
name:
'author'
,
value:
"@
#{
user
.
username
}
"
}])
expect_filtered_search_input
(
'Bug'
)
end
end
end
...
...
@@ -266,7 +302,8 @@ describe 'Filter merge requests', feature: true do
it
'filter by current user'
do
visit
namespace_project_merge_requests_path
(
project
.
namespace
,
project
,
assignee_id:
user
.
id
)
expect_filtered_search_input
(
"assignee:@
#{
user
.
username
}
"
)
expect_tokens
([{
name:
'assignee'
,
value:
"@
#{
user
.
username
}
"
}])
expect_filtered_search_input_empty
end
it
'filter by new user'
do
...
...
@@ -275,7 +312,8 @@ describe 'Filter merge requests', feature: true do
visit
namespace_project_merge_requests_path
(
project
.
namespace
,
project
,
assignee_id:
new_user
.
id
)
expect_filtered_search_input
(
"assignee:@
#{
new_user
.
username
}
"
)
expect_tokens
([{
name:
'assignee'
,
value:
"@
#{
new_user
.
username
}
"
}])
expect_filtered_search_input_empty
end
end
...
...
@@ -283,7 +321,8 @@ describe 'Filter merge requests', feature: true do
it
'filter by current user'
do
visit
namespace_project_merge_requests_path
(
project
.
namespace
,
project
,
author_id:
user
.
id
)
expect_filtered_search_input
(
"author:@
#{
user
.
username
}
"
)
expect_tokens
([{
name:
'author'
,
value:
"@
#{
user
.
username
}
"
}])
expect_filtered_search_input_empty
end
it
'filter by new user'
do
...
...
@@ -292,7 +331,8 @@ describe 'Filter merge requests', feature: true do
visit
namespace_project_merge_requests_path
(
project
.
namespace
,
project
,
author_id:
new_user
.
id
)
expect_filtered_search_input
(
"author:@
#{
new_user
.
username
}
"
)
expect_tokens
([{
name:
'author'
,
value:
"@
#{
new_user
.
username
}
"
}])
expect_filtered_search_input_empty
end
end
end
spec/features/merge_requests/reset_filters_spec.rb
View file @
f44fb5cf
require
'rails_helper'
feature
'
Issues filter reset
button'
,
feature:
true
,
js:
true
do
feature
'
Merge requests filter clear
button'
,
feature:
true
,
js:
true
do
include
FilteredSearchHelpers
include
MergeRequestHelpers
include
WaitForAjax
...
...
@@ -24,67 +24,93 @@ feature 'Issues filter reset button', feature: true, js: true do
context
'when a milestone filter has been applied'
do
it
'resets the milestone filter'
do
visit_merge_requests
(
project
,
milestone_title:
milestone
.
title
)
expect
(
page
).
to
have_css
(
merge_request_css
,
count:
1
)
expect
(
get_filtered_search_placeholder
).
to
eq
(
''
)
reset_filters
expect
(
page
).
to
have_css
(
merge_request_css
,
count:
2
)
expect
(
get_filtered_search_placeholder
).
to
eq
(
default_placeholder
)
end
end
context
'when a label filter has been applied'
do
it
'resets the label filter'
do
visit_merge_requests
(
project
,
label_name:
bug
.
name
)
expect
(
page
).
to
have_css
(
merge_request_css
,
count:
1
)
expect
(
get_filtered_search_placeholder
).
to
eq
(
''
)
reset_filters
expect
(
page
).
to
have_css
(
merge_request_css
,
count:
2
)
expect
(
get_filtered_search_placeholder
).
to
eq
(
default_placeholder
)
end
end
context
'when a text search has been conducted'
do
it
'resets the text search filter'
do
visit_merge_requests
(
project
,
search:
'Bug'
)
expect
(
page
).
to
have_css
(
merge_request_css
,
count:
1
)
expect
(
get_filtered_search_placeholder
).
to
eq
(
''
)
reset_filters
expect
(
page
).
to
have_css
(
merge_request_css
,
count:
2
)
expect
(
get_filtered_search_placeholder
).
to
eq
(
default_placeholder
)
end
end
context
'when author filter has been applied'
do
it
'resets the author filter'
do
visit_merge_requests
(
project
,
author_username:
user
.
username
)
expect
(
page
).
to
have_css
(
merge_request_css
,
count:
1
)
expect
(
get_filtered_search_placeholder
).
to
eq
(
''
)
reset_filters
expect
(
page
).
to
have_css
(
merge_request_css
,
count:
2
)
expect
(
get_filtered_search_placeholder
).
to
eq
(
default_placeholder
)
end
end
context
'when assignee filter has been applied'
do
it
'resets the assignee filter'
do
visit_merge_requests
(
project
,
assignee_username:
user
.
username
)
expect
(
page
).
to
have_css
(
merge_request_css
,
count:
1
)
expect
(
get_filtered_search_placeholder
).
to
eq
(
''
)
reset_filters
expect
(
page
).
to
have_css
(
merge_request_css
,
count:
2
)
expect
(
get_filtered_search_placeholder
).
to
eq
(
default_placeholder
)
end
end
context
'when all filters have been applied'
do
it
'
reset
s all filters'
do
it
'
clear
s all filters'
do
visit_merge_requests
(
project
,
assignee_username:
user
.
username
,
author_username:
user
.
username
,
milestone_title:
milestone
.
title
,
label_name:
bug
.
name
,
search:
'Bug'
)
expect
(
page
).
to
have_css
(
merge_request_css
,
count:
0
)
expect
(
get_filtered_search_placeholder
).
to
eq
(
''
)
reset_filters
expect
(
page
).
to
have_css
(
merge_request_css
,
count:
2
)
expect
(
get_filtered_search_placeholder
).
to
eq
(
default_placeholder
)
end
end
context
'when no filters have been applied'
do
it
'the
reset link
should not be visible'
do
it
'the
clear button
should not be visible'
do
visit_merge_requests
(
project
)
expect
(
page
).
to
have_css
(
merge_request_css
,
count:
2
)
expect
(
get_filtered_search_placeholder
).
to
eq
(
default_placeholder
)
expect
(
page
).
not_to
have_css
(
clear_search_css
)
end
end
...
...
spec/features/search_spec.rb
View file @
f44fb5cf
require
'spec_helper'
describe
"Search"
,
feature:
true
do
include
FilteredSearchHelpers
include
WaitForAjax
let
(
:user
)
{
create
(
:user
)
}
...
...
@@ -170,7 +171,8 @@ describe "Search", feature: true do
sleep
2
expect
(
page
).
to
have_selector
(
'.filtered-search'
)
expect
(
find
(
'.filtered-search'
).
value
).
to
eq
(
"assignee:@
#{
user
.
username
}
"
)
expect_tokens
([{
name:
'assignee'
,
value:
"@
#{
user
.
username
}
"
}])
expect_filtered_search_input_empty
end
it
'takes user to her issues page when issues authored is clicked'
do
...
...
@@ -178,7 +180,8 @@ describe "Search", feature: true do
sleep
2
expect
(
page
).
to
have_selector
(
'.filtered-search'
)
expect
(
find
(
'.filtered-search'
).
value
).
to
eq
(
"author:@
#{
user
.
username
}
"
)
expect_tokens
([{
name:
'author'
,
value:
"@
#{
user
.
username
}
"
}])
expect_filtered_search_input_empty
end
it
'takes user to her MR page when MR assigned is clicked'
do
...
...
@@ -186,7 +189,8 @@ describe "Search", feature: true do
sleep
2
expect
(
page
).
to
have_selector
(
'.merge-requests-holder'
)
expect
(
find
(
'.filtered-search'
).
value
).
to
eq
(
"assignee:@
#{
user
.
username
}
"
)
expect_tokens
([{
name:
'assignee'
,
value:
"@
#{
user
.
username
}
"
}])
expect_filtered_search_input_empty
end
it
'takes user to her MR page when MR authored is clicked'
do
...
...
@@ -194,7 +198,8 @@ describe "Search", feature: true do
sleep
2
expect
(
page
).
to
have_selector
(
'.merge-requests-holder'
)
expect
(
find
(
'.filtered-search'
).
value
).
to
eq
(
"author:@
#{
user
.
username
}
"
)
expect_tokens
([{
name:
'author'
,
value:
"@
#{
user
.
username
}
"
}])
expect_filtered_search_input_empty
end
end
...
...
spec/javascripts/filtered_search/dropdown_user_spec.js
View file @
f44fb5cf
...
...
@@ -18,9 +18,7 @@ require('~/filtered_search/dropdown_user');
it
(
'
should not return the double quote found in value
'
,
()
=>
{
spyOn
(
gl
.
FilteredSearchTokenizer
,
'
processTokens
'
).
and
.
returnValue
({
lastToken
:
{
value
:
'
"johnny appleseed
'
,
},
lastToken
:
'
"johnny appleseed
'
,
});
expect
(
dropdownUser
.
getSearchInput
()).
toBe
(
'
johnny appleseed
'
);
...
...
@@ -28,9 +26,7 @@ require('~/filtered_search/dropdown_user');
it
(
'
should not return the single quote found in value
'
,
()
=>
{
spyOn
(
gl
.
FilteredSearchTokenizer
,
'
processTokens
'
).
and
.
returnValue
({
lastToken
:
{
value
:
'
\'
larry boy
'
,
},
lastToken
:
'
\'
larry boy
'
,
});
expect
(
dropdownUser
.
getSearchInput
()).
toBe
(
'
larry boy
'
);
...
...
spec/javascripts/filtered_search/dropdown_utils_spec.js
View file @
f44fb5cf
...
...
@@ -45,7 +45,7 @@ require('~/filtered_search/filtered_search_dropdown_manager');
});
it
(
'
should filter without symbol
'
,
()
=>
{
input
.
value
=
'
:
roo
'
;
input
.
value
=
'
roo
'
;
const
updatedItem
=
gl
.
DropdownUtils
.
filterWithSymbol
(
'
@
'
,
input
,
item
);
expect
(
updatedItem
.
droplab_hidden
).
toBe
(
false
);
...
...
@@ -58,69 +58,62 @@ require('~/filtered_search/filtered_search_dropdown_manager');
expect
(
updatedItem
.
droplab_hidden
).
toBe
(
false
);
});
it
(
'
should filter with colon
'
,
()
=>
{
input
.
value
=
'
roo
'
;
const
updatedItem
=
gl
.
DropdownUtils
.
filterWithSymbol
(
'
@
'
,
input
,
item
);
expect
(
updatedItem
.
droplab_hidden
).
toBe
(
false
);
});
describe
(
'
filters multiple word title
'
,
()
=>
{
const
multipleWordItem
=
{
title
:
'
Community Contributions
'
,
};
it
(
'
should filter with double quote
'
,
()
=>
{
input
.
value
=
'
label:
"
'
;
input
.
value
=
'
"
'
;
const
updatedItem
=
gl
.
DropdownUtils
.
filterWithSymbol
(
'
~
'
,
input
,
multipleWordItem
);
expect
(
updatedItem
.
droplab_hidden
).
toBe
(
false
);
});
it
(
'
should filter with double quote and symbol
'
,
()
=>
{
input
.
value
=
'
label:
~"
'
;
input
.
value
=
'
~"
'
;
const
updatedItem
=
gl
.
DropdownUtils
.
filterWithSymbol
(
'
~
'
,
input
,
multipleWordItem
);
expect
(
updatedItem
.
droplab_hidden
).
toBe
(
false
);
});
it
(
'
should filter with double quote and multiple words
'
,
()
=>
{
input
.
value
=
'
label:
"community con
'
;
input
.
value
=
'
"community con
'
;
const
updatedItem
=
gl
.
DropdownUtils
.
filterWithSymbol
(
'
~
'
,
input
,
multipleWordItem
);
expect
(
updatedItem
.
droplab_hidden
).
toBe
(
false
);
});
it
(
'
should filter with double quote, symbol and multiple words
'
,
()
=>
{
input
.
value
=
'
label:
~"community con
'
;
input
.
value
=
'
~"community con
'
;
const
updatedItem
=
gl
.
DropdownUtils
.
filterWithSymbol
(
'
~
'
,
input
,
multipleWordItem
);
expect
(
updatedItem
.
droplab_hidden
).
toBe
(
false
);
});
it
(
'
should filter with single quote
'
,
()
=>
{
input
.
value
=
'
label:
\'
'
;
input
.
value
=
'
\'
'
;
const
updatedItem
=
gl
.
DropdownUtils
.
filterWithSymbol
(
'
~
'
,
input
,
multipleWordItem
);
expect
(
updatedItem
.
droplab_hidden
).
toBe
(
false
);
});
it
(
'
should filter with single quote and symbol
'
,
()
=>
{
input
.
value
=
'
label:
~
\'
'
;
input
.
value
=
'
~
\'
'
;
const
updatedItem
=
gl
.
DropdownUtils
.
filterWithSymbol
(
'
~
'
,
input
,
multipleWordItem
);
expect
(
updatedItem
.
droplab_hidden
).
toBe
(
false
);
});
it
(
'
should filter with single quote and multiple words
'
,
()
=>
{
input
.
value
=
'
label:
\'
community con
'
;
input
.
value
=
'
\'
community con
'
;
const
updatedItem
=
gl
.
DropdownUtils
.
filterWithSymbol
(
'
~
'
,
input
,
multipleWordItem
);
expect
(
updatedItem
.
droplab_hidden
).
toBe
(
false
);
});
it
(
'
should filter with single quote, symbol and multiple words
'
,
()
=>
{
input
.
value
=
'
label:
~
\'
community con
'
;
input
.
value
=
'
~
\'
community con
'
;
const
updatedItem
=
gl
.
DropdownUtils
.
filterWithSymbol
(
'
~
'
,
input
,
multipleWordItem
);
expect
(
updatedItem
.
droplab_hidden
).
toBe
(
false
);
...
...
spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js
View file @
f44fb5cf
require
(
'
~/extensions/array
'
);
require
(
'
~/filtered_search/filtered_search_visual_tokens
'
);
require
(
'
~/filtered_search/filtered_search_tokenizer
'
);
require
(
'
~/filtered_search/filtered_search_dropdown_manager
'
);
...
...
@@ -14,24 +15,44 @@ require('~/filtered_search/filtered_search_dropdown_manager');
}
beforeEach
(()
=>
{
const
input
=
document
.
createElement
(
'
input
'
);
input
.
classList
.
add
(
'
filtered-search
'
);
document
.
body
.
appendChild
(
input
);
});
afterEach
(()
=>
{
document
.
querySelector
(
'
.filtered-search
'
).
outerHTML
=
''
;
setFixtures
(
`
<ul class="tokens-container">
<li class="input-token">
<input class="filtered-search">
</li>
</ul>
`
)
;
});
describe
(
'
input has no existing value
'
,
()
=>
{
it
(
'
should add just tokenName
'
,
()
=>
{
gl
.
FilteredSearchDropdownManager
.
addWordToInput
(
'
milestone
'
);
expect
(
getInputValue
()).
toBe
(
'
milestone:
'
);
const
token
=
document
.
querySelector
(
'
.tokens-container .js-visual-token
'
);
expect
(
token
.
classList
.
contains
(
'
filtered-search-token
'
)).
toEqual
(
true
);
expect
(
token
.
querySelector
(
'
.name
'
).
innerText
).
toBe
(
'
milestone
'
);
expect
(
getInputValue
()).
toBe
(
''
);
});
it
(
'
should add tokenName and tokenValue
'
,
()
=>
{
gl
.
FilteredSearchDropdownManager
.
addWordToInput
(
'
label
'
);
let
token
=
document
.
querySelector
(
'
.tokens-container .js-visual-token
'
);
expect
(
token
.
classList
.
contains
(
'
filtered-search-token
'
)).
toEqual
(
true
);
expect
(
token
.
querySelector
(
'
.name
'
).
innerText
).
toBe
(
'
label
'
);
expect
(
getInputValue
()).
toBe
(
''
);
gl
.
FilteredSearchDropdownManager
.
addWordToInput
(
'
label
'
,
'
none
'
);
expect
(
getInputValue
()).
toBe
(
'
label:none
'
);
// We have to get that reference again
// Because gl.FilteredSearchDropdownManager deletes the previous token
token
=
document
.
querySelector
(
'
.tokens-container .js-visual-token
'
);
expect
(
token
.
classList
.
contains
(
'
filtered-search-token
'
)).
toEqual
(
true
);
expect
(
token
.
querySelector
(
'
.name
'
).
innerText
).
toBe
(
'
label
'
);
expect
(
token
.
querySelector
(
'
.value
'
).
innerText
).
toBe
(
'
none
'
);
expect
(
getInputValue
()).
toBe
(
''
);
});
});
...
...
@@ -39,19 +60,40 @@ require('~/filtered_search/filtered_search_dropdown_manager');
it
(
'
should be able to just add tokenName
'
,
()
=>
{
setInputValue
(
'
a
'
);
gl
.
FilteredSearchDropdownManager
.
addWordToInput
(
'
author
'
);
expect
(
getInputValue
()).
toBe
(
'
author:
'
);
const
token
=
document
.
querySelector
(
'
.tokens-container .js-visual-token
'
);
expect
(
token
.
classList
.
contains
(
'
filtered-search-token
'
)).
toEqual
(
true
);
expect
(
token
.
querySelector
(
'
.name
'
).
innerText
).
toBe
(
'
author
'
);
expect
(
getInputValue
()).
toBe
(
''
);
});
it
(
'
should replace tokenValue
'
,
()
=>
{
setInputValue
(
'
author:roo
'
);
gl
.
FilteredSearchDropdownManager
.
addWordToInput
(
'
author
'
,
'
@root
'
);
expect
(
getInputValue
()).
toBe
(
'
author:@root
'
);
gl
.
FilteredSearchDropdownManager
.
addWordToInput
(
'
author
'
);
setInputValue
(
'
roo
'
);
gl
.
FilteredSearchDropdownManager
.
addWordToInput
(
null
,
'
@root
'
);
const
token
=
document
.
querySelector
(
'
.tokens-container .js-visual-token
'
);
expect
(
token
.
classList
.
contains
(
'
filtered-search-token
'
)).
toEqual
(
true
);
expect
(
token
.
querySelector
(
'
.name
'
).
innerText
).
toBe
(
'
author
'
);
expect
(
token
.
querySelector
(
'
.value
'
).
innerText
).
toBe
(
'
@root
'
);
expect
(
getInputValue
()).
toBe
(
''
);
});
it
(
'
should add tokenValues containing spaces
'
,
()
=>
{
setInputValue
(
'
label:~"test
'
);
gl
.
FilteredSearchDropdownManager
.
addWordToInput
(
'
label
'
);
setInputValue
(
'
"test
'
);
gl
.
FilteredSearchDropdownManager
.
addWordToInput
(
'
label
'
,
'
~
\'
"test me"
\'
'
);
expect
(
getInputValue
()).
toBe
(
'
label:~
\'
"test me"
\'
'
);
const
token
=
document
.
querySelector
(
'
.tokens-container .js-visual-token
'
);
expect
(
token
.
classList
.
contains
(
'
filtered-search-token
'
)).
toEqual
(
true
);
expect
(
token
.
querySelector
(
'
.name
'
).
innerText
).
toBe
(
'
label
'
);
expect
(
token
.
querySelector
(
'
.value
'
).
innerText
).
toBe
(
'
~
\'
"test me"
\'
'
);
expect
(
getInputValue
()).
toBe
(
''
);
});
});
});
...
...
spec/javascripts/filtered_search/filtered_search_manager_spec.js
View file @
f44fb5cf
...
...
@@ -4,64 +4,244 @@ require('~/filtered_search/filtered_search_token_keys');
require
(
'
~/filtered_search/filtered_search_tokenizer
'
);
require
(
'
~/filtered_search/filtered_search_dropdown_manager
'
);
require
(
'
~/filtered_search/filtered_search_manager
'
);
const
FilteredSearchSpecHelper
=
require
(
'
../helpers/filtered_search_spec_helper
'
);
(()
=>
{
describe
(
'
Filtered Search Manager
'
,
()
=>
{
describe
(
'
search
'
,
()
=>
{
let
manager
;
const
defaultParams
=
'
?scope=all&utf8=✓&state=opened
'
;
let
input
;
let
manager
;
let
tokensContainer
;
const
placeholder
=
'
Search or filter results...
'
;
function
getInput
()
{
return
document
.
querySelector
(
'
.filtered-search
'
);
}
function
dispatchBackspaceEvent
(
element
,
eventType
)
{
const
backspaceKey
=
8
;
const
event
=
new
Event
(
eventType
);
event
.
keyCode
=
backspaceKey
;
element
.
dispatchEvent
(
event
);
}
beforeEach
(()
=>
{
setFixtures
(
`
<input type='text' class='filtered-search' />
`
);
function
dispatchDeleteEvent
(
element
,
eventType
)
{
const
deleteKey
=
46
;
const
event
=
new
Event
(
eventType
);
event
.
keyCode
=
deleteKey
;
element
.
dispatchEvent
(
event
);
}
spyOn
(
gl
.
FilteredSearchManager
.
prototype
,
'
bindEvents
'
).
and
.
callFake
(()
=>
{});
spyOn
(
gl
.
FilteredSearchManager
.
prototype
,
'
cleanup
'
).
and
.
callFake
(()
=>
{});
spyOn
(
gl
.
FilteredSearchManager
.
prototype
,
'
loadSearchParamsFromURL
'
).
and
.
callFake
(()
=>
{});
spyOn
(
gl
.
FilteredSearchDropdownManager
.
prototype
,
'
setDropdown
'
).
and
.
callFake
(()
=>
{});
spyOn
(
gl
.
utils
,
'
getParameterByName
'
).
and
.
returnValue
(
null
);
beforeEach
(()
=>
{
setFixtures
(
`
<div class="filtered-search-input-container">
<form>
<ul class="tokens-container list-unstyled">
${
FilteredSearchSpecHelper
.
createInputHTML
(
placeholder
)}
</ul>
<button class="clear-search" type="button">
<i class="fa fa-times"></i>
</button>
</form>
</div>
`
);
manager
=
new
gl
.
FilteredSearchManager
();
});
spyOn
(
gl
.
FilteredSearchManager
.
prototype
,
'
cleanup
'
).
and
.
callFake
(()
=>
{});
spyOn
(
gl
.
FilteredSearchManager
.
prototype
,
'
loadSearchParamsFromURL
'
).
and
.
callFake
(()
=>
{});
spyOn
(
gl
.
FilteredSearchManager
.
prototype
,
'
tokenChange
'
).
and
.
callFake
(()
=>
{});
spyOn
(
gl
.
FilteredSearchDropdownManager
.
prototype
,
'
setDropdown
'
).
and
.
callFake
(()
=>
{});
spyOn
(
gl
.
FilteredSearchDropdownManager
.
prototype
,
'
updateDropdownOffset
'
).
and
.
callFake
(()
=>
{});
spyOn
(
gl
.
utils
,
'
getParameterByName
'
).
and
.
returnValue
(
null
);
spyOn
(
gl
.
FilteredSearchVisualTokens
,
'
unselectTokens
'
).
and
.
callThrough
();
afterEach
(()
=>
{
getInput
().
outerHTML
=
''
;
});
input
=
document
.
querySelector
(
'
.filtered-search
'
);
tokensContainer
=
document
.
querySelector
(
'
.tokens-container
'
);
manager
=
new
gl
.
FilteredSearchManager
();
});
it
(
'
should search with a single word
'
,
()
=>
{
getInput
().
value
=
'
searchTerm
'
;
describe
(
'
search
'
,
()
=>
{
const
defaultParams
=
'
?scope=all&utf8=✓&state=opened
'
;
it
(
'
should search with a single word
'
,
(
done
)
=>
{
input
.
value
=
'
searchTerm
'
;
spyOn
(
gl
.
utils
,
'
visitUrl
'
).
and
.
callFake
((
url
)
=>
{
expect
(
url
).
toEqual
(
`
${
defaultParams
}
&search=searchTerm`
);
done
();
});
manager
.
search
();
});
it
(
'
should search with multiple words
'
,
()
=>
{
getInput
()
.
value
=
'
awesome search terms
'
;
it
(
'
should search with multiple words
'
,
(
done
)
=>
{
input
.
value
=
'
awesome search terms
'
;
spyOn
(
gl
.
utils
,
'
visitUrl
'
).
and
.
callFake
((
url
)
=>
{
expect
(
url
).
toEqual
(
`
${
defaultParams
}
&search=awesome+search+terms`
);
done
();
});
manager
.
search
();
});
it
(
'
should search with special characters
'
,
()
=>
{
getInput
()
.
value
=
'
~!@#$%^&*()_+{}:<>,.?/
'
;
it
(
'
should search with special characters
'
,
(
done
)
=>
{
input
.
value
=
'
~!@#$%^&*()_+{}:<>,.?/
'
;
spyOn
(
gl
.
utils
,
'
visitUrl
'
).
and
.
callFake
((
url
)
=>
{
expect
(
url
).
toEqual
(
`
${
defaultParams
}
&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`
);
done
();
});
manager
.
search
();
});
});
describe
(
'
handleInputPlaceholder
'
,
()
=>
{
it
(
'
should render placeholder when there is no input
'
,
()
=>
{
expect
(
input
.
placeholder
).
toEqual
(
placeholder
);
});
it
(
'
should not render placeholder when there is input
'
,
()
=>
{
input
.
value
=
'
test words
'
;
const
event
=
new
Event
(
'
input
'
);
input
.
dispatchEvent
(
event
);
expect
(
input
.
placeholder
).
toEqual
(
''
);
});
it
(
'
should not render placeholder when there are tokens and no input
'
,
()
=>
{
tokensContainer
.
innerHTML
=
FilteredSearchSpecHelper
.
createTokensContainerHTML
(
FilteredSearchSpecHelper
.
createFilterVisualTokenHTML
(
'
label
'
,
'
~bug
'
),
);
const
event
=
new
Event
(
'
input
'
);
input
.
dispatchEvent
(
event
);
expect
(
input
.
placeholder
).
toEqual
(
''
);
});
});
describe
(
'
checkForBackspace
'
,
()
=>
{
describe
(
'
tokens and no input
'
,
()
=>
{
beforeEach
(()
=>
{
tokensContainer
.
innerHTML
=
FilteredSearchSpecHelper
.
createTokensContainerHTML
(
FilteredSearchSpecHelper
.
createFilterVisualTokenHTML
(
'
label
'
,
'
~bug
'
),
);
});
it
(
'
removes last token
'
,
()
=>
{
spyOn
(
gl
.
FilteredSearchVisualTokens
,
'
removeLastTokenPartial
'
).
and
.
callThrough
();
dispatchBackspaceEvent
(
input
,
'
keyup
'
);
expect
(
gl
.
FilteredSearchVisualTokens
.
removeLastTokenPartial
).
toHaveBeenCalled
();
});
it
(
'
sets the input
'
,
()
=>
{
spyOn
(
gl
.
FilteredSearchVisualTokens
,
'
getLastTokenPartial
'
).
and
.
callThrough
();
dispatchDeleteEvent
(
input
,
'
keyup
'
);
expect
(
gl
.
FilteredSearchVisualTokens
.
getLastTokenPartial
).
toHaveBeenCalled
();
expect
(
input
.
value
).
toEqual
(
'
~bug
'
);
});
});
it
(
'
does not remove token or change input when there is existing input
'
,
()
=>
{
spyOn
(
gl
.
FilteredSearchVisualTokens
,
'
removeLastTokenPartial
'
).
and
.
callThrough
();
spyOn
(
gl
.
FilteredSearchVisualTokens
,
'
getLastTokenPartial
'
).
and
.
callThrough
();
input
.
value
=
'
text
'
;
dispatchDeleteEvent
(
input
,
'
keyup
'
);
expect
(
gl
.
FilteredSearchVisualTokens
.
removeLastTokenPartial
).
not
.
toHaveBeenCalled
();
expect
(
gl
.
FilteredSearchVisualTokens
.
getLastTokenPartial
).
not
.
toHaveBeenCalled
();
expect
(
input
.
value
).
toEqual
(
'
text
'
);
});
});
describe
(
'
removeSelectedToken
'
,
()
=>
{
function
getVisualTokens
()
{
return
tokensContainer
.
querySelectorAll
(
'
.js-visual-token
'
);
}
beforeEach
(()
=>
{
tokensContainer
.
innerHTML
=
FilteredSearchSpecHelper
.
createTokensContainerHTML
(
FilteredSearchSpecHelper
.
createFilterVisualTokenHTML
(
'
milestone
'
,
'
none
'
,
true
),
);
});
it
(
'
removes selected token when the backspace key is pressed
'
,
()
=>
{
expect
(
getVisualTokens
().
length
).
toEqual
(
1
);
dispatchBackspaceEvent
(
document
,
'
keydown
'
);
expect
(
getVisualTokens
().
length
).
toEqual
(
0
);
});
it
(
'
removes selected token when the delete key is pressed
'
,
()
=>
{
expect
(
getVisualTokens
().
length
).
toEqual
(
1
);
dispatchDeleteEvent
(
document
,
'
keydown
'
);
expect
(
getVisualTokens
().
length
).
toEqual
(
0
);
});
it
(
'
updates the input placeholder after removal
'
,
()
=>
{
manager
.
handleInputPlaceholder
();
expect
(
input
.
placeholder
).
toEqual
(
''
);
expect
(
getVisualTokens
().
length
).
toEqual
(
1
);
dispatchBackspaceEvent
(
document
,
'
keydown
'
);
expect
(
input
.
placeholder
).
not
.
toEqual
(
''
);
expect
(
getVisualTokens
().
length
).
toEqual
(
0
);
});
it
(
'
updates the clear button after removal
'
,
()
=>
{
manager
.
toggleClearSearchButton
();
const
clearButton
=
document
.
querySelector
(
'
.clear-search
'
);
expect
(
clearButton
.
classList
.
contains
(
'
hidden
'
)).
toEqual
(
false
);
expect
(
getVisualTokens
().
length
).
toEqual
(
1
);
dispatchBackspaceEvent
(
document
,
'
keydown
'
);
expect
(
clearButton
.
classList
.
contains
(
'
hidden
'
)).
toEqual
(
true
);
expect
(
getVisualTokens
().
length
).
toEqual
(
0
);
});
});
describe
(
'
unselects token
'
,
()
=>
{
beforeEach
(()
=>
{
tokensContainer
.
innerHTML
=
FilteredSearchSpecHelper
.
createTokensContainerHTML
(
`
${
FilteredSearchSpecHelper
.
createFilterVisualTokenHTML
(
'
label
'
,
'
~bug
'
,
true
)}
${
FilteredSearchSpecHelper
.
createSearchVisualTokenHTML
(
'
search term
'
)}
${
FilteredSearchSpecHelper
.
createFilterVisualTokenHTML
(
'
label
'
,
'
~awesome
'
)}
`
);
});
it
(
'
unselects token when input is clicked
'
,
()
=>
{
const
selectedToken
=
tokensContainer
.
querySelector
(
'
.js-visual-token .selected
'
);
expect
(
selectedToken
.
classList
.
contains
(
'
selected
'
)).
toEqual
(
true
);
expect
(
gl
.
FilteredSearchVisualTokens
.
unselectTokens
).
not
.
toHaveBeenCalled
();
// Click directly on input attached to document
// so that the click event will propagate properly
document
.
querySelector
(
'
.filtered-search
'
).
click
();
expect
(
gl
.
FilteredSearchVisualTokens
.
unselectTokens
).
toHaveBeenCalled
();
expect
(
selectedToken
.
classList
.
contains
(
'
selected
'
)).
toEqual
(
false
);
});
it
(
'
unselects token when document.body is clicked
'
,
()
=>
{
const
selectedToken
=
tokensContainer
.
querySelector
(
'
.js-visual-token .selected
'
);
expect
(
selectedToken
.
classList
.
contains
(
'
selected
'
)).
toEqual
(
true
);
expect
(
gl
.
FilteredSearchVisualTokens
.
unselectTokens
).
not
.
toHaveBeenCalled
();
document
.
body
.
click
();
expect
(
selectedToken
.
classList
.
contains
(
'
selected
'
)).
toEqual
(
false
);
expect
(
gl
.
FilteredSearchVisualTokens
.
unselectTokens
).
toHaveBeenCalled
();
});
});
});
})();
spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
0 → 100644
View file @
f44fb5cf
require
(
'
~/filtered_search/filtered_search_visual_tokens
'
);
const
FilteredSearchSpecHelper
=
require
(
'
../helpers/filtered_search_spec_helper
'
);
describe
(
'
Filtered Search Visual Tokens
'
,
()
=>
{
let
tokensContainer
;
beforeEach
(()
=>
{
setFixtures
(
`
<ul class="tokens-container">
${
FilteredSearchSpecHelper
.
createInputHTML
()}
</ul>
`
);
tokensContainer
=
document
.
querySelector
(
'
.tokens-container
'
);
});
describe
(
'
getLastVisualTokenBeforeInput
'
,
()
=>
{
it
(
'
returns when there are no visual tokens
'
,
()
=>
{
const
{
lastVisualToken
,
isLastVisualTokenValid
}
=
gl
.
FilteredSearchVisualTokens
.
getLastVisualTokenBeforeInput
();
expect
(
lastVisualToken
).
toEqual
(
null
);
expect
(
isLastVisualTokenValid
).
toEqual
(
true
);
});
describe
(
'
input is the last item in tokensContainer
'
,
()
=>
{
it
(
'
returns when there is one visual token
'
,
()
=>
{
tokensContainer
.
innerHTML
=
FilteredSearchSpecHelper
.
createTokensContainerHTML
(
FilteredSearchSpecHelper
.
createFilterVisualTokenHTML
(
'
label
'
,
'
~bug
'
),
);
const
{
lastVisualToken
,
isLastVisualTokenValid
}
=
gl
.
FilteredSearchVisualTokens
.
getLastVisualTokenBeforeInput
();
expect
(
lastVisualToken
).
toEqual
(
document
.
querySelector
(
'
.filtered-search-token
'
));
expect
(
isLastVisualTokenValid
).
toEqual
(
true
);
});
it
(
'
returns when there is an incomplete visual token
'
,
()
=>
{
tokensContainer
.
innerHTML
=
FilteredSearchSpecHelper
.
createTokensContainerHTML
(
FilteredSearchSpecHelper
.
createNameFilterVisualTokenHTML
(
'
Author
'
),
);
const
{
lastVisualToken
,
isLastVisualTokenValid
}
=
gl
.
FilteredSearchVisualTokens
.
getLastVisualTokenBeforeInput
();
expect
(
lastVisualToken
).
toEqual
(
document
.
querySelector
(
'
.filtered-search-token
'
));
expect
(
isLastVisualTokenValid
).
toEqual
(
false
);
});
it
(
'
returns when there are multiple visual tokens
'
,
()
=>
{
tokensContainer
.
innerHTML
=
FilteredSearchSpecHelper
.
createTokensContainerHTML
(
`
${
FilteredSearchSpecHelper
.
createFilterVisualTokenHTML
(
'
label
'
,
'
~bug
'
)}
${
FilteredSearchSpecHelper
.
createSearchVisualTokenHTML
(
'
search term
'
)}
${
FilteredSearchSpecHelper
.
createFilterVisualTokenHTML
(
'
author
'
,
'
@root
'
)}
`
);
const
{
lastVisualToken
,
isLastVisualTokenValid
}
=
gl
.
FilteredSearchVisualTokens
.
getLastVisualTokenBeforeInput
();
const
items
=
document
.
querySelectorAll
(
'
.tokens-container .js-visual-token
'
);
expect
(
lastVisualToken
.
isEqualNode
(
items
[
items
.
length
-
1
])).
toEqual
(
true
);
expect
(
isLastVisualTokenValid
).
toEqual
(
true
);
});
it
(
'
returns when there are multiple visual tokens and an incomplete visual token
'
,
()
=>
{
tokensContainer
.
innerHTML
=
FilteredSearchSpecHelper
.
createTokensContainerHTML
(
`
${
FilteredSearchSpecHelper
.
createFilterVisualTokenHTML
(
'
label
'
,
'
~bug
'
)}
${
FilteredSearchSpecHelper
.
createSearchVisualTokenHTML
(
'
search term
'
)}
${
FilteredSearchSpecHelper
.
createNameFilterVisualTokenHTML
(
'
assignee
'
)}
`
);
const
{
lastVisualToken
,
isLastVisualTokenValid
}
=
gl
.
FilteredSearchVisualTokens
.
getLastVisualTokenBeforeInput
();
const
items
=
document
.
querySelectorAll
(
'
.tokens-container .js-visual-token
'
);
expect
(
lastVisualToken
.
isEqualNode
(
items
[
items
.
length
-
1
])).
toEqual
(
true
);
expect
(
isLastVisualTokenValid
).
toEqual
(
false
);
});
});
describe
(
'
input is a middle item in tokensContainer
'
,
()
=>
{
it
(
'
returns last token before input
'
,
()
=>
{
tokensContainer
.
innerHTML
=
FilteredSearchSpecHelper
.
createTokensContainerHTML
(
`
${
FilteredSearchSpecHelper
.
createFilterVisualTokenHTML
(
'
label
'
,
'
~bug
'
)}
${
FilteredSearchSpecHelper
.
createInputHTML
()}
${
FilteredSearchSpecHelper
.
createFilterVisualTokenHTML
(
'
author
'
,
'
@root
'
)}
`
);
const
{
lastVisualToken
,
isLastVisualTokenValid
}
=
gl
.
FilteredSearchVisualTokens
.
getLastVisualTokenBeforeInput
();
expect
(
lastVisualToken
).
toEqual
(
document
.
querySelector
(
'
.filtered-search-token
'
));
expect
(
isLastVisualTokenValid
).
toEqual
(
true
);
});
it
(
'
returns last partial token before input
'
,
()
=>
{
tokensContainer
.
innerHTML
=
FilteredSearchSpecHelper
.
createTokensContainerHTML
(
`
${
FilteredSearchSpecHelper
.
createNameFilterVisualTokenHTML
(
'
label
'
)}
${
FilteredSearchSpecHelper
.
createInputHTML
()}
${
FilteredSearchSpecHelper
.
createFilterVisualTokenHTML
(
'
author
'
,
'
@root
'
)}
`
);
const
{
lastVisualToken
,
isLastVisualTokenValid
}
=
gl
.
FilteredSearchVisualTokens
.
getLastVisualTokenBeforeInput
();
expect
(
lastVisualToken
).
toEqual
(
document
.
querySelector
(
'
.filtered-search-token
'
));
expect
(
isLastVisualTokenValid
).
toEqual
(
false
);
});
});
});
describe
(
'
unselectTokens
'
,
()
=>
{
it
(
'
does nothing when there are no tokens
'
,
()
=>
{
const
beforeHTML
=
tokensContainer
.
innerHTML
;
gl
.
FilteredSearchVisualTokens
.
unselectTokens
();
expect
(
tokensContainer
.
innerHTML
).
toEqual
(
beforeHTML
);
});
it
(
'
removes the selected class from buttons
'
,
()
=>
{
tokensContainer
.
innerHTML
=
FilteredSearchSpecHelper
.
createTokensContainerHTML
(
`
${
FilteredSearchSpecHelper
.
createFilterVisualTokenHTML
(
'
author
'
,
'
@author
'
)}
${
FilteredSearchSpecHelper
.
createFilterVisualTokenHTML
(
'
milestone
'
,
'
%123
'
,
true
)}
`
);
const
selected
=
tokensContainer
.
querySelector
(
'
.js-visual-token .selected
'
);
expect
(
selected
.
classList
.
contains
(
'
selected
'
)).
toEqual
(
true
);
gl
.
FilteredSearchVisualTokens
.
unselectTokens
();
expect
(
selected
.
classList
.
contains
(
'
selected
'
)).
toEqual
(
false
);
});
});
describe
(
'
selectToken
'
,
()
=>
{
beforeEach
(()
=>
{
tokensContainer
.
innerHTML
=
FilteredSearchSpecHelper
.
createTokensContainerHTML
(
`
${
FilteredSearchSpecHelper
.
createFilterVisualTokenHTML
(
'
label
'
,
'
~bug
'
)}
${
FilteredSearchSpecHelper
.
createSearchVisualTokenHTML
(
'
search term
'
)}
${
FilteredSearchSpecHelper
.
createFilterVisualTokenHTML
(
'
label
'
,
'
~awesome
'
)}
`
);
});
it
(
'
removes the selected class if it has selected class
'
,
()
=>
{
const
firstTokenButton
=
tokensContainer
.
querySelector
(
'
.js-visual-token .selectable
'
);
firstTokenButton
.
classList
.
add
(
'
selected
'
);
gl
.
FilteredSearchVisualTokens
.
selectToken
(
firstTokenButton
);
expect
(
firstTokenButton
.
classList
.
contains
(
'
selected
'
)).
toEqual
(
false
);
});
describe
(
'
has no selected class
'
,
()
=>
{
it
(
'
adds selected class
'
,
()
=>
{
const
firstTokenButton
=
tokensContainer
.
querySelector
(
'
.js-visual-token .selectable
'
);
gl
.
FilteredSearchVisualTokens
.
selectToken
(
firstTokenButton
);
expect
(
firstTokenButton
.
classList
.
contains
(
'
selected
'
)).
toEqual
(
true
);
});
it
(
'
removes selected class from other tokens
'
,
()
=>
{
const
tokenButtons
=
tokensContainer
.
querySelectorAll
(
'
.js-visual-token .selectable
'
);
tokenButtons
[
1
].
classList
.
add
(
'
selected
'
);
gl
.
FilteredSearchVisualTokens
.
selectToken
(
tokenButtons
[
0
]);
expect
(
tokenButtons
[
0
].
classList
.
contains
(
'
selected
'
)).
toEqual
(
true
);
expect
(
tokenButtons
[
1
].
classList
.
contains
(
'
selected
'
)).
toEqual
(
false
);
});
});
});
describe
(
'
removeSelectedToken
'
,
()
=>
{
it
(
'
does not remove when there are no selected tokens
'
,
()
=>
{
tokensContainer
.
innerHTML
=
FilteredSearchSpecHelper
.
createTokensContainerHTML
(
FilteredSearchSpecHelper
.
createFilterVisualTokenHTML
(
'
milestone
'
,
'
none
'
),
);
expect
(
tokensContainer
.
querySelector
(
'
.js-visual-token .selectable
'
)).
not
.
toEqual
(
null
);
gl
.
FilteredSearchVisualTokens
.
removeSelectedToken
();
expect
(
tokensContainer
.
querySelector
(
'
.js-visual-token .selectable
'
)).
not
.
toEqual
(
null
);
});
it
(
'
removes selected token
'
,
()
=>
{
tokensContainer
.
innerHTML
=
FilteredSearchSpecHelper
.
createTokensContainerHTML
(
FilteredSearchSpecHelper
.
createFilterVisualTokenHTML
(
'
milestone
'
,
'
none
'
,
true
),
);
expect
(
tokensContainer
.
querySelector
(
'
.js-visual-token .selectable
'
)).
not
.
toEqual
(
null
);
gl
.
FilteredSearchVisualTokens
.
removeSelectedToken
();
expect
(
tokensContainer
.
querySelector
(
'
.js-visual-token .selectable
'
)).
toEqual
(
null
);
});
});
describe
(
'
createVisualTokenElementHTML
'
,
()
=>
{
let
tokenElement
;
beforeEach
(()
=>
{
setFixtures
(
`
<div class="test-area">
${
gl
.
FilteredSearchVisualTokens
.
createVisualTokenElementHTML
()}
</div>
`
);
tokenElement
=
document
.
querySelector
(
'
.test-area
'
).
firstElementChild
;
});
it
(
'
contains name div
'
,
()
=>
{
expect
(
tokenElement
.
querySelector
(
'
.name
'
)).
toEqual
(
jasmine
.
anything
());
});
it
(
'
contains value div
'
,
()
=>
{
expect
(
tokenElement
.
querySelector
(
'
.value
'
)).
toEqual
(
jasmine
.
anything
());
});
it
(
'
contains selectable class
'
,
()
=>
{
expect
(
tokenElement
.
classList
.
contains
(
'
selectable
'
)).
toEqual
(
true
);
});
it
(
'
contains button role
'
,
()
=>
{
expect
(
tokenElement
.
getAttribute
(
'
role
'
)).
toEqual
(
'
button
'
);
});
});
describe
(
'
addVisualTokenElement
'
,
()
=>
{
it
(
'
renders search visual tokens
'
,
()
=>
{
gl
.
FilteredSearchVisualTokens
.
addVisualTokenElement
(
'
search term
'
,
null
,
true
);
const
token
=
tokensContainer
.
querySelector
(
'
.js-visual-token
'
);
expect
(
token
.
classList
.
contains
(
'
filtered-search-term
'
)).
toEqual
(
true
);
expect
(
token
.
querySelector
(
'
.name
'
).
innerText
).
toEqual
(
'
search term
'
);
expect
(
token
.
querySelector
(
'
.value
'
)).
toEqual
(
null
);
});
it
(
'
renders filter visual token name
'
,
()
=>
{
gl
.
FilteredSearchVisualTokens
.
addVisualTokenElement
(
'
milestone
'
);
const
token
=
tokensContainer
.
querySelector
(
'
.js-visual-token
'
);
expect
(
token
.
classList
.
contains
(
'
filtered-search-token
'
)).
toEqual
(
true
);
expect
(
token
.
querySelector
(
'
.name
'
).
innerText
).
toEqual
(
'
milestone
'
);
expect
(
token
.
querySelector
(
'
.value
'
)).
toEqual
(
null
);
});
it
(
'
renders filter visual token name and value
'
,
()
=>
{
gl
.
FilteredSearchVisualTokens
.
addVisualTokenElement
(
'
label
'
,
'
Frontend
'
);
const
token
=
tokensContainer
.
querySelector
(
'
.js-visual-token
'
);
expect
(
token
.
classList
.
contains
(
'
filtered-search-token
'
)).
toEqual
(
true
);
expect
(
token
.
querySelector
(
'
.name
'
).
innerText
).
toEqual
(
'
label
'
);
expect
(
token
.
querySelector
(
'
.value
'
).
innerText
).
toEqual
(
'
Frontend
'
);
});
it
(
'
inserts visual token before input
'
,
()
=>
{
tokensContainer
.
appendChild
(
FilteredSearchSpecHelper
.
createFilterVisualToken
(
'
assignee
'
,
'
@root
'
));
gl
.
FilteredSearchVisualTokens
.
addVisualTokenElement
(
'
label
'
,
'
Frontend
'
);
const
tokens
=
tokensContainer
.
querySelectorAll
(
'
.js-visual-token
'
);
const
labelToken
=
tokens
[
0
];
const
assigneeToken
=
tokens
[
1
];
expect
(
labelToken
.
classList
.
contains
(
'
filtered-search-token
'
)).
toEqual
(
true
);
expect
(
labelToken
.
querySelector
(
'
.name
'
).
innerText
).
toEqual
(
'
label
'
);
expect
(
labelToken
.
querySelector
(
'
.value
'
).
innerText
).
toEqual
(
'
Frontend
'
);
expect
(
assigneeToken
.
classList
.
contains
(
'
filtered-search-token
'
)).
toEqual
(
true
);
expect
(
assigneeToken
.
querySelector
(
'
.name
'
).
innerText
).
toEqual
(
'
assignee
'
);
expect
(
assigneeToken
.
querySelector
(
'
.value
'
).
innerText
).
toEqual
(
'
@root
'
);
});
});
describe
(
'
addValueToPreviousVisualTokenElement
'
,
()
=>
{
it
(
'
does not add when previous visual token element has no value
'
,
()
=>
{
tokensContainer
.
innerHTML
=
FilteredSearchSpecHelper
.
createTokensContainerHTML
(
FilteredSearchSpecHelper
.
createFilterVisualTokenHTML
(
'
author
'
,
'
@root
'
),
);
const
original
=
tokensContainer
.
innerHTML
;
gl
.
FilteredSearchVisualTokens
.
addValueToPreviousVisualTokenElement
(
'
value
'
);
expect
(
original
).
toEqual
(
tokensContainer
.
innerHTML
);
});
it
(
'
does not add when previous visual token element is a search
'
,
()
=>
{
tokensContainer
.
innerHTML
=
FilteredSearchSpecHelper
.
createTokensContainerHTML
(
`
${
FilteredSearchSpecHelper
.
createFilterVisualTokenHTML
(
'
author
'
,
'
@root
'
)}
${
FilteredSearchSpecHelper
.
createSearchVisualTokenHTML
(
'
search term
'
)}
`
);
const
original
=
tokensContainer
.
innerHTML
;
gl
.
FilteredSearchVisualTokens
.
addValueToPreviousVisualTokenElement
(
'
value
'
);
expect
(
original
).
toEqual
(
tokensContainer
.
innerHTML
);
});
it
(
'
adds value to previous visual filter token
'
,
()
=>
{
tokensContainer
.
innerHTML
=
FilteredSearchSpecHelper
.
createTokensContainerHTML
(
FilteredSearchSpecHelper
.
createNameFilterVisualTokenHTML
(
'
label
'
),
);
const
original
=
tokensContainer
.
innerHTML
;
gl
.
FilteredSearchVisualTokens
.
addValueToPreviousVisualTokenElement
(
'
value
'
);
const
updatedToken
=
tokensContainer
.
querySelector
(
'
.js-visual-token
'
);
expect
(
updatedToken
.
querySelector
(
'
.name
'
).
innerText
).
toEqual
(
'
label
'
);
expect
(
updatedToken
.
querySelector
(
'
.value
'
).
innerText
).
toEqual
(
'
value
'
);
expect
(
original
).
not
.
toEqual
(
tokensContainer
.
innerHTML
);
});
});
describe
(
'
addFilterVisualToken
'
,
()
=>
{
it
(
'
creates visual token with just tokenName
'
,
()
=>
{
gl
.
FilteredSearchVisualTokens
.
addFilterVisualToken
(
'
milestone
'
);
const
token
=
tokensContainer
.
querySelector
(
'
.js-visual-token
'
);
expect
(
token
.
classList
.
contains
(
'
filtered-search-token
'
)).
toEqual
(
true
);
expect
(
token
.
querySelector
(
'
.name
'
).
innerText
).
toEqual
(
'
milestone
'
);
expect
(
token
.
querySelector
(
'
.value
'
)).
toEqual
(
null
);
});
it
(
'
creates visual token with just tokenValue
'
,
()
=>
{
gl
.
FilteredSearchVisualTokens
.
addFilterVisualToken
(
'
milestone
'
);
gl
.
FilteredSearchVisualTokens
.
addFilterVisualToken
(
'
%8.17
'
);
const
token
=
tokensContainer
.
querySelector
(
'
.js-visual-token
'
);
expect
(
token
.
classList
.
contains
(
'
filtered-search-token
'
)).
toEqual
(
true
);
expect
(
token
.
querySelector
(
'
.name
'
).
innerText
).
toEqual
(
'
milestone
'
);
expect
(
token
.
querySelector
(
'
.value
'
).
innerText
).
toEqual
(
'
%8.17
'
);
});
it
(
'
creates full visual token
'
,
()
=>
{
gl
.
FilteredSearchVisualTokens
.
addFilterVisualToken
(
'
assignee
'
,
'
@john
'
);
const
token
=
tokensContainer
.
querySelector
(
'
.js-visual-token
'
);
expect
(
token
.
classList
.
contains
(
'
filtered-search-token
'
)).
toEqual
(
true
);
expect
(
token
.
querySelector
(
'
.name
'
).
innerText
).
toEqual
(
'
assignee
'
);
expect
(
token
.
querySelector
(
'
.value
'
).
innerText
).
toEqual
(
'
@john
'
);
});
});
describe
(
'
addSearchVisualToken
'
,
()
=>
{
it
(
'
creates search visual token
'
,
()
=>
{
gl
.
FilteredSearchVisualTokens
.
addSearchVisualToken
(
'
search term
'
);
const
token
=
tokensContainer
.
querySelector
(
'
.js-visual-token
'
);
expect
(
token
.
classList
.
contains
(
'
filtered-search-term
'
)).
toEqual
(
true
);
expect
(
token
.
querySelector
(
'
.name
'
).
innerText
).
toEqual
(
'
search term
'
);
expect
(
token
.
querySelector
(
'
.value
'
)).
toEqual
(
null
);
});
it
(
'
appends to previous search visual token if previous token was a search token
'
,
()
=>
{
tokensContainer
.
innerHTML
=
FilteredSearchSpecHelper
.
createTokensContainerHTML
(
`
${
FilteredSearchSpecHelper
.
createFilterVisualTokenHTML
(
'
author
'
,
'
@root
'
)}
${
FilteredSearchSpecHelper
.
createSearchVisualTokenHTML
(
'
search term
'
)}
`
);
gl
.
FilteredSearchVisualTokens
.
addSearchVisualToken
(
'
append this
'
);
const
token
=
tokensContainer
.
querySelector
(
'
.filtered-search-term
'
);
expect
(
token
.
querySelector
(
'
.name
'
).
innerText
).
toEqual
(
'
search term append this
'
);
expect
(
token
.
querySelector
(
'
.value
'
)).
toEqual
(
null
);
});
});
describe
(
'
getLastTokenPartial
'
,
()
=>
{
it
(
'
should get last token value
'
,
()
=>
{
const
value
=
'
~bug
'
;
tokensContainer
.
innerHTML
=
FilteredSearchSpecHelper
.
createTokensContainerHTML
(
FilteredSearchSpecHelper
.
createFilterVisualTokenHTML
(
'
label
'
,
value
),
);
expect
(
gl
.
FilteredSearchVisualTokens
.
getLastTokenPartial
()).
toEqual
(
value
);
});
it
(
'
should get last token name if there is no value
'
,
()
=>
{
const
name
=
'
assignee
'
;
tokensContainer
.
innerHTML
=
FilteredSearchSpecHelper
.
createTokensContainerHTML
(
FilteredSearchSpecHelper
.
createNameFilterVisualTokenHTML
(
name
),
);
expect
(
gl
.
FilteredSearchVisualTokens
.
getLastTokenPartial
()).
toEqual
(
name
);
});
it
(
'
should return empty when there are no tokens
'
,
()
=>
{
expect
(
gl
.
FilteredSearchVisualTokens
.
getLastTokenPartial
()).
toEqual
(
''
);
});
});
describe
(
'
removeLastTokenPartial
'
,
()
=>
{
it
(
'
should remove the last token value if it exists
'
,
()
=>
{
tokensContainer
.
innerHTML
=
FilteredSearchSpecHelper
.
createTokensContainerHTML
(
FilteredSearchSpecHelper
.
createFilterVisualTokenHTML
(
'
label
'
,
'
~"Community Contribution"
'
),
);
expect
(
tokensContainer
.
querySelector
(
'
.js-visual-token .value
'
)).
not
.
toEqual
(
null
);
gl
.
FilteredSearchVisualTokens
.
removeLastTokenPartial
();
expect
(
tokensContainer
.
querySelector
(
'
.js-visual-token .value
'
)).
toEqual
(
null
);
});
it
(
'
should remove the last token name if there is no value
'
,
()
=>
{
tokensContainer
.
innerHTML
=
FilteredSearchSpecHelper
.
createTokensContainerHTML
(
FilteredSearchSpecHelper
.
createNameFilterVisualTokenHTML
(
'
milestone
'
),
);
expect
(
tokensContainer
.
querySelector
(
'
.js-visual-token .name
'
)).
not
.
toEqual
(
null
);
gl
.
FilteredSearchVisualTokens
.
removeLastTokenPartial
();
expect
(
tokensContainer
.
querySelector
(
'
.js-visual-token .name
'
)).
toEqual
(
null
);
});
it
(
'
should not remove anything when there are no tokens
'
,
()
=>
{
const
html
=
tokensContainer
.
innerHTML
;
gl
.
FilteredSearchVisualTokens
.
removeLastTokenPartial
();
expect
(
tokensContainer
.
innerHTML
).
toEqual
(
html
);
});
});
describe
(
'
tokenizeInput
'
,
()
=>
{
it
(
'
does not do anything if there is no input
'
,
()
=>
{
const
original
=
tokensContainer
.
innerHTML
;
gl
.
FilteredSearchVisualTokens
.
tokenizeInput
();
expect
(
tokensContainer
.
innerHTML
).
toEqual
(
original
);
});
it
(
'
adds search visual token if previous visual token is valid
'
,
()
=>
{
tokensContainer
.
innerHTML
=
FilteredSearchSpecHelper
.
createTokensContainerHTML
(
FilteredSearchSpecHelper
.
createFilterVisualTokenHTML
(
'
assignee
'
,
'
none
'
),
);
const
input
=
document
.
querySelector
(
'
.filtered-search
'
);
input
.
value
=
'
some value
'
;
gl
.
FilteredSearchVisualTokens
.
tokenizeInput
();
const
newToken
=
tokensContainer
.
querySelector
(
'
.filtered-search-term
'
);
expect
(
input
.
value
).
toEqual
(
''
);
expect
(
newToken
.
querySelector
(
'
.name
'
).
innerText
).
toEqual
(
'
some value
'
);
expect
(
newToken
.
querySelector
(
'
.value
'
)).
toEqual
(
null
);
});
it
(
'
adds value to previous visual token element if previous visual token is invalid
'
,
()
=>
{
tokensContainer
.
innerHTML
=
FilteredSearchSpecHelper
.
createTokensContainerHTML
(
FilteredSearchSpecHelper
.
createNameFilterVisualTokenHTML
(
'
assignee
'
),
);
const
input
=
document
.
querySelector
(
'
.filtered-search
'
);
input
.
value
=
'
@john
'
;
gl
.
FilteredSearchVisualTokens
.
tokenizeInput
();
const
updatedToken
=
tokensContainer
.
querySelector
(
'
.filtered-search-token
'
);
expect
(
input
.
value
).
toEqual
(
''
);
expect
(
updatedToken
.
querySelector
(
'
.name
'
).
innerText
).
toEqual
(
'
assignee
'
);
expect
(
updatedToken
.
querySelector
(
'
.value
'
).
innerText
).
toEqual
(
'
@john
'
);
});
});
describe
(
'
editToken
'
,
()
=>
{
let
input
;
let
token
;
beforeEach
(()
=>
{
tokensContainer
.
innerHTML
=
FilteredSearchSpecHelper
.
createTokensContainerHTML
(
`
${
FilteredSearchSpecHelper
.
createFilterVisualTokenHTML
(
'
label
'
,
'
none
'
)}
${
FilteredSearchSpecHelper
.
createSearchVisualTokenHTML
(
'
search
'
)}
${
FilteredSearchSpecHelper
.
createFilterVisualTokenHTML
(
'
milestone
'
,
'
upcoming
'
)}
`
);
input
=
document
.
querySelector
(
'
.filtered-search
'
);
token
=
document
.
querySelector
(
'
.js-visual-token
'
);
});
it
(
'
tokenize
\'
s existing input
'
,
()
=>
{
input
.
value
=
'
some text
'
;
spyOn
(
gl
.
FilteredSearchVisualTokens
,
'
tokenizeInput
'
).
and
.
callThrough
();
gl
.
FilteredSearchVisualTokens
.
editToken
(
token
);
expect
(
gl
.
FilteredSearchVisualTokens
.
tokenizeInput
).
toHaveBeenCalled
();
expect
(
input
.
value
).
not
.
toEqual
(
'
some text
'
);
});
it
(
'
moves input to the token position
'
,
()
=>
{
expect
(
tokensContainer
.
children
[
3
].
querySelector
(
'
.filtered-search
'
)).
not
.
toEqual
(
null
);
gl
.
FilteredSearchVisualTokens
.
editToken
(
token
);
expect
(
tokensContainer
.
children
[
1
].
querySelector
(
'
.filtered-search
'
)).
not
.
toEqual
(
null
);
expect
(
tokensContainer
.
children
[
3
].
querySelector
(
'
.filtered-search
'
)).
toEqual
(
null
);
});
it
(
'
input contains the visual token value
'
,
()
=>
{
gl
.
FilteredSearchVisualTokens
.
editToken
(
token
);
expect
(
input
.
value
).
toEqual
(
'
none
'
);
});
describe
(
'
selected token is a search term token
'
,
()
=>
{
beforeEach
(()
=>
{
token
=
document
.
querySelector
(
'
.filtered-search-term
'
);
});
it
(
'
token is removed
'
,
()
=>
{
expect
(
tokensContainer
.
querySelector
(
'
.filtered-search-term
'
)).
not
.
toEqual
(
null
);
gl
.
FilteredSearchVisualTokens
.
editToken
(
token
);
expect
(
tokensContainer
.
querySelector
(
'
.filtered-search-term
'
)).
toEqual
(
null
);
});
it
(
'
input has the same value as removed token
'
,
()
=>
{
expect
(
input
.
value
).
toEqual
(
''
);
gl
.
FilteredSearchVisualTokens
.
editToken
(
token
);
expect
(
input
.
value
).
toEqual
(
'
search
'
);
});
});
});
describe
(
'
moveInputTotheRight
'
,
()
=>
{
it
(
'
does nothing if the input is already the right most element
'
,
()
=>
{
tokensContainer
.
innerHTML
=
FilteredSearchSpecHelper
.
createTokensContainerHTML
(
FilteredSearchSpecHelper
.
createFilterVisualTokenHTML
(
'
label
'
,
'
none
'
),
);
spyOn
(
gl
.
FilteredSearchVisualTokens
,
'
tokenizeInput
'
).
and
.
callThrough
();
spyOn
(
gl
.
FilteredSearchVisualTokens
,
'
getLastVisualTokenBeforeInput
'
).
and
.
callThrough
();
gl
.
FilteredSearchVisualTokens
.
moveInputToTheRight
();
expect
(
gl
.
FilteredSearchVisualTokens
.
tokenizeInput
).
not
.
toHaveBeenCalled
();
expect
(
gl
.
FilteredSearchVisualTokens
.
getLastVisualTokenBeforeInput
).
not
.
toHaveBeenCalled
();
});
it
(
'
tokenize
\'
s input
'
,
()
=>
{
tokensContainer
.
innerHTML
=
`
${
FilteredSearchSpecHelper
.
createNameFilterVisualTokenHTML
(
'
label
'
)}
${
FilteredSearchSpecHelper
.
createInputHTML
()}
${
FilteredSearchSpecHelper
.
createFilterVisualTokenHTML
(
'
label
'
,
'
~bug
'
)}
`
;
document
.
querySelector
(
'
.filtered-search
'
).
value
=
'
none
'
;
gl
.
FilteredSearchVisualTokens
.
moveInputToTheRight
();
const
value
=
tokensContainer
.
querySelector
(
'
.js-visual-token .value
'
);
expect
(
value
.
innerText
).
toEqual
(
'
none
'
);
});
it
(
'
converts input into search term token if last token is valid
'
,
()
=>
{
tokensContainer
.
innerHTML
=
`
${
FilteredSearchSpecHelper
.
createFilterVisualTokenHTML
(
'
label
'
,
'
none
'
)}
${
FilteredSearchSpecHelper
.
createInputHTML
()}
${
FilteredSearchSpecHelper
.
createFilterVisualTokenHTML
(
'
label
'
,
'
~bug
'
)}
`
;
document
.
querySelector
(
'
.filtered-search
'
).
value
=
'
test
'
;
gl
.
FilteredSearchVisualTokens
.
moveInputToTheRight
();
const
searchValue
=
tokensContainer
.
querySelector
(
'
.filtered-search-term .name
'
);
expect
(
searchValue
.
innerText
).
toEqual
(
'
test
'
);
});
it
(
'
moves the input to the right most element
'
,
()
=>
{
tokensContainer
.
innerHTML
=
`
${
FilteredSearchSpecHelper
.
createFilterVisualTokenHTML
(
'
label
'
,
'
none
'
)}
${
FilteredSearchSpecHelper
.
createInputHTML
()}
${
FilteredSearchSpecHelper
.
createFilterVisualTokenHTML
(
'
label
'
,
'
~bug
'
)}
`
;
gl
.
FilteredSearchVisualTokens
.
moveInputToTheRight
();
expect
(
tokensContainer
.
children
[
2
].
querySelector
(
'
.filtered-search
'
)).
not
.
toEqual
(
null
);
});
});
});
spec/javascripts/helpers/filtered_search_spec_helper.js
0 → 100644
View file @
f44fb5cf
class
FilteredSearchSpecHelper
{
static
createFilterVisualTokenHTML
(
name
,
value
,
isSelected
)
{
return
FilteredSearchSpecHelper
.
createFilterVisualToken
(
name
,
value
,
isSelected
).
outerHTML
;
}
static
createFilterVisualToken
(
name
,
value
,
isSelected
=
false
)
{
const
li
=
document
.
createElement
(
'
li
'
);
li
.
classList
.
add
(
'
js-visual-token
'
,
'
filtered-search-token
'
);
li
.
innerHTML
=
`
<div class="selectable
${
isSelected
?
'
selected
'
:
''
}
" role="button">
<div class="name">
${
name
}
</div>
<div class="value">
${
value
}
</div>
</div>
`
;
return
li
;
}
static
createNameFilterVisualTokenHTML
(
name
)
{
return
`
<li class="js-visual-token filtered-search-token">
<div class="name">
${
name
}
</div>
</li>
`
;
}
static
createSearchVisualTokenHTML
(
name
)
{
return
`
<li class="js-visual-token filtered-search-term">
<div class="name">
${
name
}
</div>
</li>
`
;
}
static
createInputHTML
(
placeholder
=
''
)
{
return
`
<li class="input-token">
<input type='text' class='filtered-search' placeholder='
${
placeholder
}
' />
</li>
`
;
}
static
createTokensContainerHTML
(
html
,
inputPlaceholder
)
{
return
`
${
html
}
${
FilteredSearchSpecHelper
.
createInputHTML
(
inputPlaceholder
)}
`
;
}
}
module
.
exports
=
FilteredSearchSpecHelper
;
spec/support/filtered_search_helpers.rb
View file @
f44fb5cf
...
...
@@ -3,16 +3,20 @@ module FilteredSearchHelpers
page
.
find
(
'.filtered-search'
)
end
# Enables input to be set (similar to copy and paste)
def
input_filtered_search
(
search_term
,
submit:
true
)
filtered_search
.
set
(
search_term
)
# Add an extra space to engage visual tokens
filtered_search
.
set
(
"
#{
search_term
}
"
)
if
submit
filtered_search
.
send_keys
(
:enter
)
end
end
# Enables input to be added character by character
def
input_filtered_search_keys
(
search_term
)
filtered_search
.
send_keys
(
search_term
)
# Add an extra space to engage visual tokens
filtered_search
.
send_keys
(
"
#{
search_term
}
"
)
filtered_search
.
send_keys
(
:enter
)
end
...
...
@@ -34,4 +38,32 @@ module FilteredSearchHelpers
# This ensures the dropdown is shown
expect
(
find
(
'#js-dropdown-label'
)).
not_to
have_css
(
'.filter-dropdown-loading'
)
end
def
expect_filtered_search_input_empty
expect
(
find
(
'.filtered-search'
).
value
).
to
eq
(
''
)
end
# Iterates through each visual token inside
# .tokens-container to make sure the correct names and values are rendered
def
expect_tokens
(
tokens
)
page
.
find
'.filtered-search-input-container .tokens-container'
do
page
.
all
(
:css
,
'.tokens-container li'
).
each_with_index
do
|
el
,
index
|
token_name
=
tokens
[
index
][
:name
]
token_value
=
tokens
[
index
][
:value
]
expect
(
el
.
find
(
'.name'
)).
to
have_content
(
token_name
)
if
token_value
expect
(
el
.
find
(
'.value'
)).
to
have_content
(
token_value
)
end
end
end
end
def
default_placeholder
'Search or filter results...'
end
def
get_filtered_search_placeholder
find
(
'.filtered-search'
)[
'placeholder'
]
end
end
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