Commit 51c19616 authored by Tim Zallmann's avatar Tim Zallmann

Merge branch 'winh-styled-people-search-bar' into 'master'

Style people in issuable search bar

Closes #30468

See merge request !11402
parents b1f86081 0583916d
...@@ -102,10 +102,13 @@ class DropdownUtils { ...@@ -102,10 +102,13 @@ class DropdownUtils {
if (token.classList.contains('js-visual-token')) { if (token.classList.contains('js-visual-token')) {
const name = token.querySelector('.name'); const name = token.querySelector('.name');
const value = token.querySelector('.value'); const value = token.querySelector('.value');
const valueContainer = token.querySelector('.value-container');
const symbol = value && value.dataset.symbol ? value.dataset.symbol : ''; const symbol = value && value.dataset.symbol ? value.dataset.symbol : '';
let valueText = ''; let valueText = '';
if (value && value.innerText) { if (valueContainer && valueContainer.dataset.originalValue) {
valueText = valueContainer.dataset.originalValue;
} else if (value && value.innerText) {
valueText = value.innerText; valueText = value.innerText;
} }
......
import AjaxCache from '~/lib/utils/ajax_cache'; import AjaxCache from '../lib/utils/ajax_cache';
import '~/flash'; /* global Flash */ import '../flash'; /* global Flash */
import FilteredSearchContainer from './container'; import FilteredSearchContainer from './container';
import UsersCache from '../lib/utils/users_cache';
class FilteredSearchVisualTokens { class FilteredSearchVisualTokens {
static getLastVisualTokenBeforeInput() { static getLastVisualTokenBeforeInput() {
...@@ -82,12 +83,42 @@ class FilteredSearchVisualTokens { ...@@ -82,12 +83,42 @@ class FilteredSearchVisualTokens {
.catch(() => new Flash('An error occurred while fetching label colors.')); .catch(() => new Flash('An error occurred while fetching label colors.'));
} }
static updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue) {
if (tokenValue === 'none') {
return Promise.resolve();
}
const username = tokenValue.replace(/^@/, '');
return UsersCache.retrieve(username)
.then((user) => {
if (!user) {
return;
}
/* eslint-disable no-param-reassign */
tokenValueContainer.dataset.originalValue = tokenValue;
tokenValueElement.innerHTML = `
<img class="avatar s20" src="${user.avatar_url}" alt="${user.name}'s avatar">
${user.name}
`;
/* eslint-enable no-param-reassign */
})
// ignore error and leave username in the search bar
.catch(() => { });
}
static renderVisualTokenValue(parentElement, tokenName, tokenValue) { static renderVisualTokenValue(parentElement, tokenName, tokenValue) {
const tokenValueContainer = parentElement.querySelector('.value-container'); const tokenValueContainer = parentElement.querySelector('.value-container');
tokenValueContainer.querySelector('.value').innerText = tokenValue; const tokenValueElement = tokenValueContainer.querySelector('.value');
tokenValueElement.innerText = tokenValue;
if (tokenName.toLowerCase() === 'label') { const tokenType = tokenName.toLowerCase();
if (tokenType === 'label') {
FilteredSearchVisualTokens.updateLabelTokenColor(tokenValueContainer, tokenValue); FilteredSearchVisualTokens.updateLabelTokenColor(tokenValueContainer, tokenValue);
} else if ((tokenType === 'author') || (tokenType === 'assignee')) {
FilteredSearchVisualTokens.updateUserTokenAppearance(
tokenValueContainer, tokenValueElement, tokenValue,
);
} }
} }
...@@ -153,6 +184,12 @@ class FilteredSearchVisualTokens { ...@@ -153,6 +184,12 @@ class FilteredSearchVisualTokens {
if (!lastVisualToken) return ''; if (!lastVisualToken) return '';
const valueContainer = lastVisualToken.querySelector('.value-container');
const originalValue = valueContainer && valueContainer.dataset.originalValue;
if (originalValue) {
return originalValue;
}
const value = lastVisualToken.querySelector('.value'); const value = lastVisualToken.querySelector('.value');
const name = lastVisualToken.querySelector('.name'); const name = lastVisualToken.querySelector('.name');
...@@ -205,17 +242,28 @@ class FilteredSearchVisualTokens { ...@@ -205,17 +242,28 @@ class FilteredSearchVisualTokens {
const inputLi = input.parentElement; const inputLi = input.parentElement;
tokenContainer.replaceChild(inputLi, token); tokenContainer.replaceChild(inputLi, token);
const name = token.querySelector('.name'); const nameElement = token.querySelector('.name');
const value = token.querySelector('.value'); let value;
if (token.classList.contains('filtered-search-token')) {
FilteredSearchVisualTokens.addFilterVisualToken(nameElement.innerText);
const valueContainerElement = token.querySelector('.value-container');
value = valueContainerElement.dataset.originalValue;
if (!value) {
const valueElement = valueContainerElement.querySelector('.value');
value = valueElement.innerText;
}
}
if (token.classList.contains('filtered-search-token') && value) {
FilteredSearchVisualTokens.addFilterVisualToken(name.innerText);
input.value = value.innerText;
} else {
// token is a search term // token is a search term
input.value = name.innerText; if (!value) {
value = nameElement.innerText;
} }
input.value = value;
// Opens dropdown // Opens dropdown
const inputEvent = new Event('input'); const inputEvent = new Event('input');
input.dispatchEvent(inputEvent); input.dispatchEvent(inputEvent);
......
...@@ -90,6 +90,7 @@ ...@@ -90,6 +90,7 @@
.filtered-search-term { .filtered-search-term {
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
flex-shrink: 0;
margin-top: 5px; margin-top: 5px;
margin-bottom: 5px; margin-bottom: 5px;
...@@ -239,7 +240,7 @@ ...@@ -239,7 +240,7 @@
width: 35px; width: 35px;
background-color: $white-light; background-color: $white-light;
border: none; border: none;
position: absolute; position: static;
right: 0; right: 0;
height: 100%; height: 100%;
outline: none; outline: none;
......
...@@ -26,8 +26,6 @@ ...@@ -26,8 +26,6 @@
%li.input-token %li.input-token
%input.form-control.filtered-search{ id: "filtered-search-#{type.to_s}", placeholder: 'Search or filter results...', data: { 'project-id' => @project.id, 'username-params' => @users.to_json(only: [:id, :username]), 'base-endpoint' => namespace_project_path(@project.namespace, @project) } } %input.form-control.filtered-search{ id: "filtered-search-#{type.to_s}", placeholder: 'Search or filter results...', data: { 'project-id' => @project.id, 'username-params' => @users.to_json(only: [:id, :username]), 'base-endpoint' => namespace_project_path(@project.namespace, @project) } }
= icon('filter') = icon('filter')
%button.clear-search.hidden{ type: 'button' }
= icon('times')
#js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown #js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
%ul{ data: { dropdown: true } } %ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { action: 'submit' } } %li.filter-dropdown-item{ data: { action: 'submit' } }
...@@ -95,6 +93,8 @@ ...@@ -95,6 +93,8 @@
%span.dropdown-label-box{ style: 'background: {{color}}' } %span.dropdown-label-box{ style: 'background: {{color}}' }
%span.label-title.js-data-value %span.label-title.js-data-value
{{title}} {{title}}
%button.clear-search.hidden{ type: 'button' }
= icon('times')
.filter-dropdown-container .filter-dropdown-container
- if type == :boards - if type == :boards
- if can?(current_user, :admin_list, @project) - if can?(current_user, :admin_list, @project)
......
---
title: Style people in issuable search bar
merge_request: 11402
author:
...@@ -89,7 +89,7 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do ...@@ -89,7 +89,7 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
page.within('.add-issues-modal') do page.within('.add-issues-modal') do
wait_for_requests wait_for_requests
expect(page).to have_selector('.js-visual-token', text: user2.username) expect(page).to have_selector('.js-visual-token', text: user2.name)
expect(page).to have_selector('.card', count: 1) expect(page).to have_selector('.card', count: 1)
end end
end end
...@@ -125,7 +125,7 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do ...@@ -125,7 +125,7 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
page.within('.add-issues-modal') do page.within('.add-issues-modal') do
wait_for_requests wait_for_requests
expect(page).to have_selector('.js-visual-token', text: user2.username) expect(page).to have_selector('.js-visual-token', text: user2.name)
expect(page).to have_selector('.card', count: 1) expect(page).to have_selector('.card', count: 1)
end end
end end
......
...@@ -6,7 +6,7 @@ describe 'Filter issues', js: true, feature: true do ...@@ -6,7 +6,7 @@ describe 'Filter issues', js: true, feature: true do
let!(:group) { create(:group) } let!(:group) { create(:group) }
let!(:project) { create(:project, group: group) } let!(:project) { create(:project, group: group) }
let!(:user) { create(:user, username: 'joe') } let!(:user) { create(:user, username: 'joe', name: 'Joe') }
let!(:user2) { create(:user, username: 'jane') } let!(:user2) { create(:user, username: 'jane') }
let!(:label) { create(:label, project: project) } let!(:label) { create(:label, project: project) }
let!(:wontfix) { create(:label, project: project, title: "Won't fix") } let!(:wontfix) { create(:label, project: project, title: "Won't fix") }
......
...@@ -2,6 +2,7 @@ require 'rails_helper' ...@@ -2,6 +2,7 @@ require 'rails_helper'
describe 'Visual tokens', js: true, feature: true do describe 'Visual tokens', js: true, feature: true do
include FilteredSearchHelpers include FilteredSearchHelpers
include WaitForRequests
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') }
...@@ -70,7 +71,8 @@ describe 'Visual tokens', js: true, feature: true do ...@@ -70,7 +71,8 @@ describe 'Visual tokens', js: true, feature: true do
end end
it 'changes value in visual token' do it 'changes value in visual token' do
expect(first('.tokens-container .filtered-search-token .value').text).to eq("@#{user_rock.username}") wait_for_requests
expect(first('.tokens-container .filtered-search-token .value').text).to eq("#{user_rock.name}")
end end
it 'moves input to the right' do it 'moves input to the right' do
......
...@@ -2,8 +2,12 @@ import '~/extensions/array'; ...@@ -2,8 +2,12 @@ import '~/extensions/array';
import '~/filtered_search/dropdown_utils'; import '~/filtered_search/dropdown_utils';
import '~/filtered_search/filtered_search_tokenizer'; import '~/filtered_search/filtered_search_tokenizer';
import '~/filtered_search/filtered_search_dropdown_manager'; import '~/filtered_search/filtered_search_dropdown_manager';
import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper';
describe('Dropdown Utils', () => { describe('Dropdown Utils', () => {
const issueListFixture = 'issues/issue_list.html.raw';
preloadFixtures(issueListFixture);
describe('getEscapedText', () => { describe('getEscapedText', () => {
it('should return same word when it has no space', () => { it('should return same word when it has no space', () => {
const escaped = gl.DropdownUtils.getEscapedText('textWithoutSpace'); const escaped = gl.DropdownUtils.getEscapedText('textWithoutSpace');
...@@ -314,4 +318,29 @@ describe('Dropdown Utils', () => { ...@@ -314,4 +318,29 @@ describe('Dropdown Utils', () => {
}); });
}); });
}); });
describe('getSearchQuery', () => {
let authorToken;
beforeEach(() => {
loadFixtures(issueListFixture);
authorToken = FilteredSearchSpecHelper.createFilterVisualToken('author', '@user');
const searchTermToken = FilteredSearchSpecHelper.createSearchVisualToken('search term');
const tokensContainer = document.querySelector('.tokens-container');
tokensContainer.appendChild(searchTermToken);
tokensContainer.appendChild(authorToken);
});
it('uses original value if present', () => {
const originalValue = 'original dance';
const valueContainer = authorToken.querySelector('.value-container');
valueContainer.dataset.originalValue = originalValue;
const searchQuery = gl.DropdownUtils.getSearchQuery();
expect(searchQuery).toBe(' search term author:original dance');
});
});
}); });
import AjaxCache from '~/lib/utils/ajax_cache'; import AjaxCache from '~/lib/utils/ajax_cache';
import UsersCache from '~/lib/utils/users_cache';
import '~/filtered_search/filtered_search_visual_tokens'; import '~/filtered_search/filtered_search_visual_tokens';
import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper'; import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper';
describe('Filtered Search Visual Tokens', () => { describe('Filtered Search Visual Tokens', () => {
const subject = gl.FilteredSearchVisualTokens;
const findElements = (tokenElement) => {
const tokenNameElement = tokenElement.querySelector('.name');
const tokenValueContainer = tokenElement.querySelector('.value-container');
const tokenValueElement = tokenValueContainer.querySelector('.value');
return { tokenNameElement, tokenValueContainer, tokenValueElement };
};
let tokensContainer; let tokensContainer;
let authorToken;
let bugLabelToken;
beforeEach(() => { beforeEach(() => {
setFixtures(` setFixtures(`
...@@ -13,12 +25,15 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -13,12 +25,15 @@ describe('Filtered Search Visual Tokens', () => {
</ul> </ul>
`); `);
tokensContainer = document.querySelector('.tokens-container'); tokensContainer = document.querySelector('.tokens-container');
authorToken = FilteredSearchSpecHelper.createFilterVisualToken('author', '@user');
bugLabelToken = FilteredSearchSpecHelper.createFilterVisualToken('label', '~bug');
}); });
describe('getLastVisualTokenBeforeInput', () => { describe('getLastVisualTokenBeforeInput', () => {
it('returns when there are no visual tokens', () => { it('returns when there are no visual tokens', () => {
const { lastVisualToken, isLastVisualTokenValid } const { lastVisualToken, isLastVisualTokenValid }
= gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); = subject.getLastVisualTokenBeforeInput();
expect(lastVisualToken).toEqual(null); expect(lastVisualToken).toEqual(null);
expect(isLastVisualTokenValid).toEqual(true); expect(isLastVisualTokenValid).toEqual(true);
...@@ -27,11 +42,11 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -27,11 +42,11 @@ describe('Filtered Search Visual Tokens', () => {
describe('input is the last item in tokensContainer', () => { describe('input is the last item in tokensContainer', () => {
it('returns when there is one visual token', () => { it('returns when there is one visual token', () => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'), bugLabelToken.outerHTML,
); );
const { lastVisualToken, isLastVisualTokenValid } const { lastVisualToken, isLastVisualTokenValid }
= gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); = subject.getLastVisualTokenBeforeInput();
expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token')); expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token'));
expect(isLastVisualTokenValid).toEqual(true); expect(isLastVisualTokenValid).toEqual(true);
...@@ -43,7 +58,7 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -43,7 +58,7 @@ describe('Filtered Search Visual Tokens', () => {
); );
const { lastVisualToken, isLastVisualTokenValid } const { lastVisualToken, isLastVisualTokenValid }
= gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); = subject.getLastVisualTokenBeforeInput();
expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token')); expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token'));
expect(isLastVisualTokenValid).toEqual(false); expect(isLastVisualTokenValid).toEqual(false);
...@@ -51,13 +66,13 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -51,13 +66,13 @@ describe('Filtered Search Visual Tokens', () => {
it('returns when there are multiple visual tokens', () => { it('returns when there are multiple visual tokens', () => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} ${bugLabelToken.outerHTML}
${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')} ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')} ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')}
`); `);
const { lastVisualToken, isLastVisualTokenValid } const { lastVisualToken, isLastVisualTokenValid }
= gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); = subject.getLastVisualTokenBeforeInput();
const items = document.querySelectorAll('.tokens-container .js-visual-token'); const items = document.querySelectorAll('.tokens-container .js-visual-token');
expect(lastVisualToken.isEqualNode(items[items.length - 1])).toEqual(true); expect(lastVisualToken.isEqualNode(items[items.length - 1])).toEqual(true);
...@@ -66,13 +81,13 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -66,13 +81,13 @@ describe('Filtered Search Visual Tokens', () => {
it('returns when there are multiple visual tokens and an incomplete visual token', () => { it('returns when there are multiple visual tokens and an incomplete visual token', () => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} ${bugLabelToken.outerHTML}
${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')} ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
${FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('assignee')} ${FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('assignee')}
`); `);
const { lastVisualToken, isLastVisualTokenValid } const { lastVisualToken, isLastVisualTokenValid }
= gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); = subject.getLastVisualTokenBeforeInput();
const items = document.querySelectorAll('.tokens-container .js-visual-token'); const items = document.querySelectorAll('.tokens-container .js-visual-token');
expect(lastVisualToken.isEqualNode(items[items.length - 1])).toEqual(true); expect(lastVisualToken.isEqualNode(items[items.length - 1])).toEqual(true);
...@@ -83,13 +98,13 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -83,13 +98,13 @@ describe('Filtered Search Visual Tokens', () => {
describe('input is a middle item in tokensContainer', () => { describe('input is a middle item in tokensContainer', () => {
it('returns last token before input', () => { it('returns last token before input', () => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} ${bugLabelToken.outerHTML}
${FilteredSearchSpecHelper.createInputHTML()} ${FilteredSearchSpecHelper.createInputHTML()}
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')} ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')}
`); `);
const { lastVisualToken, isLastVisualTokenValid } const { lastVisualToken, isLastVisualTokenValid }
= gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); = subject.getLastVisualTokenBeforeInput();
expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token')); expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token'));
expect(isLastVisualTokenValid).toEqual(true); expect(isLastVisualTokenValid).toEqual(true);
...@@ -103,7 +118,7 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -103,7 +118,7 @@ describe('Filtered Search Visual Tokens', () => {
`); `);
const { lastVisualToken, isLastVisualTokenValid } const { lastVisualToken, isLastVisualTokenValid }
= gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); = subject.getLastVisualTokenBeforeInput();
expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token')); expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token'));
expect(isLastVisualTokenValid).toEqual(false); expect(isLastVisualTokenValid).toEqual(false);
...@@ -114,7 +129,7 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -114,7 +129,7 @@ describe('Filtered Search Visual Tokens', () => {
describe('unselectTokens', () => { describe('unselectTokens', () => {
it('does nothing when there are no tokens', () => { it('does nothing when there are no tokens', () => {
const beforeHTML = tokensContainer.innerHTML; const beforeHTML = tokensContainer.innerHTML;
gl.FilteredSearchVisualTokens.unselectTokens(); subject.unselectTokens();
expect(tokensContainer.innerHTML).toEqual(beforeHTML); expect(tokensContainer.innerHTML).toEqual(beforeHTML);
}); });
...@@ -128,7 +143,7 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -128,7 +143,7 @@ describe('Filtered Search Visual Tokens', () => {
const selected = tokensContainer.querySelector('.js-visual-token .selected'); const selected = tokensContainer.querySelector('.js-visual-token .selected');
expect(selected.classList.contains('selected')).toEqual(true); expect(selected.classList.contains('selected')).toEqual(true);
gl.FilteredSearchVisualTokens.unselectTokens(); subject.unselectTokens();
expect(selected.classList.contains('selected')).toEqual(false); expect(selected.classList.contains('selected')).toEqual(false);
}); });
...@@ -137,7 +152,7 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -137,7 +152,7 @@ describe('Filtered Search Visual Tokens', () => {
describe('selectToken', () => { describe('selectToken', () => {
beforeEach(() => { beforeEach(() => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} ${bugLabelToken.outerHTML}
${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')} ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~awesome')} ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~awesome')}
`); `);
...@@ -147,7 +162,7 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -147,7 +162,7 @@ describe('Filtered Search Visual Tokens', () => {
const firstTokenButton = tokensContainer.querySelector('.js-visual-token .selectable'); const firstTokenButton = tokensContainer.querySelector('.js-visual-token .selectable');
firstTokenButton.classList.add('selected'); firstTokenButton.classList.add('selected');
gl.FilteredSearchVisualTokens.selectToken(firstTokenButton); subject.selectToken(firstTokenButton);
expect(firstTokenButton.classList.contains('selected')).toEqual(false); expect(firstTokenButton.classList.contains('selected')).toEqual(false);
}); });
...@@ -156,7 +171,7 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -156,7 +171,7 @@ describe('Filtered Search Visual Tokens', () => {
it('adds selected class', () => { it('adds selected class', () => {
const firstTokenButton = tokensContainer.querySelector('.js-visual-token .selectable'); const firstTokenButton = tokensContainer.querySelector('.js-visual-token .selectable');
gl.FilteredSearchVisualTokens.selectToken(firstTokenButton); subject.selectToken(firstTokenButton);
expect(firstTokenButton.classList.contains('selected')).toEqual(true); expect(firstTokenButton.classList.contains('selected')).toEqual(true);
}); });
...@@ -165,7 +180,7 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -165,7 +180,7 @@ describe('Filtered Search Visual Tokens', () => {
const tokenButtons = tokensContainer.querySelectorAll('.js-visual-token .selectable'); const tokenButtons = tokensContainer.querySelectorAll('.js-visual-token .selectable');
tokenButtons[1].classList.add('selected'); tokenButtons[1].classList.add('selected');
gl.FilteredSearchVisualTokens.selectToken(tokenButtons[0]); subject.selectToken(tokenButtons[0]);
expect(tokenButtons[0].classList.contains('selected')).toEqual(true); expect(tokenButtons[0].classList.contains('selected')).toEqual(true);
expect(tokenButtons[1].classList.contains('selected')).toEqual(false); expect(tokenButtons[1].classList.contains('selected')).toEqual(false);
...@@ -181,7 +196,7 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -181,7 +196,7 @@ describe('Filtered Search Visual Tokens', () => {
expect(tokensContainer.querySelector('.js-visual-token .selectable')).not.toEqual(null); expect(tokensContainer.querySelector('.js-visual-token .selectable')).not.toEqual(null);
gl.FilteredSearchVisualTokens.removeSelectedToken(); subject.removeSelectedToken();
expect(tokensContainer.querySelector('.js-visual-token .selectable')).not.toEqual(null); expect(tokensContainer.querySelector('.js-visual-token .selectable')).not.toEqual(null);
}); });
...@@ -193,7 +208,7 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -193,7 +208,7 @@ describe('Filtered Search Visual Tokens', () => {
expect(tokensContainer.querySelector('.js-visual-token .selectable')).not.toEqual(null); expect(tokensContainer.querySelector('.js-visual-token .selectable')).not.toEqual(null);
gl.FilteredSearchVisualTokens.removeSelectedToken(); subject.removeSelectedToken();
expect(tokensContainer.querySelector('.js-visual-token .selectable')).toEqual(null); expect(tokensContainer.querySelector('.js-visual-token .selectable')).toEqual(null);
}); });
...@@ -205,7 +220,7 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -205,7 +220,7 @@ describe('Filtered Search Visual Tokens', () => {
beforeEach(() => { beforeEach(() => {
setFixtures(` setFixtures(`
<div class="test-area"> <div class="test-area">
${gl.FilteredSearchVisualTokens.createVisualTokenElementHTML()} ${subject.createVisualTokenElementHTML()}
</div> </div>
`); `);
...@@ -245,7 +260,7 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -245,7 +260,7 @@ describe('Filtered Search Visual Tokens', () => {
describe('addVisualTokenElement', () => { describe('addVisualTokenElement', () => {
it('renders search visual tokens', () => { it('renders search visual tokens', () => {
gl.FilteredSearchVisualTokens.addVisualTokenElement('search term', null, true); subject.addVisualTokenElement('search term', null, true);
const token = tokensContainer.querySelector('.js-visual-token'); const token = tokensContainer.querySelector('.js-visual-token');
expect(token.classList.contains('filtered-search-term')).toEqual(true); expect(token.classList.contains('filtered-search-term')).toEqual(true);
...@@ -254,7 +269,7 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -254,7 +269,7 @@ describe('Filtered Search Visual Tokens', () => {
}); });
it('renders filter visual token name', () => { it('renders filter visual token name', () => {
gl.FilteredSearchVisualTokens.addVisualTokenElement('milestone'); subject.addVisualTokenElement('milestone');
const token = tokensContainer.querySelector('.js-visual-token'); const token = tokensContainer.querySelector('.js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true); expect(token.classList.contains('filtered-search-token')).toEqual(true);
...@@ -263,7 +278,7 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -263,7 +278,7 @@ describe('Filtered Search Visual Tokens', () => {
}); });
it('renders filter visual token name and value', () => { it('renders filter visual token name and value', () => {
gl.FilteredSearchVisualTokens.addVisualTokenElement('label', 'Frontend'); subject.addVisualTokenElement('label', 'Frontend');
const token = tokensContainer.querySelector('.js-visual-token'); const token = tokensContainer.querySelector('.js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true); expect(token.classList.contains('filtered-search-token')).toEqual(true);
...@@ -274,7 +289,7 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -274,7 +289,7 @@ describe('Filtered Search Visual Tokens', () => {
it('inserts visual token before input', () => { it('inserts visual token before input', () => {
tokensContainer.appendChild(FilteredSearchSpecHelper.createFilterVisualToken('assignee', '@root')); tokensContainer.appendChild(FilteredSearchSpecHelper.createFilterVisualToken('assignee', '@root'));
gl.FilteredSearchVisualTokens.addVisualTokenElement('label', 'Frontend'); subject.addVisualTokenElement('label', 'Frontend');
const tokens = tokensContainer.querySelectorAll('.js-visual-token'); const tokens = tokensContainer.querySelectorAll('.js-visual-token');
const labelToken = tokens[0]; const labelToken = tokens[0];
const assigneeToken = tokens[1]; const assigneeToken = tokens[1];
...@@ -296,7 +311,7 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -296,7 +311,7 @@ describe('Filtered Search Visual Tokens', () => {
); );
const original = tokensContainer.innerHTML; const original = tokensContainer.innerHTML;
gl.FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement('value'); subject.addValueToPreviousVisualTokenElement('value');
expect(original).toEqual(tokensContainer.innerHTML); expect(original).toEqual(tokensContainer.innerHTML);
}); });
...@@ -308,7 +323,7 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -308,7 +323,7 @@ describe('Filtered Search Visual Tokens', () => {
`); `);
const original = tokensContainer.innerHTML; const original = tokensContainer.innerHTML;
gl.FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement('value'); subject.addValueToPreviousVisualTokenElement('value');
expect(original).toEqual(tokensContainer.innerHTML); expect(original).toEqual(tokensContainer.innerHTML);
}); });
...@@ -319,7 +334,7 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -319,7 +334,7 @@ describe('Filtered Search Visual Tokens', () => {
); );
const original = tokensContainer.innerHTML; const original = tokensContainer.innerHTML;
gl.FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement('value'); subject.addValueToPreviousVisualTokenElement('value');
const updatedToken = tokensContainer.querySelector('.js-visual-token'); const updatedToken = tokensContainer.querySelector('.js-visual-token');
expect(updatedToken.querySelector('.name').innerText).toEqual('label'); expect(updatedToken.querySelector('.name').innerText).toEqual('label');
...@@ -330,7 +345,7 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -330,7 +345,7 @@ describe('Filtered Search Visual Tokens', () => {
describe('addFilterVisualToken', () => { describe('addFilterVisualToken', () => {
it('creates visual token with just tokenName', () => { it('creates visual token with just tokenName', () => {
gl.FilteredSearchVisualTokens.addFilterVisualToken('milestone'); subject.addFilterVisualToken('milestone');
const token = tokensContainer.querySelector('.js-visual-token'); const token = tokensContainer.querySelector('.js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true); expect(token.classList.contains('filtered-search-token')).toEqual(true);
...@@ -339,8 +354,8 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -339,8 +354,8 @@ describe('Filtered Search Visual Tokens', () => {
}); });
it('creates visual token with just tokenValue', () => { it('creates visual token with just tokenValue', () => {
gl.FilteredSearchVisualTokens.addFilterVisualToken('milestone'); subject.addFilterVisualToken('milestone');
gl.FilteredSearchVisualTokens.addFilterVisualToken('%8.17'); subject.addFilterVisualToken('%8.17');
const token = tokensContainer.querySelector('.js-visual-token'); const token = tokensContainer.querySelector('.js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true); expect(token.classList.contains('filtered-search-token')).toEqual(true);
...@@ -349,7 +364,7 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -349,7 +364,7 @@ describe('Filtered Search Visual Tokens', () => {
}); });
it('creates full visual token', () => { it('creates full visual token', () => {
gl.FilteredSearchVisualTokens.addFilterVisualToken('assignee', '@john'); subject.addFilterVisualToken('assignee', '@john');
const token = tokensContainer.querySelector('.js-visual-token'); const token = tokensContainer.querySelector('.js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true); expect(token.classList.contains('filtered-search-token')).toEqual(true);
...@@ -360,7 +375,7 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -360,7 +375,7 @@ describe('Filtered Search Visual Tokens', () => {
describe('addSearchVisualToken', () => { describe('addSearchVisualToken', () => {
it('creates search visual token', () => { it('creates search visual token', () => {
gl.FilteredSearchVisualTokens.addSearchVisualToken('search term'); subject.addSearchVisualToken('search term');
const token = tokensContainer.querySelector('.js-visual-token'); const token = tokensContainer.querySelector('.js-visual-token');
expect(token.classList.contains('filtered-search-term')).toEqual(true); expect(token.classList.contains('filtered-search-term')).toEqual(true);
...@@ -374,7 +389,7 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -374,7 +389,7 @@ describe('Filtered Search Visual Tokens', () => {
${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')} ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
`); `);
gl.FilteredSearchVisualTokens.addSearchVisualToken('append this'); subject.addSearchVisualToken('append this');
const token = tokensContainer.querySelector('.filtered-search-term'); const token = tokensContainer.querySelector('.filtered-search-term');
expect(token.querySelector('.name').innerText).toEqual('search term append this'); expect(token.querySelector('.name').innerText).toEqual('search term append this');
...@@ -386,10 +401,26 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -386,10 +401,26 @@ describe('Filtered Search Visual Tokens', () => {
it('should get last token value', () => { it('should get last token value', () => {
const value = '~bug'; const value = '~bug';
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', value), bugLabelToken.outerHTML,
); );
expect(gl.FilteredSearchVisualTokens.getLastTokenPartial()).toEqual(value); expect(subject.getLastTokenPartial()).toEqual(value);
});
it('should get last token original value if available', () => {
const originalValue = '@user';
const valueContainer = authorToken.querySelector('.value-container');
valueContainer.dataset.originalValue = originalValue;
const avatar = document.createElement('img');
const valueElement = valueContainer.querySelector('.value');
valueElement.insertAdjacentElement('afterbegin', avatar);
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
authorToken.outerHTML,
);
const lastTokenValue = subject.getLastTokenPartial();
expect(lastTokenValue).toEqual(originalValue);
}); });
it('should get last token name if there is no value', () => { it('should get last token name if there is no value', () => {
...@@ -398,11 +429,11 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -398,11 +429,11 @@ describe('Filtered Search Visual Tokens', () => {
FilteredSearchSpecHelper.createNameFilterVisualTokenHTML(name), FilteredSearchSpecHelper.createNameFilterVisualTokenHTML(name),
); );
expect(gl.FilteredSearchVisualTokens.getLastTokenPartial()).toEqual(name); expect(subject.getLastTokenPartial()).toEqual(name);
}); });
it('should return empty when there are no tokens', () => { it('should return empty when there are no tokens', () => {
expect(gl.FilteredSearchVisualTokens.getLastTokenPartial()).toEqual(''); expect(subject.getLastTokenPartial()).toEqual('');
}); });
}); });
...@@ -414,7 +445,7 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -414,7 +445,7 @@ describe('Filtered Search Visual Tokens', () => {
expect(tokensContainer.querySelector('.js-visual-token .value')).not.toEqual(null); expect(tokensContainer.querySelector('.js-visual-token .value')).not.toEqual(null);
gl.FilteredSearchVisualTokens.removeLastTokenPartial(); subject.removeLastTokenPartial();
expect(tokensContainer.querySelector('.js-visual-token .value')).toEqual(null); expect(tokensContainer.querySelector('.js-visual-token .value')).toEqual(null);
}); });
...@@ -426,14 +457,14 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -426,14 +457,14 @@ describe('Filtered Search Visual Tokens', () => {
expect(tokensContainer.querySelector('.js-visual-token .name')).not.toEqual(null); expect(tokensContainer.querySelector('.js-visual-token .name')).not.toEqual(null);
gl.FilteredSearchVisualTokens.removeLastTokenPartial(); subject.removeLastTokenPartial();
expect(tokensContainer.querySelector('.js-visual-token .name')).toEqual(null); expect(tokensContainer.querySelector('.js-visual-token .name')).toEqual(null);
}); });
it('should not remove anything when there are no tokens', () => { it('should not remove anything when there are no tokens', () => {
const html = tokensContainer.innerHTML; const html = tokensContainer.innerHTML;
gl.FilteredSearchVisualTokens.removeLastTokenPartial(); subject.removeLastTokenPartial();
expect(tokensContainer.innerHTML).toEqual(html); expect(tokensContainer.innerHTML).toEqual(html);
}); });
...@@ -442,7 +473,7 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -442,7 +473,7 @@ describe('Filtered Search Visual Tokens', () => {
describe('tokenizeInput', () => { describe('tokenizeInput', () => {
it('does not do anything if there is no input', () => { it('does not do anything if there is no input', () => {
const original = tokensContainer.innerHTML; const original = tokensContainer.innerHTML;
gl.FilteredSearchVisualTokens.tokenizeInput(); subject.tokenizeInput();
expect(tokensContainer.innerHTML).toEqual(original); expect(tokensContainer.innerHTML).toEqual(original);
}); });
...@@ -454,7 +485,7 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -454,7 +485,7 @@ describe('Filtered Search Visual Tokens', () => {
const input = document.querySelector('.filtered-search'); const input = document.querySelector('.filtered-search');
input.value = 'some value'; input.value = 'some value';
gl.FilteredSearchVisualTokens.tokenizeInput(); subject.tokenizeInput();
const newToken = tokensContainer.querySelector('.filtered-search-term'); const newToken = tokensContainer.querySelector('.filtered-search-term');
...@@ -470,7 +501,7 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -470,7 +501,7 @@ describe('Filtered Search Visual Tokens', () => {
const input = document.querySelector('.filtered-search'); const input = document.querySelector('.filtered-search');
input.value = '@john'; input.value = '@john';
gl.FilteredSearchVisualTokens.tokenizeInput(); subject.tokenizeInput();
const updatedToken = tokensContainer.querySelector('.filtered-search-token'); const updatedToken = tokensContainer.querySelector('.filtered-search-token');
...@@ -497,29 +528,39 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -497,29 +528,39 @@ describe('Filtered Search Visual Tokens', () => {
it('tokenize\'s existing input', () => { it('tokenize\'s existing input', () => {
input.value = 'some text'; input.value = 'some text';
spyOn(gl.FilteredSearchVisualTokens, 'tokenizeInput').and.callThrough(); spyOn(subject, 'tokenizeInput').and.callThrough();
gl.FilteredSearchVisualTokens.editToken(token); subject.editToken(token);
expect(gl.FilteredSearchVisualTokens.tokenizeInput).toHaveBeenCalled(); expect(subject.tokenizeInput).toHaveBeenCalled();
expect(input.value).not.toEqual('some text'); expect(input.value).not.toEqual('some text');
}); });
it('moves input to the token position', () => { it('moves input to the token position', () => {
expect(tokensContainer.children[3].querySelector('.filtered-search')).not.toEqual(null); expect(tokensContainer.children[3].querySelector('.filtered-search')).not.toEqual(null);
gl.FilteredSearchVisualTokens.editToken(token); subject.editToken(token);
expect(tokensContainer.children[1].querySelector('.filtered-search')).not.toEqual(null); expect(tokensContainer.children[1].querySelector('.filtered-search')).not.toEqual(null);
expect(tokensContainer.children[3].querySelector('.filtered-search')).toEqual(null); expect(tokensContainer.children[3].querySelector('.filtered-search')).toEqual(null);
}); });
it('input contains the visual token value', () => { it('input contains the visual token value', () => {
gl.FilteredSearchVisualTokens.editToken(token); subject.editToken(token);
expect(input.value).toEqual('none'); expect(input.value).toEqual('none');
}); });
it('input contains the original value if present', () => {
const originalValue = '@user';
const valueContainer = token.querySelector('.value-container');
valueContainer.dataset.originalValue = originalValue;
subject.editToken(token);
expect(input.value).toEqual(originalValue);
});
describe('selected token is a search term token', () => { describe('selected token is a search term token', () => {
beforeEach(() => { beforeEach(() => {
token = document.querySelector('.filtered-search-term'); token = document.querySelector('.filtered-search-term');
...@@ -528,7 +569,7 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -528,7 +569,7 @@ describe('Filtered Search Visual Tokens', () => {
it('token is removed', () => { it('token is removed', () => {
expect(tokensContainer.querySelector('.filtered-search-term')).not.toEqual(null); expect(tokensContainer.querySelector('.filtered-search-term')).not.toEqual(null);
gl.FilteredSearchVisualTokens.editToken(token); subject.editToken(token);
expect(tokensContainer.querySelector('.filtered-search-term')).toEqual(null); expect(tokensContainer.querySelector('.filtered-search-term')).toEqual(null);
}); });
...@@ -536,7 +577,7 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -536,7 +577,7 @@ describe('Filtered Search Visual Tokens', () => {
it('input has the same value as removed token', () => { it('input has the same value as removed token', () => {
expect(input.value).toEqual(''); expect(input.value).toEqual('');
gl.FilteredSearchVisualTokens.editToken(token); subject.editToken(token);
expect(input.value).toEqual('search'); expect(input.value).toEqual('search');
}); });
...@@ -549,25 +590,25 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -549,25 +590,25 @@ describe('Filtered Search Visual Tokens', () => {
FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none'), FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none'),
); );
spyOn(gl.FilteredSearchVisualTokens, 'tokenizeInput').and.callFake(() => {}); spyOn(subject, 'tokenizeInput').and.callFake(() => {});
spyOn(gl.FilteredSearchVisualTokens, 'getLastVisualTokenBeforeInput').and.callThrough(); spyOn(subject, 'getLastVisualTokenBeforeInput').and.callThrough();
gl.FilteredSearchVisualTokens.moveInputToTheRight(); subject.moveInputToTheRight();
expect(gl.FilteredSearchVisualTokens.tokenizeInput).toHaveBeenCalled(); expect(subject.tokenizeInput).toHaveBeenCalled();
expect(gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput).not.toHaveBeenCalled(); expect(subject.getLastVisualTokenBeforeInput).not.toHaveBeenCalled();
}); });
it('tokenize\'s input', () => { it('tokenize\'s input', () => {
tokensContainer.innerHTML = ` tokensContainer.innerHTML = `
${FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('label')} ${FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('label')}
${FilteredSearchSpecHelper.createInputHTML()} ${FilteredSearchSpecHelper.createInputHTML()}
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} ${bugLabelToken.outerHTML}
`; `;
document.querySelector('.filtered-search').value = 'none'; document.querySelector('.filtered-search').value = 'none';
gl.FilteredSearchVisualTokens.moveInputToTheRight(); subject.moveInputToTheRight();
const value = tokensContainer.querySelector('.js-visual-token .value'); const value = tokensContainer.querySelector('.js-visual-token .value');
expect(value.innerText).toEqual('none'); expect(value.innerText).toEqual('none');
...@@ -577,12 +618,12 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -577,12 +618,12 @@ describe('Filtered Search Visual Tokens', () => {
tokensContainer.innerHTML = ` tokensContainer.innerHTML = `
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')} ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')}
${FilteredSearchSpecHelper.createInputHTML()} ${FilteredSearchSpecHelper.createInputHTML()}
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} ${bugLabelToken.outerHTML}
`; `;
document.querySelector('.filtered-search').value = 'test'; document.querySelector('.filtered-search').value = 'test';
gl.FilteredSearchVisualTokens.moveInputToTheRight(); subject.moveInputToTheRight();
const searchValue = tokensContainer.querySelector('.filtered-search-term .name'); const searchValue = tokensContainer.querySelector('.filtered-search-term .name');
expect(searchValue.innerText).toEqual('test'); expect(searchValue.innerText).toEqual('test');
...@@ -592,10 +633,10 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -592,10 +633,10 @@ describe('Filtered Search Visual Tokens', () => {
tokensContainer.innerHTML = ` tokensContainer.innerHTML = `
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')} ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')}
${FilteredSearchSpecHelper.createInputHTML()} ${FilteredSearchSpecHelper.createInputHTML()}
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} ${bugLabelToken.outerHTML}
`; `;
gl.FilteredSearchVisualTokens.moveInputToTheRight(); subject.moveInputToTheRight();
expect(tokensContainer.children[2].querySelector('.filtered-search')).not.toEqual(null); expect(tokensContainer.children[2].querySelector('.filtered-search')).not.toEqual(null);
}); });
...@@ -607,7 +648,7 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -607,7 +648,7 @@ describe('Filtered Search Visual Tokens', () => {
${FilteredSearchSpecHelper.createInputHTML('', '~bug')} ${FilteredSearchSpecHelper.createInputHTML('', '~bug')}
`; `;
gl.FilteredSearchVisualTokens.moveInputToTheRight(); subject.moveInputToTheRight();
const token = tokensContainer.children[1]; const token = tokensContainer.children[1];
expect(token.querySelector('.value').innerText).toEqual('~bug'); expect(token.querySelector('.value').innerText).toEqual('~bug');
...@@ -615,42 +656,144 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -615,42 +656,144 @@ describe('Filtered Search Visual Tokens', () => {
}); });
describe('renderVisualTokenValue', () => { describe('renderVisualTokenValue', () => {
let searchTokens; const keywordToken = FilteredSearchSpecHelper.createFilterVisualToken('search');
const milestoneToken = FilteredSearchSpecHelper.createFilterVisualToken('milestone', 'upcoming');
let updateLabelTokenColorSpy;
let updateUserTokenAppearanceSpy;
beforeEach(() => { beforeEach(() => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')} ${authorToken.outerHTML}
${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search')} ${bugLabelToken.outerHTML}
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'upcoming')} ${keywordToken.outerHTML}
${milestoneToken.outerHTML}
`); `);
searchTokens = document.querySelectorAll('.filtered-search-token'); spyOn(subject, 'updateLabelTokenColor');
updateLabelTokenColorSpy = subject.updateLabelTokenColor;
spyOn(subject, 'updateUserTokenAppearance');
updateUserTokenAppearanceSpy = subject.updateUserTokenAppearance;
}); });
it('renders a token value element', () => { it('renders a author token value element', () => {
spyOn(gl.FilteredSearchVisualTokens, 'updateLabelTokenColor'); const { tokenNameElement, tokenValueContainer, tokenValueElement } =
const updateLabelTokenColorSpy = gl.FilteredSearchVisualTokens.updateLabelTokenColor; findElements(authorToken);
const tokenName = tokenNameElement.innerText;
const tokenValue = 'new value';
expect(searchTokens.length).toBe(2); subject.renderVisualTokenValue(authorToken, tokenName, tokenValue);
Array.prototype.forEach.call(searchTokens, (token) => {
updateLabelTokenColorSpy.calls.reset();
const tokenName = token.querySelector('.name').innerText; expect(tokenValueElement.innerText).toBe(tokenValue);
expect(updateUserTokenAppearanceSpy.calls.count()).toBe(1);
const expectedArgs = [tokenValueContainer, tokenValueElement, tokenValue];
expect(updateUserTokenAppearanceSpy.calls.argsFor(0)).toEqual(expectedArgs);
expect(updateLabelTokenColorSpy.calls.count()).toBe(0);
});
it('renders a label token value element', () => {
const { tokenNameElement, tokenValueContainer, tokenValueElement } =
findElements(bugLabelToken);
const tokenName = tokenNameElement.innerText;
const tokenValue = 'new value'; const tokenValue = 'new value';
gl.FilteredSearchVisualTokens.renderVisualTokenValue(token, tokenName, tokenValue);
const tokenValueElement = token.querySelector('.value'); subject.renderVisualTokenValue(bugLabelToken, tokenName, tokenValue);
expect(tokenValueElement.innerText).toBe(tokenValue);
if (tokenName.toLowerCase() === 'label') { expect(tokenValueElement.innerText).toBe(tokenValue);
const tokenValueContainer = token.querySelector('.value-container');
expect(updateLabelTokenColorSpy.calls.count()).toBe(1); expect(updateLabelTokenColorSpy.calls.count()).toBe(1);
const expectedArgs = [tokenValueContainer, tokenValue]; const expectedArgs = [tokenValueContainer, tokenValue];
expect(updateLabelTokenColorSpy.calls.argsFor(0)).toEqual(expectedArgs); expect(updateLabelTokenColorSpy.calls.argsFor(0)).toEqual(expectedArgs);
} else { expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0);
});
it('renders a milestone token value element', () => {
const { tokenNameElement, tokenValueElement } = findElements(milestoneToken);
const tokenName = tokenNameElement.innerText;
const tokenValue = 'new value';
subject.renderVisualTokenValue(milestoneToken, tokenName, tokenValue);
expect(tokenValueElement.innerText).toBe(tokenValue);
expect(updateLabelTokenColorSpy.calls.count()).toBe(0); expect(updateLabelTokenColorSpy.calls.count()).toBe(0);
} expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0);
});
});
describe('updateUserTokenAppearance', () => {
let usersCacheSpy;
beforeEach(() => {
spyOn(UsersCache, 'retrieve').and.callFake(username => usersCacheSpy(username));
}); });
it('ignores special value "none"', (done) => {
usersCacheSpy = (username) => {
expect(username).toBe('none');
done.fail('Should not resolve "none"!');
};
const { tokenValueContainer, tokenValueElement } = findElements(authorToken);
subject.updateUserTokenAppearance(tokenValueContainer, tokenValueElement, 'none')
.then(done)
.catch(done.fail);
});
it('ignores error if UsersCache throws', (done) => {
spyOn(window, 'Flash');
const dummyError = new Error('Earth rotated backwards');
const { tokenValueContainer, tokenValueElement } = findElements(authorToken);
const tokenValue = tokenValueElement.innerText;
usersCacheSpy = (username) => {
expect(`@${username}`).toBe(tokenValue);
return Promise.reject(dummyError);
};
subject.updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue)
.then(() => {
expect(window.Flash.calls.count()).toBe(0);
})
.then(done)
.catch(done.fail);
});
it('does nothing if user cannot be found', (done) => {
const { tokenValueContainer, tokenValueElement } = findElements(authorToken);
const tokenValue = tokenValueElement.innerText;
usersCacheSpy = (username) => {
expect(`@${username}`).toBe(tokenValue);
return Promise.resolve(undefined);
};
subject.updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue)
.then(() => {
expect(tokenValueElement.innerText).toBe(tokenValue);
})
.then(done)
.catch(done.fail);
});
it('replaces author token with avatar and display name', (done) => {
const dummyUser = {
name: 'Important Person',
avatar_url: 'https://host.invalid/mypics/avatar.png',
};
const { tokenValueContainer, tokenValueElement } = findElements(authorToken);
const tokenValue = tokenValueElement.innerText;
usersCacheSpy = (username) => {
expect(`@${username}`).toBe(tokenValue);
return Promise.resolve(dummyUser);
};
subject.updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue)
.then(() => {
expect(tokenValueContainer.dataset.originalValue).toBe(tokenValue);
expect(tokenValueElement.innerText.trim()).toBe(dummyUser.name);
const avatar = tokenValueElement.querySelector('img.avatar');
expect(avatar.src).toBe(dummyUser.avatar_url);
})
.then(done)
.catch(done.fail);
}); });
}); });
...@@ -659,21 +802,16 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -659,21 +802,16 @@ describe('Filtered Search Visual Tokens', () => {
const dummyEndpoint = '/dummy/endpoint'; const dummyEndpoint = '/dummy/endpoint';
preloadFixtures(jsonFixtureName); preloadFixtures(jsonFixtureName);
const labelData = getJSONFixture(jsonFixtureName);
const findLabel = tokenValue => labelData.find(
label => tokenValue === `~${gl.DropdownUtils.getEscapedText(label.title)}`,
);
const bugLabelToken = FilteredSearchSpecHelper.createFilterVisualToken('label', '~bug'); let labelData;
beforeAll(() => {
labelData = getJSONFixture(jsonFixtureName);
});
const missingLabelToken = FilteredSearchSpecHelper.createFilterVisualToken('label', '~doesnotexist'); const missingLabelToken = FilteredSearchSpecHelper.createFilterVisualToken('label', '~doesnotexist');
const spaceLabelToken = FilteredSearchSpecHelper.createFilterVisualToken('label', '~"some space"'); const spaceLabelToken = FilteredSearchSpecHelper.createFilterVisualToken('label', '~"some space"');
const parseColor = (color) => {
const dummyElement = document.createElement('div');
dummyElement.style.color = color;
return dummyElement.style.color;
};
beforeEach(() => { beforeEach(() => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
${bugLabelToken.outerHTML} ${bugLabelToken.outerHTML}
...@@ -688,28 +826,60 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -688,28 +826,60 @@ describe('Filtered Search Visual Tokens', () => {
AjaxCache.internalStorage[`${dummyEndpoint}/labels.json`] = labelData; AjaxCache.internalStorage[`${dummyEndpoint}/labels.json`] = labelData;
}); });
const testCase = (token, done) => { const parseColor = (color) => {
const tokenValueContainer = token.querySelector('.value-container'); const dummyElement = document.createElement('div');
const tokenValue = token.querySelector('.value').innerText; dummyElement.style.color = color;
const label = findLabel(tokenValue); return dummyElement.style.color;
};
gl.FilteredSearchVisualTokens.updateLabelTokenColor(tokenValueContainer, tokenValue) const expectValueContainerStyle = (tokenValueContainer, label) => {
.then(() => {
if (label) {
expect(tokenValueContainer.getAttribute('style')).not.toBe(null); expect(tokenValueContainer.getAttribute('style')).not.toBe(null);
expect(tokenValueContainer.style.backgroundColor).toBe(parseColor(label.color)); expect(tokenValueContainer.style.backgroundColor).toBe(parseColor(label.color));
expect(tokenValueContainer.style.color).toBe(parseColor(label.text_color)); expect(tokenValueContainer.style.color).toBe(parseColor(label.text_color));
} else { };
expect(token).toBe(missingLabelToken);
expect(tokenValueContainer.getAttribute('style')).toBe(null); const findLabel = tokenValue => labelData.find(
} label => tokenValue === `~${gl.DropdownUtils.getEscapedText(label.title)}`,
);
it('updates the color of a label token', (done) => {
const { tokenValueContainer, tokenValueElement } = findElements(bugLabelToken);
const tokenValue = tokenValueElement.innerText;
const matchingLabel = findLabel(tokenValue);
subject.updateLabelTokenColor(tokenValueContainer, tokenValue)
.then(() => {
expectValueContainerStyle(tokenValueContainer, matchingLabel);
}) })
.then(done) .then(done)
.catch(fail); .catch(done.fail);
}; });
it('updates the color of a label token with spaces', (done) => {
const { tokenValueContainer, tokenValueElement } = findElements(spaceLabelToken);
const tokenValue = tokenValueElement.innerText;
const matchingLabel = findLabel(tokenValue);
subject.updateLabelTokenColor(tokenValueContainer, tokenValue)
.then(() => {
expectValueContainerStyle(tokenValueContainer, matchingLabel);
})
.then(done)
.catch(done.fail);
});
it('does not change color of a missing label', (done) => {
const { tokenValueContainer, tokenValueElement } = findElements(missingLabelToken);
const tokenValue = tokenValueElement.innerText;
const matchingLabel = findLabel(tokenValue);
expect(matchingLabel).toBe(undefined);
it('updates the color of a label token', done => testCase(bugLabelToken, done)); subject.updateLabelTokenColor(tokenValueContainer, tokenValue)
it('updates the color of a label token with spaces', done => testCase(spaceLabelToken, done)); .then(() => {
it('does not change color of a missing label', done => testCase(missingLabelToken, done)); expect(tokenValueContainer.getAttribute('style')).toBe(null);
})
.then(done)
.catch(done.fail);
});
}); });
}); });
...@@ -36,6 +36,17 @@ describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller ...@@ -36,6 +36,17 @@ describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller
render_issue(example.description, issue) render_issue(example.description, issue)
end end
it 'issues/issue_list.html.raw' do |example|
create(:issue, project: project)
get :index,
namespace_id: project.namespace.to_param,
project_id: project
expect(response).to be_success
store_frontend_fixture(response, example.description)
end
private private
def render_issue(fixture_file_name, issue) def render_issue(fixture_file_name, issue)
......
...@@ -30,12 +30,15 @@ export default class FilteredSearchSpecHelper { ...@@ -30,12 +30,15 @@ export default class FilteredSearchSpecHelper {
`; `;
} }
static createSearchVisualToken(name) {
const li = document.createElement('li');
li.classList.add('js-visual-token', 'filtered-search-term');
li.innerHTML = `<div class="name">${name}</div>`;
return li;
}
static createSearchVisualTokenHTML(name) { static createSearchVisualTokenHTML(name) {
return ` return FilteredSearchSpecHelper.createSearchVisualToken(name).outerHTML;
<li class="js-visual-token filtered-search-term">
<div class="name">${name}</div>
</li>
`;
} }
static createInputHTML(placeholder = '', value = '') { static createInputHTML(placeholder = '', value = '') {
......
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