Commit f44fb5cf authored by Clement Ho's avatar Clement Ho

Add filtered search visual tokens

parent b5cb1115
...@@ -37,11 +37,14 @@ require('../window')(function(w){ ...@@ -37,11 +37,14 @@ require('../window')(function(w){
} }
} }
if (!self.destroyed) {
self.hook.list[config.method].call(self.hook.list, data); self.hook.list[config.method].call(self.hook.list, data);
}
}, },
init: function init(hook) { init: function init(hook) {
var self = this; var self = this;
self.destroyed = false;
self.cache = self.cache || {}; self.cache = self.cache || {};
var config = hook.config.droplabAjax; var config = hook.config.droplabAjax;
this.hook = hook; this.hook = hook;
...@@ -79,6 +82,7 @@ require('../window')(function(w){ ...@@ -79,6 +82,7 @@ require('../window')(function(w){
destroy: function() { destroy: function() {
var dynamicList = this.hook.list.list.querySelector('[data-dynamic]'); var dynamicList = this.hook.list.list.querySelector('[data-dynamic]');
this.destroyed = true;
if (this.listTemplate && dynamicList) { if (this.listTemplate && dynamicList) {
dynamicList.outerHTML = this.listTemplate; dynamicList.outerHTML = this.listTemplate;
} }
......
...@@ -28,6 +28,23 @@ require('./filtered_search_dropdown'); ...@@ -28,6 +28,23 @@ require('./filtered_search_dropdown');
const tag = selected.querySelector('.js-filter-tag').innerText.trim(); const tag = selected.querySelector('.js-filter-tag').innerText.trim();
if (tag.length) { 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(':', '')); gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', ''));
} }
this.dismissDropdown(); this.dismissDropdown();
...@@ -39,7 +56,7 @@ require('./filtered_search_dropdown'); ...@@ -39,7 +56,7 @@ require('./filtered_search_dropdown');
renderContent() { renderContent() {
const dropdownData = []; 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; const { icon, hint, tag } = dropdownMenu.dataset;
if (icon && hint && tag) { if (icon && hint && tag) {
dropdownData.push({ dropdownData.push({
......
...@@ -39,7 +39,12 @@ require('./filtered_search_dropdown'); ...@@ -39,7 +39,12 @@ require('./filtered_search_dropdown');
getSearchInput() { getSearchInput() {
const query = gl.DropdownUtils.getSearchInput(this.input); const query = gl.DropdownUtils.getSearchInput(this.input);
const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); 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 // Removes the first character if it is a quotation so that we can search
// with multiple words // with multiple words
......
...@@ -22,12 +22,17 @@ ...@@ -22,12 +22,17 @@
static filterWithSymbol(filterSymbol, input, item) { static filterWithSymbol(filterSymbol, input, item) {
const updatedItem = item; const updatedItem = item;
const query = gl.DropdownUtils.getSearchInput(input); const searchInput = gl.DropdownUtils.getSearchInput(input);
const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(query);
if (lastToken !== searchToken) {
const title = updatedItem.title.toLowerCase(); const title = updatedItem.title.toLowerCase();
let value = lastToken.value.toLowerCase(); let value = searchInput.toLowerCase();
let symbol = '';
// Remove the symbol for filter
if (value[0] === filterSymbol) {
symbol = value[0];
value = value.slice(1);
}
// Removes the first character if it is a quotation so that we can search // Removes the first character if it is a quotation so that we can search
// with multiple words // with multiple words
...@@ -36,24 +41,21 @@ ...@@ -36,24 +41,21 @@
} }
// Eg. filterSymbol = ~ for labels // Eg. filterSymbol = ~ for labels
const matchWithoutSymbol = lastToken.symbol === filterSymbol && title.indexOf(value) !== -1; const matchWithoutSymbol = symbol === filterSymbol && title.indexOf(value) !== -1;
const match = title.indexOf(`${lastToken.symbol}${value}`) !== -1; const match = title.indexOf(`${symbol}${value}`) !== -1;
updatedItem.droplab_hidden = !match && !matchWithoutSymbol; updatedItem.droplab_hidden = !match && !matchWithoutSymbol;
} else {
updatedItem.droplab_hidden = false;
}
return updatedItem; return updatedItem;
} }
static filterHint(input, item) { static filterHint(input, item) {
const updatedItem = item; const updatedItem = item;
const query = gl.DropdownUtils.getSearchInput(input); const searchInput = gl.DropdownUtils.getSearchInput(input);
let { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); let { lastToken } = gl.FilteredSearchTokenizer.processTokens(searchInput);
lastToken = lastToken.key || lastToken || ''; lastToken = lastToken.key || lastToken || '';
if (!lastToken || query.split('').last() === ' ') { if (!lastToken || searchInput.split('').last() === ' ') {
updatedItem.droplab_hidden = false; updatedItem.droplab_hidden = false;
} else if (lastToken) { } else if (lastToken) {
const split = lastToken.split(':'); const split = lastToken.split(':');
...@@ -70,13 +72,40 @@ ...@@ -70,13 +72,40 @@
const dataValue = selected.getAttribute('data-value'); const dataValue = selected.getAttribute('data-value');
if (dataValue) { if (dataValue) {
gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue); gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue, true);
} }
// Return boolean based on whether it was set // Return boolean based on whether it was set
return dataValue !== null; 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) { static getSearchInput(filteredSearchInput) {
const inputValue = filteredSearchInput.value; const inputValue = filteredSearchInput.value;
const { right } = gl.DropdownUtils.getInputSelectionPosition(filteredSearchInput); const { right } = gl.DropdownUtils.getInputSelectionPosition(filteredSearchInput);
......
...@@ -7,3 +7,4 @@ require('./filtered_search_dropdown'); ...@@ -7,3 +7,4 @@ require('./filtered_search_dropdown');
require('./filtered_search_manager'); require('./filtered_search_manager');
require('./filtered_search_token_keys'); require('./filtered_search_token_keys');
require('./filtered_search_tokenizer'); require('./filtered_search_tokenizer');
require('./filtered_search_visual_tokens');
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
if (!dataValueSet) { if (!dataValueSet) {
const value = getValueFunction(selected); const value = getValueFunction(selected);
gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value); gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value, true);
} }
this.dismissDropdown(); this.dismissDropdown();
......
...@@ -58,35 +58,15 @@ ...@@ -58,35 +58,15 @@
}; };
} }
static addWordToInput(tokenName, tokenValue = '') { static addWordToInput(tokenName, tokenValue = '', clicked = false) {
const input = document.querySelector('.filtered-search'); const input = document.querySelector('.filtered-search');
const inputValue = input.value;
const word = `${tokenName}:${tokenValue}`;
// Get the string to replace gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue);
let newCaretPosition = input.selectionStart; input.value = '';
const { left, right } = gl.DropdownUtils.getInputSelectionPosition(input);
input.value = `${inputValue.substr(0, left)}${word}${inputValue.substr(right)}`; if (clicked) {
gl.FilteredSearchVisualTokens.moveInputToTheRight();
// 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;
}
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() { updateCurrentDropdownOffset() {
...@@ -94,19 +74,14 @@ ...@@ -94,19 +74,14 @@
} }
updateDropdownOffset(key) { updateDropdownOffset(key) {
if (!this.font) { // Always align dropdown with the input field
this.font = window.getComputedStyle(this.filteredSearchInput).font; let offset = this.filteredSearchInput.getBoundingClientRect().left - document.querySelector('.scroll-container').getBoundingClientRect().left;
}
const input = this.filteredSearchInput;
const inputText = input.value.slice(0, input.selectionStart);
const filterIconPadding = 27;
let offset = gl.text.getTextWidth(inputText, this.font) + filterIconPadding;
const currentDropdownWidth = this.mapping[key].element.clientWidth === 0 ? 200 : const maxInputWidth = 240;
this.mapping[key].element.clientWidth; const currentDropdownWidth = this.mapping[key].element.clientWidth || maxInputWidth;
const offsetMaxWidth = this.filteredSearchInput.clientWidth - currentDropdownWidth;
// Make sure offset never exceeds the input container
const offsetMaxWidth = document.querySelector('.scroll-container').clientWidth - currentDropdownWidth;
if (offsetMaxWidth < offset) { if (offsetMaxWidth < offset) {
offset = offsetMaxWidth; offset = offsetMaxWidth;
} }
...@@ -164,8 +139,8 @@ ...@@ -164,8 +139,8 @@
} }
setDropdown() { setDropdown() {
const { lastToken, searchToken } = this.tokenizer const query = gl.DropdownUtils.getSearchQuery();
.processTokens(gl.DropdownUtils.getSearchInput(this.filteredSearchInput)); const { lastToken, searchToken } = this.tokenizer.processTokens(query);
if (this.currentDropdown) { if (this.currentDropdown) {
this.updateCurrentDropdownOffset(); this.updateCurrentDropdownOffset();
......
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;
...@@ -64,6 +64,89 @@ ...@@ -64,6 +64,89 @@
-webkit-flex-direction: column; -webkit-flex-direction: column;
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 { .filtered-search-input-container {
...@@ -71,6 +154,9 @@ ...@@ -71,6 +154,9 @@
display: flex; display: flex;
position: relative; position: relative;
width: 100%; width: 100%;
border: 1px solid $border-color;
background-color: $white-light;
max-width: 87%;
@media (max-width: $screen-xs-min) { @media (max-width: $screen-xs-min) {
-webkit-flex: 1 1 100%; -webkit-flex: 1 1 100%;
...@@ -87,12 +173,22 @@ ...@@ -87,12 +173,22 @@
} }
.form-control { .form-control {
padding-left: 25px; position: relative;
min-width: 200px;
padding-left: 0;
padding-right: 25px; padding-right: 25px;
border-color: transparent;
&:focus ~ .fa-filter { &:focus ~ .fa-filter {
color: $common-gray-dark; color: $common-gray-dark;
} }
&:focus,
&:hover {
outline: none;
border-color: transparent;
box-shadow: none;
}
} }
.fa-filter { .fa-filter {
...@@ -109,12 +205,13 @@ ...@@ -109,12 +205,13 @@
.clear-search { .clear-search {
width: 35px; width: 35px;
background-color: transparent; background-color: $white-light;
border: none; border: none;
position: absolute; position: absolute;
right: 0; right: 0;
height: 100%; height: 100%;
outline: none; outline: none;
z-index: 1;
&:hover .fa-times { &:hover .fa-times {
color: $common-gray-dark; color: $common-gray-dark;
......
...@@ -540,3 +540,12 @@ Pipeline Graph ...@@ -540,3 +540,12 @@ Pipeline Graph
$stage-hover-bg: #eaf3fc; $stage-hover-bg: #eaf3fc;
$stage-hover-border: #d1e7fc; $stage-hover-border: #d1e7fc;
$action-icon-color: #d6d6d6; $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;
...@@ -11,6 +11,9 @@ ...@@ -11,6 +11,9 @@
class: "check_all_issues left" class: "check_all_issues left"
.issues-other-filters.filtered-search-container .issues-other-filters.filtered-search-container
.filtered-search-input-container .filtered-search-input-container
.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) } %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') = icon('filter')
%button.clear-search.hidden{ type: 'button' } %button.clear-search.hidden{ type: 'button' }
......
require 'rails_helper' require 'rails_helper'
describe 'Dropdown assignee', :feature, :js do describe 'Dropdown assignee', :feature, :js do
include FilteredSearchHelpers
include WaitForAjax
let!(:project) { create(:empty_project) } let!(:project) { create(:empty_project) }
let!(:user) { create(:user, name: 'administrator', username: 'root') } let!(:user) { create(:user, name: 'administrator', username: 'root') }
let!(:user_john) { create(:user, name: 'John', username: 'th0mas') } let!(:user_john) { create(:user, name: 'John', username: 'th0mas') }
...@@ -133,7 +136,8 @@ describe 'Dropdown assignee', :feature, :js do ...@@ -133,7 +136,8 @@ describe 'Dropdown assignee', :feature, :js do
click_assignee(user_jacob.name) click_assignee(user_jacob.name)
expect(page).to have_css(js_dropdown_assignee, visible: false) 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 end
it 'fills in the assignee username when the assignee has been filtered' do it 'fills in the assignee username when the assignee has been filtered' do
...@@ -141,14 +145,16 @@ describe 'Dropdown assignee', :feature, :js do ...@@ -141,14 +145,16 @@ describe 'Dropdown assignee', :feature, :js do
click_assignee(user.name) click_assignee(user.name)
expect(page).to have_css(js_dropdown_assignee, visible: false) 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 end
it 'selects `no assignee`' do it 'selects `no assignee`' do
find('#js-dropdown-assignee .filter-dropdown-item', text: 'No Assignee').click find('#js-dropdown-assignee .filter-dropdown-item', text: 'No Assignee').click
expect(page).to have_css(js_dropdown_assignee, visible: false) 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
end end
......
require 'rails_helper' require 'rails_helper'
describe 'Dropdown author', js: true, feature: true do describe 'Dropdown author', js: true, feature: true do
include FilteredSearchHelpers
include WaitForAjax include WaitForAjax
let!(:project) { create(:empty_project) } let!(:project) { create(:empty_project) }
...@@ -121,14 +122,16 @@ describe 'Dropdown author', js: true, feature: true do ...@@ -121,14 +122,16 @@ describe 'Dropdown author', js: true, feature: true do
click_author(user_jacob.name) click_author(user_jacob.name)
expect(page).to have_css(js_dropdown_author, visible: false) 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 end
it 'fills in the author username when the author has been filtered' do it 'fills in the author username when the author has been filtered' do
click_author(user.name) click_author(user.name)
expect(page).to have_css(js_dropdown_author, visible: false) 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
end end
......
require 'rails_helper' require 'rails_helper'
describe 'Dropdown hint', js: true, feature: true do describe 'Dropdown hint', js: true, feature: true do
include FilteredSearchHelpers
include WaitForAjax include WaitForAjax
let!(:project) { create(:empty_project) } let!(:project) { create(:empty_project) }
...@@ -66,7 +67,8 @@ describe 'Dropdown hint', js: true, feature: true do ...@@ -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_hint, visible: false)
expect(page).to have_css('#js-dropdown-author', visible: true) 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 end
it 'opens the assignee dropdown when you click on assignee' do it 'opens the assignee dropdown when you click on assignee' do
...@@ -74,7 +76,8 @@ describe 'Dropdown hint', js: true, feature: true 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_hint, visible: false)
expect(page).to have_css('#js-dropdown-assignee', visible: true) 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 end
it 'opens the milestone dropdown when you click on milestone' do it 'opens the milestone dropdown when you click on milestone' do
...@@ -82,7 +85,8 @@ describe 'Dropdown hint', js: true, feature: true 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_hint, visible: false)
expect(page).to have_css('#js-dropdown-milestone', visible: true) 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 end
it 'opens the label dropdown when you click on label' do it 'opens the label dropdown when you click on label' do
...@@ -90,7 +94,8 @@ describe 'Dropdown hint', js: true, feature: true 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_hint, visible: false)
expect(page).to have_css('#js-dropdown-label', visible: true) 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
end end
...@@ -101,7 +106,8 @@ describe 'Dropdown hint', js: true, feature: true do ...@@ -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_hint, visible: false)
expect(page).to have_css('#js-dropdown-author', visible: true) 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 end
it 'opens the assignee dropdown when you click on assignee' do it 'opens the assignee dropdown when you click on assignee' do
...@@ -110,7 +116,8 @@ describe 'Dropdown hint', js: true, feature: true 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_hint, visible: false)
expect(page).to have_css('#js-dropdown-assignee', visible: true) 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 end
it 'opens the milestone dropdown when you click on milestone' do it 'opens the milestone dropdown when you click on milestone' do
...@@ -119,7 +126,8 @@ describe 'Dropdown hint', js: true, feature: true 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_hint, visible: false)
expect(page).to have_css('#js-dropdown-milestone', visible: true) 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 end
it 'opens the label dropdown when you click on label' do it 'opens the label dropdown when you click on label' do
...@@ -128,7 +136,46 @@ describe 'Dropdown hint', js: true, feature: true 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_hint, visible: false)
expect(page).to have_css('#js-dropdown-label', visible: true) 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 end
end end
...@@ -51,7 +51,8 @@ describe 'Dropdown label', js: true, feature: true do ...@@ -51,7 +51,8 @@ describe 'Dropdown label', js: true, feature: true do
filtered_search.native.send_keys(:down, :down, :enter) 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
end end
...@@ -92,7 +93,7 @@ describe 'Dropdown label', js: true, feature: true do ...@@ -92,7 +93,7 @@ describe 'Dropdown label', js: true, feature: true do
end end
it 'filters by case-insensitive name with or without symbol' do 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: bug_label.title)).to be_visible
expect(filter_dropdown.find('.filter-dropdown-item', text: uppercase_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 ...@@ -101,7 +102,7 @@ describe 'Dropdown label', js: true, feature: true do
clear_search_field clear_search_field
init_label_search 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: bug_label.title)).to be_visible
expect(filter_dropdown.find('.filter-dropdown-item', text: uppercase_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 ...@@ -180,7 +181,8 @@ describe 'Dropdown label', js: true, feature: true do
click_label(bug_label.title) click_label(bug_label.title)
expect(page).not_to have_css(js_dropdown_label) 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 end
it 'fills in the label name when the label is partially filled' do 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 ...@@ -188,49 +190,56 @@ describe 'Dropdown label', js: true, feature: true do
click_label(bug_label.title) click_label(bug_label.title)
expect(page).not_to have_css(js_dropdown_label) 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 end
it 'fills in the label name that contains multiple words' do it 'fills in the label name that contains multiple words' do
click_label(two_words_label.title) click_label(two_words_label.title)
expect(page).not_to have_css(js_dropdown_label) 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 end
it 'fills in the label name that contains multiple words and is very long' do it 'fills in the label name that contains multiple words and is very long' do
click_label(long_label.title) click_label(long_label.title)
expect(page).not_to have_css(js_dropdown_label) 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 end
it 'fills in the label name that contains double quotes' do it 'fills in the label name that contains double quotes' do
click_label(wont_fix_label.title) click_label(wont_fix_label.title)
expect(page).not_to have_css(js_dropdown_label) 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 end
it 'fills in the label name with the correct capitalization' do it 'fills in the label name with the correct capitalization' do
click_label(uppercase_label.title) click_label(uppercase_label.title)
expect(page).not_to have_css(js_dropdown_label) 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 end
it 'fills in the label name with special characters' do it 'fills in the label name with special characters' do
click_label(special_label.title) click_label(special_label.title)
expect(page).not_to have_css(js_dropdown_label) 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 end
it 'selects `no label`' do it 'selects `no label`' do
find("#{js_dropdown_label} .filter-dropdown-item", text: 'No Label').click find("#{js_dropdown_label} .filter-dropdown-item", text: 'No Label').click
expect(page).not_to have_css(js_dropdown_label) 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
end end
......
require 'rails_helper' require 'rails_helper'
describe 'Dropdown milestone', js: true, feature: true do describe 'Dropdown milestone', js: true, feature: true do
include FilteredSearchHelpers
include WaitForAjax include WaitForAjax
let!(:project) { create(:empty_project) } let!(:project) { create(:empty_project) }
...@@ -127,7 +128,8 @@ describe 'Dropdown milestone', js: true, feature: true do ...@@ -127,7 +128,8 @@ describe 'Dropdown milestone', js: true, feature: true do
click_milestone(milestone.title) click_milestone(milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false) 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 end
it 'fills in the milestone name when the milestone is partially filled' do 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 ...@@ -135,56 +137,64 @@ describe 'Dropdown milestone', js: true, feature: true do
click_milestone(milestone.title) click_milestone(milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false) 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 end
it 'fills in the milestone name that contains multiple words' do it 'fills in the milestone name that contains multiple words' do
click_milestone(two_words_milestone.title) click_milestone(two_words_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false) 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 end
it 'fills in the milestone name that contains multiple words and is very long' do it 'fills in the milestone name that contains multiple words and is very long' do
click_milestone(long_milestone.title) click_milestone(long_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false) 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 end
it 'fills in the milestone name that contains double quotes' do it 'fills in the milestone name that contains double quotes' do
click_milestone(wont_fix_milestone.title) click_milestone(wont_fix_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false) 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 end
it 'fills in the milestone name with the correct capitalization' do it 'fills in the milestone name with the correct capitalization' do
click_milestone(uppercase_milestone.title) click_milestone(uppercase_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false) 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 end
it 'fills in the milestone name with special characters' do it 'fills in the milestone name with special characters' do
click_milestone(special_milestone.title) click_milestone(special_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false) 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 end
it 'selects `no milestone`' do it 'selects `no milestone`' do
click_static_milestone('No Milestone') click_static_milestone('No Milestone')
expect(page).to have_css(js_dropdown_milestone, visible: false) 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 end
it 'selects `upcoming milestone`' do it 'selects `upcoming milestone`' do
click_static_milestone('Upcoming') click_static_milestone('Upcoming')
expect(page).to have_css(js_dropdown_milestone, visible: false) 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
end end
......
require 'rails_helper' require 'rails_helper'
describe 'Search bar', js: true, feature: true do describe 'Search bar', js: true, feature: true do
include FilteredSearchHelpers
include WaitForAjax include WaitForAjax
let!(:project) { create(:empty_project) } let!(:project) { create(:empty_project) }
...@@ -32,7 +33,8 @@ describe 'Search bar', js: true, feature: true do ...@@ -32,7 +33,8 @@ describe 'Search bar', js: true, feature: true do
it 'selects item' do it 'selects item' do
filtered_search.native.send_keys(:down, :down, :enter) 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
end end
......
This diff is collapsed.
...@@ -70,7 +70,7 @@ feature 'Issue filtering by Labels', feature: true, js: true do ...@@ -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 context 'filter by label enhancement and bug in issues list' do
before do before do
input_filtered_search('label:~bug label:~enhancement') input_filtered_search('label:~bug label:~enhancement ')
end end
it 'applies the filters' do it 'applies the filters' do
......
...@@ -25,6 +25,9 @@ feature 'Merge Request filtering by Milestone', feature: true do ...@@ -25,6 +25,9 @@ feature 'Merge Request filtering by Milestone', feature: true do
visit_merge_requests(project) visit_merge_requests(project)
input_filtered_search('milestone:none') 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_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_css('.merge-request', count: 1) expect(page).to have_css('.merge-request', count: 1)
end end
......
...@@ -24,6 +24,11 @@ describe 'Filter merge requests', feature: true do ...@@ -24,6 +24,11 @@ describe 'Filter merge requests', feature: true do
describe 'for assignee from mr#index' do describe 'for assignee from mr#index' do
let(:search_query) { "assignee:@#{user.username}" } 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 before do
input_filtered_search(search_query) input_filtered_search(search_query)
...@@ -32,25 +37,30 @@ describe 'Filter merge requests', feature: true do ...@@ -32,25 +37,30 @@ describe 'Filter merge requests', feature: true do
context 'assignee', js: true do context 'assignee', js: true do
it 'updates to current user' do it 'updates to current user' do
expect_filtered_search_input(search_query) expect_assignee_visual_tokens()
end end
it 'does not change when closed link is clicked' do it 'does not change when closed link is clicked' do
find('.issues-state-filters a', text: "Closed").click find('.issues-state-filters a', text: "Closed").click
expect_filtered_search_input(search_query) expect_assignee_visual_tokens()
end end
it 'does not change when all link is clicked' do it 'does not change when all link is clicked' do
find('.issues-state-filters a', text: "All").click find('.issues-state-filters a', text: "All").click
expect_filtered_search_input(search_query) expect_assignee_visual_tokens()
end end
end end
end end
describe 'for milestone from mr#index' do 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 before do
input_filtered_search(search_query) input_filtered_search(search_query)
...@@ -60,19 +70,19 @@ describe 'Filter merge requests', feature: true do ...@@ -60,19 +70,19 @@ describe 'Filter merge requests', feature: true do
context 'milestone', js: true do context 'milestone', js: true do
it 'updates to current milestone' do it 'updates to current milestone' do
expect_filtered_search_input(search_query) expect_milestone_visual_tokens()
end end
it 'does not change when closed link is clicked' do it 'does not change when closed link is clicked' do
find('.issues-state-filters a', text: "Closed").click find('.issues-state-filters a', text: "Closed").click
expect_filtered_search_input(search_query) expect_milestone_visual_tokens()
end end
it 'does not change when all link is clicked' do it 'does not change when all link is clicked' do
find('.issues-state-filters a', text: "All").click find('.issues-state-filters a', text: "All").click
expect_filtered_search_input(search_query) expect_milestone_visual_tokens()
end end
end end
end end
...@@ -82,35 +92,44 @@ describe 'Filter merge requests', feature: true do ...@@ -82,35 +92,44 @@ describe 'Filter merge requests', feature: true do
input_filtered_search('label:none') input_filtered_search('label:none')
expect_mr_list_count(1) expect_mr_list_count(1)
expect_filtered_search_input('label:none') expect_tokens([{ name: 'label', value: 'none' }])
expect_filtered_search_input_empty
end end
it 'filters by a label' do it 'filters by a label' do
input_filtered_search("label:~#{label.title}") input_filtered_search("label:~#{label.title}")
expect_mr_list_count(0) 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 end
it "filters by `won't fix` and another label" do it "filters by `won't fix` and another label" do
input_filtered_search("label:~\"#{wontfix.title}\" label:~#{label.title}") input_filtered_search("label:~\"#{wontfix.title}\" label:~#{label.title}")
expect_mr_list_count(0) 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
it "filters by `won't fix` label followed by another label after page load" do it "filters by `won't fix` label followed by another label after page load" do
input_filtered_search("label:~\"#{wontfix.title}\"") input_filtered_search("label:~\"#{wontfix.title}\"")
expect_mr_list_count(0) expect_mr_list_count(0)
expect_filtered_search_input("label:~\"#{wontfix.title}\"") expect_tokens([{ name: 'label', value: "~\"#{wontfix.title}\"" }])
expect_filtered_search_input_empty
input_filtered_search_keys(" label:~#{label.title}")
expect_filtered_search_input("label:~\"#{wontfix.title}\" label:~#{label.title}") input_filtered_search_keys("label:~#{label.title}")
expect_mr_list_count(0) 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
end end
...@@ -121,9 +140,10 @@ describe 'Filter merge requests', feature: true do ...@@ -121,9 +140,10 @@ describe 'Filter merge requests', feature: true do
input_filtered_search("assignee:@#{user.username}") input_filtered_search("assignee:@#{user.username}")
expect_mr_list_count(1) 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) expect_mr_list_count(1)
...@@ -131,20 +151,28 @@ describe 'Filter merge requests', feature: true do ...@@ -131,20 +151,28 @@ describe 'Filter merge requests', feature: true do
end end
context 'assignee and label', js: true do 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 it 'updates to current assignee and label' do
expect_filtered_search_input(search_query) expect_assignee_label_visual_tokens()
end end
it 'does not change when closed link is clicked' do it 'does not change when closed link is clicked' do
find('.issues-state-filters a', text: "Closed").click find('.issues-state-filters a', text: "Closed").click
expect_filtered_search_input(search_query) expect_assignee_label_visual_tokens()
end end
it 'does not change when all link is clicked' do it 'does not change when all link is clicked' do
find('.issues-state-filters a', text: "All").click find('.issues-state-filters a', text: "All").click
expect_filtered_search_input(search_query) expect_assignee_label_visual_tokens()
end end
end end
end end
...@@ -195,6 +223,8 @@ describe 'Filter merge requests', feature: true do ...@@ -195,6 +223,8 @@ describe 'Filter merge requests', feature: true do
input_filtered_search_keys(' label:~bug') input_filtered_search_keys(' label:~bug')
expect_mr_list_count(1) expect_mr_list_count(1)
expect_tokens([{ name: 'label', value: '~bug' }])
expect_filtered_search_input('Bug')
end end
it 'filters by text and milestone' do it 'filters by text and milestone' do
...@@ -206,6 +236,8 @@ describe 'Filter merge requests', feature: true do ...@@ -206,6 +236,8 @@ describe 'Filter merge requests', feature: true do
input_filtered_search_keys(' milestone:%8') input_filtered_search_keys(' milestone:%8')
expect_mr_list_count(1) expect_mr_list_count(1)
expect_tokens([{ name: 'milestone', value: '%8' }])
expect_filtered_search_input('Bug')
end end
it 'filters by text and assignee' do it 'filters by text and assignee' do
...@@ -217,6 +249,8 @@ describe 'Filter merge requests', feature: true do ...@@ -217,6 +249,8 @@ describe 'Filter merge requests', feature: true do
input_filtered_search_keys(" assignee:@#{user.username}") input_filtered_search_keys(" assignee:@#{user.username}")
expect_mr_list_count(1) expect_mr_list_count(1)
expect_tokens([{ name: 'assignee', value: "@#{user.username}" }])
expect_filtered_search_input('Bug')
end end
it 'filters by text and author' do it 'filters by text and author' do
...@@ -228,6 +262,8 @@ describe 'Filter merge requests', feature: true do ...@@ -228,6 +262,8 @@ describe 'Filter merge requests', feature: true do
input_filtered_search_keys(" author:@#{user.username}") input_filtered_search_keys(" author:@#{user.username}")
expect_mr_list_count(1) expect_mr_list_count(1)
expect_tokens([{ name: 'author', value: "@#{user.username}" }])
expect_filtered_search_input('Bug')
end end
end end
end end
...@@ -266,7 +302,8 @@ describe 'Filter merge requests', feature: true do ...@@ -266,7 +302,8 @@ describe 'Filter merge requests', feature: true do
it 'filter by current user' do it 'filter by current user' do
visit namespace_project_merge_requests_path(project.namespace, project, assignee_id: user.id) 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 end
it 'filter by new user' do it 'filter by new user' do
...@@ -275,7 +312,8 @@ describe 'Filter merge requests', feature: true 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) 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
end end
...@@ -283,7 +321,8 @@ describe 'Filter merge requests', feature: true do ...@@ -283,7 +321,8 @@ describe 'Filter merge requests', feature: true do
it 'filter by current user' do it 'filter by current user' do
visit namespace_project_merge_requests_path(project.namespace, project, author_id: user.id) 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 end
it 'filter by new user' do it 'filter by new user' do
...@@ -292,7 +331,8 @@ describe 'Filter merge requests', feature: true 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) 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 end
end end
require 'rails_helper' 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 FilteredSearchHelpers
include MergeRequestHelpers include MergeRequestHelpers
include WaitForAjax include WaitForAjax
...@@ -24,67 +24,93 @@ feature 'Issues filter reset button', feature: true, js: true do ...@@ -24,67 +24,93 @@ feature 'Issues filter reset button', feature: true, js: true do
context 'when a milestone filter has been applied' do context 'when a milestone filter has been applied' do
it 'resets the milestone filter' do it 'resets the milestone filter' do
visit_merge_requests(project, milestone_title: milestone.title) visit_merge_requests(project, milestone_title: milestone.title)
expect(page).to have_css(merge_request_css, count: 1) expect(page).to have_css(merge_request_css, count: 1)
expect(get_filtered_search_placeholder).to eq('')
reset_filters reset_filters
expect(page).to have_css(merge_request_css, count: 2) expect(page).to have_css(merge_request_css, count: 2)
expect(get_filtered_search_placeholder).to eq(default_placeholder)
end end
end end
context 'when a label filter has been applied' do context 'when a label filter has been applied' do
it 'resets the label filter' do it 'resets the label filter' do
visit_merge_requests(project, label_name: bug.name) visit_merge_requests(project, label_name: bug.name)
expect(page).to have_css(merge_request_css, count: 1) expect(page).to have_css(merge_request_css, count: 1)
expect(get_filtered_search_placeholder).to eq('')
reset_filters reset_filters
expect(page).to have_css(merge_request_css, count: 2) expect(page).to have_css(merge_request_css, count: 2)
expect(get_filtered_search_placeholder).to eq(default_placeholder)
end end
end end
context 'when a text search has been conducted' do context 'when a text search has been conducted' do
it 'resets the text search filter' do it 'resets the text search filter' do
visit_merge_requests(project, search: 'Bug') visit_merge_requests(project, search: 'Bug')
expect(page).to have_css(merge_request_css, count: 1) expect(page).to have_css(merge_request_css, count: 1)
expect(get_filtered_search_placeholder).to eq('')
reset_filters reset_filters
expect(page).to have_css(merge_request_css, count: 2) expect(page).to have_css(merge_request_css, count: 2)
expect(get_filtered_search_placeholder).to eq(default_placeholder)
end end
end end
context 'when author filter has been applied' do context 'when author filter has been applied' do
it 'resets the author filter' do it 'resets the author filter' do
visit_merge_requests(project, author_username: user.username) visit_merge_requests(project, author_username: user.username)
expect(page).to have_css(merge_request_css, count: 1) expect(page).to have_css(merge_request_css, count: 1)
expect(get_filtered_search_placeholder).to eq('')
reset_filters reset_filters
expect(page).to have_css(merge_request_css, count: 2) expect(page).to have_css(merge_request_css, count: 2)
expect(get_filtered_search_placeholder).to eq(default_placeholder)
end end
end end
context 'when assignee filter has been applied' do context 'when assignee filter has been applied' do
it 'resets the assignee filter' do it 'resets the assignee filter' do
visit_merge_requests(project, assignee_username: user.username) visit_merge_requests(project, assignee_username: user.username)
expect(page).to have_css(merge_request_css, count: 1) expect(page).to have_css(merge_request_css, count: 1)
expect(get_filtered_search_placeholder).to eq('')
reset_filters reset_filters
expect(page).to have_css(merge_request_css, count: 2) expect(page).to have_css(merge_request_css, count: 2)
expect(get_filtered_search_placeholder).to eq(default_placeholder)
end end
end end
context 'when all filters have been applied' do context 'when all filters have been applied' do
it 'resets all filters' do it 'clears 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') 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(page).to have_css(merge_request_css, count: 0)
expect(get_filtered_search_placeholder).to eq('')
reset_filters reset_filters
expect(page).to have_css(merge_request_css, count: 2) expect(page).to have_css(merge_request_css, count: 2)
expect(get_filtered_search_placeholder).to eq(default_placeholder)
end end
end end
context 'when no filters have been applied' do 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) visit_merge_requests(project)
expect(page).to have_css(merge_request_css, count: 2) 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) expect(page).not_to have_css(clear_search_css)
end end
end end
......
require 'spec_helper' require 'spec_helper'
describe "Search", feature: true do describe "Search", feature: true do
include FilteredSearchHelpers
include WaitForAjax include WaitForAjax
let(:user) { create(:user) } let(:user) { create(:user) }
...@@ -170,7 +171,8 @@ describe "Search", feature: true do ...@@ -170,7 +171,8 @@ describe "Search", feature: true do
sleep 2 sleep 2
expect(page).to have_selector('.filtered-search') 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 end
it 'takes user to her issues page when issues authored is clicked' do it 'takes user to her issues page when issues authored is clicked' do
...@@ -178,7 +180,8 @@ describe "Search", feature: true do ...@@ -178,7 +180,8 @@ describe "Search", feature: true do
sleep 2 sleep 2
expect(page).to have_selector('.filtered-search') 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 end
it 'takes user to her MR page when MR assigned is clicked' do it 'takes user to her MR page when MR assigned is clicked' do
...@@ -186,7 +189,8 @@ describe "Search", feature: true do ...@@ -186,7 +189,8 @@ describe "Search", feature: true do
sleep 2 sleep 2
expect(page).to have_selector('.merge-requests-holder') 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 end
it 'takes user to her MR page when MR authored is clicked' do it 'takes user to her MR page when MR authored is clicked' do
...@@ -194,7 +198,8 @@ describe "Search", feature: true do ...@@ -194,7 +198,8 @@ describe "Search", feature: true do
sleep 2 sleep 2
expect(page).to have_selector('.merge-requests-holder') 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
end end
......
...@@ -18,9 +18,7 @@ require('~/filtered_search/dropdown_user'); ...@@ -18,9 +18,7 @@ require('~/filtered_search/dropdown_user');
it('should not return the double quote found in value', () => { it('should not return the double quote found in value', () => {
spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({ spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({
lastToken: { lastToken: '"johnny appleseed',
value: '"johnny appleseed',
},
}); });
expect(dropdownUser.getSearchInput()).toBe('johnny appleseed'); expect(dropdownUser.getSearchInput()).toBe('johnny appleseed');
...@@ -28,9 +26,7 @@ require('~/filtered_search/dropdown_user'); ...@@ -28,9 +26,7 @@ require('~/filtered_search/dropdown_user');
it('should not return the single quote found in value', () => { it('should not return the single quote found in value', () => {
spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({ spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({
lastToken: { lastToken: '\'larry boy',
value: '\'larry boy',
},
}); });
expect(dropdownUser.getSearchInput()).toBe('larry boy'); expect(dropdownUser.getSearchInput()).toBe('larry boy');
......
...@@ -45,7 +45,7 @@ require('~/filtered_search/filtered_search_dropdown_manager'); ...@@ -45,7 +45,7 @@ require('~/filtered_search/filtered_search_dropdown_manager');
}); });
it('should filter without symbol', () => { it('should filter without symbol', () => {
input.value = ':roo'; input.value = 'roo';
const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item); const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item);
expect(updatedItem.droplab_hidden).toBe(false); expect(updatedItem.droplab_hidden).toBe(false);
...@@ -58,69 +58,62 @@ require('~/filtered_search/filtered_search_dropdown_manager'); ...@@ -58,69 +58,62 @@ require('~/filtered_search/filtered_search_dropdown_manager');
expect(updatedItem.droplab_hidden).toBe(false); 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', () => { describe('filters multiple word title', () => {
const multipleWordItem = { const multipleWordItem = {
title: 'Community Contributions', title: 'Community Contributions',
}; };
it('should filter with double quote', () => { it('should filter with double quote', () => {
input.value = 'label:"'; input.value = '"';
const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false); expect(updatedItem.droplab_hidden).toBe(false);
}); });
it('should filter with double quote and symbol', () => { it('should filter with double quote and symbol', () => {
input.value = 'label:~"'; input.value = '~"';
const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false); expect(updatedItem.droplab_hidden).toBe(false);
}); });
it('should filter with double quote and multiple words', () => { 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); const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false); expect(updatedItem.droplab_hidden).toBe(false);
}); });
it('should filter with double quote, symbol and multiple words', () => { 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); const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false); expect(updatedItem.droplab_hidden).toBe(false);
}); });
it('should filter with single quote', () => { it('should filter with single quote', () => {
input.value = 'label:\''; input.value = '\'';
const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false); expect(updatedItem.droplab_hidden).toBe(false);
}); });
it('should filter with single quote and symbol', () => { it('should filter with single quote and symbol', () => {
input.value = 'label:~\''; input.value = '~\'';
const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false); expect(updatedItem.droplab_hidden).toBe(false);
}); });
it('should filter with single quote and multiple words', () => { 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); const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false); expect(updatedItem.droplab_hidden).toBe(false);
}); });
it('should filter with single quote, symbol and multiple words', () => { 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); const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false); expect(updatedItem.droplab_hidden).toBe(false);
......
require('~/extensions/array'); require('~/extensions/array');
require('~/filtered_search/filtered_search_visual_tokens');
require('~/filtered_search/filtered_search_tokenizer'); require('~/filtered_search/filtered_search_tokenizer');
require('~/filtered_search/filtered_search_dropdown_manager'); require('~/filtered_search/filtered_search_dropdown_manager');
...@@ -14,24 +15,44 @@ require('~/filtered_search/filtered_search_dropdown_manager'); ...@@ -14,24 +15,44 @@ require('~/filtered_search/filtered_search_dropdown_manager');
} }
beforeEach(() => { beforeEach(() => {
const input = document.createElement('input'); setFixtures(`
input.classList.add('filtered-search'); <ul class="tokens-container">
document.body.appendChild(input); <li class="input-token">
}); <input class="filtered-search">
</li>
afterEach(() => { </ul>
document.querySelector('.filtered-search').outerHTML = ''; `);
}); });
describe('input has no existing value', () => { describe('input has no existing value', () => {
it('should add just tokenName', () => { it('should add just tokenName', () => {
gl.FilteredSearchDropdownManager.addWordToInput('milestone'); 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', () => { 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'); 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'); ...@@ -39,19 +60,40 @@ require('~/filtered_search/filtered_search_dropdown_manager');
it('should be able to just add tokenName', () => { it('should be able to just add tokenName', () => {
setInputValue('a'); setInputValue('a');
gl.FilteredSearchDropdownManager.addWordToInput('author'); 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', () => { it('should replace tokenValue', () => {
setInputValue('author:roo'); gl.FilteredSearchDropdownManager.addWordToInput('author');
gl.FilteredSearchDropdownManager.addWordToInput('author', '@root');
expect(getInputValue()).toBe('author:@root '); 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', () => { it('should add tokenValues containing spaces', () => {
setInputValue('label:~"test'); gl.FilteredSearchDropdownManager.addWordToInput('label');
setInputValue('"test ');
gl.FilteredSearchDropdownManager.addWordToInput('label', '~\'"test me"\''); 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('');
}); });
}); });
}); });
......
...@@ -4,64 +4,244 @@ require('~/filtered_search/filtered_search_token_keys'); ...@@ -4,64 +4,244 @@ require('~/filtered_search/filtered_search_token_keys');
require('~/filtered_search/filtered_search_tokenizer'); require('~/filtered_search/filtered_search_tokenizer');
require('~/filtered_search/filtered_search_dropdown_manager'); require('~/filtered_search/filtered_search_dropdown_manager');
require('~/filtered_search/filtered_search_manager'); require('~/filtered_search/filtered_search_manager');
const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper');
(() => { (() => {
describe('Filtered Search Manager', () => { describe('Filtered Search Manager', () => {
describe('search', () => { let input;
let manager; let manager;
const defaultParams = '?scope=all&utf8=✓&state=opened'; let tokensContainer;
const placeholder = 'Search or filter results...';
function getInput() { function dispatchBackspaceEvent(element, eventType) {
return document.querySelector('.filtered-search'); const backspaceKey = 8;
const event = new Event(eventType);
event.keyCode = backspaceKey;
element.dispatchEvent(event);
}
function dispatchDeleteEvent(element, eventType) {
const deleteKey = 46;
const event = new Event(eventType);
event.keyCode = deleteKey;
element.dispatchEvent(event);
} }
beforeEach(() => { beforeEach(() => {
setFixtures(` setFixtures(`
<input type='text' class='filtered-search' /> <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>
`); `);
spyOn(gl.FilteredSearchManager.prototype, 'bindEvents').and.callFake(() => {});
spyOn(gl.FilteredSearchManager.prototype, 'cleanup').and.callFake(() => {}); spyOn(gl.FilteredSearchManager.prototype, 'cleanup').and.callFake(() => {});
spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').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, 'setDropdown').and.callFake(() => {});
spyOn(gl.FilteredSearchDropdownManager.prototype, 'updateDropdownOffset').and.callFake(() => {});
spyOn(gl.utils, 'getParameterByName').and.returnValue(null); spyOn(gl.utils, 'getParameterByName').and.returnValue(null);
spyOn(gl.FilteredSearchVisualTokens, 'unselectTokens').and.callThrough();
input = document.querySelector('.filtered-search');
tokensContainer = document.querySelector('.tokens-container');
manager = new gl.FilteredSearchManager(); manager = new gl.FilteredSearchManager();
}); });
afterEach(() => { describe('search', () => {
getInput().outerHTML = ''; const defaultParams = '?scope=all&utf8=✓&state=opened';
});
it('should search with a single word', () => { it('should search with a single word', (done) => {
getInput().value = 'searchTerm'; input.value = 'searchTerm';
spyOn(gl.utils, 'visitUrl').and.callFake((url) => { spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&search=searchTerm`); expect(url).toEqual(`${defaultParams}&search=searchTerm`);
done();
}); });
manager.search(); manager.search();
}); });
it('should search with multiple words', () => { it('should search with multiple words', (done) => {
getInput().value = 'awesome search terms'; input.value = 'awesome search terms';
spyOn(gl.utils, 'visitUrl').and.callFake((url) => { spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`); expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`);
done();
}); });
manager.search(); manager.search();
}); });
it('should search with special characters', () => { it('should search with special characters', (done) => {
getInput().value = '~!@#$%^&*()_+{}:<>,.?/'; input.value = '~!@#$%^&*()_+{}:<>,.?/';
spyOn(gl.utils, 'visitUrl').and.callFake((url) => { 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`); expect(url).toEqual(`${defaultParams}&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`);
done();
}); });
manager.search(); 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();
});
});
}); });
})(); })();
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;
...@@ -3,16 +3,20 @@ module FilteredSearchHelpers ...@@ -3,16 +3,20 @@ module FilteredSearchHelpers
page.find('.filtered-search') page.find('.filtered-search')
end end
# Enables input to be set (similar to copy and paste)
def input_filtered_search(search_term, submit: true) 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 if submit
filtered_search.send_keys(:enter) filtered_search.send_keys(:enter)
end end
end end
# Enables input to be added character by character
def input_filtered_search_keys(search_term) 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) filtered_search.send_keys(:enter)
end end
...@@ -34,4 +38,32 @@ module FilteredSearchHelpers ...@@ -34,4 +38,32 @@ module FilteredSearchHelpers
# This ensures the dropdown is shown # This ensures the dropdown is shown
expect(find('#js-dropdown-label')).not_to have_css('.filter-dropdown-loading') expect(find('#js-dropdown-label')).not_to have_css('.filter-dropdown-loading')
end 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 end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment