diff options
Diffstat (limited to 'spec/javascripts/filtered_search')
12 files changed, 1431 insertions, 799 deletions
diff --git a/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js b/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js new file mode 100644 index 00000000000..d0f09a561d5 --- /dev/null +++ b/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js @@ -0,0 +1,186 @@ +import Vue from 'vue'; +import eventHub from '~/filtered_search/event_hub'; +import RecentSearchesDropdownContent from '~/filtered_search/components/recent_searches_dropdown_content'; + +const createComponent = (propsData) => { + const Component = Vue.extend(RecentSearchesDropdownContent); + + return new Component({ + el: document.createElement('div'), + propsData, + }); +}; + +// Remove all the newlines and whitespace from the formatted markup +const trimMarkupWhitespace = text => text.replace(/(\n|\s)+/gm, ' ').trim(); + +describe('RecentSearchesDropdownContent', () => { + const propsDataWithoutItems = { + items: [], + }; + const propsDataWithItems = { + items: [ + 'foo', + 'author:@root label:~foo bar', + ], + }; + + let vm; + afterEach(() => { + if (vm) { + vm.$destroy(); + } + }); + + describe('with no items', () => { + let el; + + beforeEach(() => { + vm = createComponent(propsDataWithoutItems); + el = vm.$el; + }); + + it('should render empty state', () => { + expect(el.querySelector('.dropdown-info-note')).toBeDefined(); + + const items = el.querySelectorAll('.filtered-search-history-dropdown-item'); + expect(items.length).toEqual(propsDataWithoutItems.items.length); + }); + }); + + describe('with items', () => { + let el; + + beforeEach(() => { + vm = createComponent(propsDataWithItems); + el = vm.$el; + }); + + it('should render clear recent searches button', () => { + expect(el.querySelector('.filtered-search-history-clear-button')).toBeDefined(); + }); + + it('should render recent search items', () => { + const items = el.querySelectorAll('.filtered-search-history-dropdown-item'); + expect(items.length).toEqual(propsDataWithItems.items.length); + + expect(trimMarkupWhitespace(items[0].querySelector('.filtered-search-history-dropdown-search-token').textContent)).toEqual('foo'); + + const item1Tokens = items[1].querySelectorAll('.filtered-search-history-dropdown-token'); + expect(item1Tokens.length).toEqual(2); + expect(item1Tokens[0].querySelector('.name').textContent).toEqual('author:'); + expect(item1Tokens[0].querySelector('.value').textContent).toEqual('@root'); + expect(item1Tokens[1].querySelector('.name').textContent).toEqual('label:'); + expect(item1Tokens[1].querySelector('.value').textContent).toEqual('~foo'); + expect(trimMarkupWhitespace(items[1].querySelector('.filtered-search-history-dropdown-search-token').textContent)).toEqual('bar'); + }); + }); + + describe('if isLocalStorageAvailable is `false`', () => { + let el; + + beforeEach(() => { + const props = Object.assign({ isLocalStorageAvailable: false }, propsDataWithItems); + + vm = createComponent(props); + el = vm.$el; + }); + + it('should render an info note', () => { + const note = el.querySelector('.dropdown-info-note'); + const items = el.querySelectorAll('.filtered-search-history-dropdown-item'); + + expect(note).toBeDefined(); + expect(note.innerText.trim()).toBe('This feature requires local storage to be enabled'); + expect(items.length).toEqual(propsDataWithoutItems.items.length); + }); + }); + + describe('computed', () => { + describe('processedItems', () => { + it('with items', () => { + vm = createComponent(propsDataWithItems); + const processedItems = vm.processedItems; + + expect(processedItems.length).toEqual(2); + + expect(processedItems[0].text).toEqual(propsDataWithItems.items[0]); + expect(processedItems[0].tokens).toEqual([]); + expect(processedItems[0].searchToken).toEqual('foo'); + + expect(processedItems[1].text).toEqual(propsDataWithItems.items[1]); + expect(processedItems[1].tokens.length).toEqual(2); + expect(processedItems[1].tokens[0].prefix).toEqual('author:'); + expect(processedItems[1].tokens[0].suffix).toEqual('@root'); + expect(processedItems[1].tokens[1].prefix).toEqual('label:'); + expect(processedItems[1].tokens[1].suffix).toEqual('~foo'); + expect(processedItems[1].searchToken).toEqual('bar'); + }); + + it('with no items', () => { + vm = createComponent(propsDataWithoutItems); + const processedItems = vm.processedItems; + + expect(processedItems.length).toEqual(0); + }); + }); + + describe('hasItems', () => { + it('with items', () => { + vm = createComponent(propsDataWithItems); + const hasItems = vm.hasItems; + expect(hasItems).toEqual(true); + }); + + it('with no items', () => { + vm = createComponent(propsDataWithoutItems); + const hasItems = vm.hasItems; + expect(hasItems).toEqual(false); + }); + }); + }); + + describe('methods', () => { + describe('onItemActivated', () => { + let onRecentSearchesItemSelectedSpy; + + beforeEach(() => { + onRecentSearchesItemSelectedSpy = jasmine.createSpy('spy'); + eventHub.$on('recentSearchesItemSelected', onRecentSearchesItemSelectedSpy); + + vm = createComponent(propsDataWithItems); + }); + + afterEach(() => { + eventHub.$off('recentSearchesItemSelected', onRecentSearchesItemSelectedSpy); + }); + + it('emits event', () => { + expect(onRecentSearchesItemSelectedSpy).not.toHaveBeenCalled(); + vm.onItemActivated('something'); + expect(onRecentSearchesItemSelectedSpy).toHaveBeenCalledWith('something'); + }); + }); + + describe('onRequestClearRecentSearches', () => { + let onRequestClearRecentSearchesSpy; + + beforeEach(() => { + onRequestClearRecentSearchesSpy = jasmine.createSpy('spy'); + eventHub.$on('requestClearRecentSearches', onRequestClearRecentSearchesSpy); + + vm = createComponent(propsDataWithItems); + }); + + afterEach(() => { + eventHub.$off('requestClearRecentSearches', onRequestClearRecentSearchesSpy); + }); + + it('emits event', () => { + expect(onRequestClearRecentSearchesSpy).not.toHaveBeenCalled(); + vm.onRequestClearRecentSearches({ stopPropagation: () => {} }); + expect(onRequestClearRecentSearchesSpy).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/spec/javascripts/filtered_search/dropdown_user_spec.js b/spec/javascripts/filtered_search/dropdown_user_spec.js index c16f77c53a2..0d8bdf4c8e7 100644 --- a/spec/javascripts/filtered_search/dropdown_user_spec.js +++ b/spec/javascripts/filtered_search/dropdown_user_spec.js @@ -1,71 +1,69 @@ -require('~/filtered_search/dropdown_utils'); -require('~/filtered_search/filtered_search_tokenizer'); -require('~/filtered_search/filtered_search_dropdown'); -require('~/filtered_search/dropdown_user'); +import '~/filtered_search/dropdown_utils'; +import '~/filtered_search/filtered_search_tokenizer'; +import '~/filtered_search/filtered_search_dropdown'; +import '~/filtered_search/dropdown_user'; -(() => { - describe('Dropdown User', () => { - describe('getSearchInput', () => { - let dropdownUser; +describe('Dropdown User', () => { + describe('getSearchInput', () => { + let dropdownUser; - beforeEach(() => { - spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {}); - spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {}); - spyOn(gl.DropdownUtils, 'getSearchInput').and.callFake(() => {}); + beforeEach(() => { + spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {}); + spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {}); + spyOn(gl.DropdownUtils, 'getSearchInput').and.callFake(() => {}); - dropdownUser = new gl.DropdownUser(); - }); - - it('should not return the double quote found in value', () => { - spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({ - lastToken: '"johnny appleseed', - }); + dropdownUser = new gl.DropdownUser(); + }); - expect(dropdownUser.getSearchInput()).toBe('johnny appleseed'); + it('should not return the double quote found in value', () => { + spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({ + lastToken: '"johnny appleseed', }); - it('should not return the single quote found in value', () => { - spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({ - lastToken: '\'larry boy', - }); + expect(dropdownUser.getSearchInput()).toBe('johnny appleseed'); + }); - expect(dropdownUser.getSearchInput()).toBe('larry boy'); + it('should not return the single quote found in value', () => { + spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({ + lastToken: '\'larry boy', }); + + expect(dropdownUser.getSearchInput()).toBe('larry boy'); }); + }); - describe('config droplabAjaxFilter\'s endpoint', () => { - beforeEach(() => { - spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {}); - spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {}); - }); + describe('config AjaxFilter\'s endpoint', () => { + beforeEach(() => { + spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {}); + spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {}); + }); - it('should return endpoint', () => { - window.gon = { - relative_url_root: '', - }; - const dropdown = new gl.DropdownUser(); + it('should return endpoint', () => { + window.gon = { + relative_url_root: '', + }; + const dropdown = new gl.DropdownUser(); - expect(dropdown.config.droplabAjaxFilter.endpoint).toBe('/autocomplete/users.json'); - }); + expect(dropdown.config.AjaxFilter.endpoint).toBe('/autocomplete/users.json'); + }); - it('should return endpoint when relative_url_root is undefined', () => { - const dropdown = new gl.DropdownUser(); + it('should return endpoint when relative_url_root is undefined', () => { + const dropdown = new gl.DropdownUser(); - expect(dropdown.config.droplabAjaxFilter.endpoint).toBe('/autocomplete/users.json'); - }); + expect(dropdown.config.AjaxFilter.endpoint).toBe('/autocomplete/users.json'); + }); - it('should return endpoint with relative url when available', () => { - window.gon = { - relative_url_root: '/gitlab_directory', - }; - const dropdown = new gl.DropdownUser(); + it('should return endpoint with relative url when available', () => { + window.gon = { + relative_url_root: '/gitlab_directory', + }; + const dropdown = new gl.DropdownUser(); - expect(dropdown.config.droplabAjaxFilter.endpoint).toBe('/gitlab_directory/autocomplete/users.json'); - }); + expect(dropdown.config.AjaxFilter.endpoint).toBe('/gitlab_directory/autocomplete/users.json'); + }); - afterEach(() => { - window.gon = {}; - }); + afterEach(() => { + window.gon = {}; }); }); -})(); +}); diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js b/spec/javascripts/filtered_search/dropdown_utils_spec.js index e6538020896..a68e315e3e4 100644 --- a/spec/javascripts/filtered_search/dropdown_utils_spec.js +++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js @@ -1,310 +1,308 @@ -require('~/extensions/array'); -require('~/filtered_search/dropdown_utils'); -require('~/filtered_search/filtered_search_tokenizer'); -require('~/filtered_search/filtered_search_dropdown_manager'); - -(() => { - describe('Dropdown Utils', () => { - describe('getEscapedText', () => { - it('should return same word when it has no space', () => { - const escaped = gl.DropdownUtils.getEscapedText('textWithoutSpace'); - expect(escaped).toBe('textWithoutSpace'); - }); +import '~/extensions/array'; +import '~/filtered_search/dropdown_utils'; +import '~/filtered_search/filtered_search_tokenizer'; +import '~/filtered_search/filtered_search_dropdown_manager'; + +describe('Dropdown Utils', () => { + describe('getEscapedText', () => { + it('should return same word when it has no space', () => { + const escaped = gl.DropdownUtils.getEscapedText('textWithoutSpace'); + expect(escaped).toBe('textWithoutSpace'); + }); - it('should escape with double quotes', () => { - let escaped = gl.DropdownUtils.getEscapedText('text with space'); - expect(escaped).toBe('"text with space"'); + it('should escape with double quotes', () => { + let escaped = gl.DropdownUtils.getEscapedText('text with space'); + expect(escaped).toBe('"text with space"'); - escaped = gl.DropdownUtils.getEscapedText('won\'t fix'); - expect(escaped).toBe('"won\'t fix"'); - }); + escaped = gl.DropdownUtils.getEscapedText('won\'t fix'); + expect(escaped).toBe('"won\'t fix"'); + }); - it('should escape with single quotes', () => { - const escaped = gl.DropdownUtils.getEscapedText('won"t fix'); - expect(escaped).toBe('\'won"t fix\''); - }); + it('should escape with single quotes', () => { + const escaped = gl.DropdownUtils.getEscapedText('won"t fix'); + expect(escaped).toBe('\'won"t fix\''); + }); - it('should escape with single quotes by default', () => { - const escaped = gl.DropdownUtils.getEscapedText('won"t\' fix'); - expect(escaped).toBe('\'won"t\' fix\''); - }); + it('should escape with single quotes by default', () => { + const escaped = gl.DropdownUtils.getEscapedText('won"t\' fix'); + expect(escaped).toBe('\'won"t\' fix\''); }); + }); - describe('filterWithSymbol', () => { - let input; - const item = { - title: '@root', - }; + describe('filterWithSymbol', () => { + let input; + const item = { + title: '@root', + }; - beforeEach(() => { - setFixtures(` - <input type="text" id="test" /> - `); + beforeEach(() => { + setFixtures(` + <input type="text" id="test" /> + `); - input = document.getElementById('test'); - }); + input = document.getElementById('test'); + }); - it('should filter without symbol', () => { - input.value = 'roo'; + it('should filter without symbol', () => { + input.value = 'roo'; - const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item); - expect(updatedItem.droplab_hidden).toBe(false); - }); + const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item); + expect(updatedItem.droplab_hidden).toBe(false); + }); - it('should filter with symbol', () => { - input.value = '@roo'; + it('should filter with symbol', () => { + input.value = '@roo'; - const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item); - expect(updatedItem.droplab_hidden).toBe(false); - }); + const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item); + expect(updatedItem.droplab_hidden).toBe(false); + }); - describe('filters multiple word title', () => { - const multipleWordItem = { - title: 'Community Contributions', - }; + describe('filters multiple word title', () => { + const multipleWordItem = { + title: 'Community Contributions', + }; - it('should filter with double quote', () => { - input.value = '"'; + it('should filter with double quote', () => { + input.value = '"'; - const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); - expect(updatedItem.droplab_hidden).toBe(false); - }); + const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); + expect(updatedItem.droplab_hidden).toBe(false); + }); - it('should filter with double quote and symbol', () => { - input.value = '~"'; + it('should filter with double quote and symbol', () => { + input.value = '~"'; - const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); - expect(updatedItem.droplab_hidden).toBe(false); - }); + const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); + expect(updatedItem.droplab_hidden).toBe(false); + }); - it('should filter with double quote and multiple words', () => { - input.value = '"community con'; + it('should filter with double quote and multiple words', () => { + input.value = '"community con'; - const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); - expect(updatedItem.droplab_hidden).toBe(false); - }); + const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); + expect(updatedItem.droplab_hidden).toBe(false); + }); - it('should filter with double quote, symbol and multiple words', () => { - input.value = '~"community con'; + it('should filter with double quote, symbol and multiple words', () => { + input.value = '~"community con'; - const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); - expect(updatedItem.droplab_hidden).toBe(false); - }); + const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); + expect(updatedItem.droplab_hidden).toBe(false); + }); - it('should filter with single quote', () => { - input.value = '\''; + it('should filter with single quote', () => { + input.value = '\''; - const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); - expect(updatedItem.droplab_hidden).toBe(false); - }); + const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); + expect(updatedItem.droplab_hidden).toBe(false); + }); - it('should filter with single quote and symbol', () => { - input.value = '~\''; + it('should filter with single quote and symbol', () => { + input.value = '~\''; - const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); - expect(updatedItem.droplab_hidden).toBe(false); - }); + const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); + expect(updatedItem.droplab_hidden).toBe(false); + }); - it('should filter with single quote and multiple words', () => { - input.value = '\'community con'; + it('should filter with single quote and multiple words', () => { + input.value = '\'community con'; - const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); - expect(updatedItem.droplab_hidden).toBe(false); - }); + const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); + expect(updatedItem.droplab_hidden).toBe(false); + }); - it('should filter with single quote, symbol and multiple words', () => { - input.value = '~\'community con'; + it('should filter with single quote, symbol and multiple words', () => { + input.value = '~\'community con'; - const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); - expect(updatedItem.droplab_hidden).toBe(false); - }); + const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); + expect(updatedItem.droplab_hidden).toBe(false); }); }); + }); - describe('filterHint', () => { - let input; - - beforeEach(() => { - setFixtures(` - <ul class="tokens-container"> - <li class="input-token"> - <input class="filtered-search" type="text" id="test" /> - </li> - </ul> - `); - - input = document.getElementById('test'); - }); + describe('filterHint', () => { + let input; - it('should filter', () => { - input.value = 'l'; - let updatedItem = gl.DropdownUtils.filterHint(input, { - hint: 'label', - }); - expect(updatedItem.droplab_hidden).toBe(false); + beforeEach(() => { + setFixtures(` + <ul class="tokens-container"> + <li class="input-token"> + <input class="filtered-search" type="text" id="test" /> + </li> + </ul> + `); - input.value = 'o'; - updatedItem = gl.DropdownUtils.filterHint(input, { - hint: 'label', - }); - expect(updatedItem.droplab_hidden).toBe(true); - }); + input = document.getElementById('test'); + }); - it('should return droplab_hidden false when item has no hint', () => { - const updatedItem = gl.DropdownUtils.filterHint(input, {}, ''); - expect(updatedItem.droplab_hidden).toBe(false); + it('should filter', () => { + input.value = 'l'; + let updatedItem = gl.DropdownUtils.filterHint(input, { + hint: 'label', }); + expect(updatedItem.droplab_hidden).toBe(false); - it('should allow multiple if item.type is array', () => { - input.value = 'label:~first la'; - const updatedItem = gl.DropdownUtils.filterHint(input, { - hint: 'label', - type: 'array', - }); - expect(updatedItem.droplab_hidden).toBe(false); + input.value = 'o'; + updatedItem = gl.DropdownUtils.filterHint(input, { + hint: 'label', }); + expect(updatedItem.droplab_hidden).toBe(true); + }); - it('should prevent multiple if item.type is not array', () => { - input.value = 'milestone:~first mile'; - let updatedItem = gl.DropdownUtils.filterHint(input, { - hint: 'milestone', - }); - expect(updatedItem.droplab_hidden).toBe(true); + it('should return droplab_hidden false when item has no hint', () => { + const updatedItem = gl.DropdownUtils.filterHint(input, {}, ''); + expect(updatedItem.droplab_hidden).toBe(false); + }); - updatedItem = gl.DropdownUtils.filterHint(input, { - hint: 'milestone', - type: 'string', - }); - expect(updatedItem.droplab_hidden).toBe(true); + it('should allow multiple if item.type is array', () => { + input.value = 'label:~first la'; + const updatedItem = gl.DropdownUtils.filterHint(input, { + hint: 'label', + type: 'array', }); + expect(updatedItem.droplab_hidden).toBe(false); }); - describe('setDataValueIfSelected', () => { - beforeEach(() => { - spyOn(gl.FilteredSearchDropdownManager, 'addWordToInput') - .and.callFake(() => {}); + it('should prevent multiple if item.type is not array', () => { + input.value = 'milestone:~first mile'; + let updatedItem = gl.DropdownUtils.filterHint(input, { + hint: 'milestone', }); + expect(updatedItem.droplab_hidden).toBe(true); - it('calls addWordToInput when dataValue exists', () => { - const selected = { - getAttribute: () => 'value', - }; - - gl.DropdownUtils.setDataValueIfSelected(null, selected); - expect(gl.FilteredSearchDropdownManager.addWordToInput.calls.count()).toEqual(1); + updatedItem = gl.DropdownUtils.filterHint(input, { + hint: 'milestone', + type: 'string', }); + expect(updatedItem.droplab_hidden).toBe(true); + }); + }); - it('returns true when dataValue exists', () => { - const selected = { - getAttribute: () => 'value', - }; + describe('setDataValueIfSelected', () => { + beforeEach(() => { + spyOn(gl.FilteredSearchDropdownManager, 'addWordToInput') + .and.callFake(() => {}); + }); - const result = gl.DropdownUtils.setDataValueIfSelected(null, selected); - expect(result).toBe(true); - }); + it('calls addWordToInput when dataValue exists', () => { + const selected = { + getAttribute: () => 'value', + }; - it('returns false when dataValue does not exist', () => { - const selected = { - getAttribute: () => null, - }; + gl.DropdownUtils.setDataValueIfSelected(null, selected); + expect(gl.FilteredSearchDropdownManager.addWordToInput.calls.count()).toEqual(1); + }); - const result = gl.DropdownUtils.setDataValueIfSelected(null, selected); - expect(result).toBe(false); - }); + it('returns true when dataValue exists', () => { + const selected = { + getAttribute: () => 'value', + }; + + const result = gl.DropdownUtils.setDataValueIfSelected(null, selected); + expect(result).toBe(true); }); - describe('getInputSelectionPosition', () => { - describe('word with trailing spaces', () => { - const value = 'label:none '; + it('returns false when dataValue does not exist', () => { + const selected = { + getAttribute: () => null, + }; + + const result = gl.DropdownUtils.setDataValueIfSelected(null, selected); + expect(result).toBe(false); + }); + }); - it('should return selectionStart when cursor is at the trailing space', () => { - const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ - selectionStart: 11, - value, - }); + describe('getInputSelectionPosition', () => { + describe('word with trailing spaces', () => { + const value = 'label:none '; - expect(left).toBe(11); - expect(right).toBe(11); + it('should return selectionStart when cursor is at the trailing space', () => { + const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ + selectionStart: 11, + value, }); - it('should return input when cursor is at the start of input', () => { - const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ - selectionStart: 0, - value, - }); + expect(left).toBe(11); + expect(right).toBe(11); + }); - expect(left).toBe(0); - expect(right).toBe(10); + it('should return input when cursor is at the start of input', () => { + const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ + selectionStart: 0, + value, }); - it('should return input when cursor is at the middle of input', () => { - const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ - selectionStart: 7, - value, - }); + expect(left).toBe(0); + expect(right).toBe(10); + }); - expect(left).toBe(0); - expect(right).toBe(10); + it('should return input when cursor is at the middle of input', () => { + const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ + selectionStart: 7, + value, }); - it('should return input when cursor is at the end of input', () => { - const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ - selectionStart: 10, - value, - }); + expect(left).toBe(0); + expect(right).toBe(10); + }); - expect(left).toBe(0); - expect(right).toBe(10); + it('should return input when cursor is at the end of input', () => { + const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ + selectionStart: 10, + value, }); - }); - describe('multiple words', () => { - const value = 'label:~"Community Contribution"'; + expect(left).toBe(0); + expect(right).toBe(10); + }); + }); - it('should return input when cursor is after the first word', () => { - const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ - selectionStart: 17, - value, - }); + describe('multiple words', () => { + const value = 'label:~"Community Contribution"'; - expect(left).toBe(0); - expect(right).toBe(31); + it('should return input when cursor is after the first word', () => { + const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ + selectionStart: 17, + value, }); - it('should return input when cursor is before the second word', () => { - const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ - selectionStart: 18, - value, - }); + expect(left).toBe(0); + expect(right).toBe(31); + }); - expect(left).toBe(0); - expect(right).toBe(31); + it('should return input when cursor is before the second word', () => { + const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ + selectionStart: 18, + value, }); - }); - describe('incomplete multiple words', () => { - const value = 'label:~"Community Contribution'; + expect(left).toBe(0); + expect(right).toBe(31); + }); + }); - it('should return entire input when cursor is at the start of input', () => { - const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ - selectionStart: 0, - value, - }); + describe('incomplete multiple words', () => { + const value = 'label:~"Community Contribution'; - expect(left).toBe(0); - expect(right).toBe(30); + it('should return entire input when cursor is at the start of input', () => { + const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ + selectionStart: 0, + value, }); - it('should return entire input when cursor is at the end of input', () => { - const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ - selectionStart: 30, - value, - }); + expect(left).toBe(0); + expect(right).toBe(30); + }); - expect(left).toBe(0); - expect(right).toBe(30); + it('should return entire input when cursor is at the end of input', () => { + const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ + selectionStart: 30, + value, }); + + expect(left).toBe(0); + expect(right).toBe(30); }); }); }); -})(); +}); diff --git a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js index a1da3396d7b..c92a147b937 100644 --- a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js @@ -1,101 +1,99 @@ -require('~/extensions/array'); -require('~/filtered_search/filtered_search_visual_tokens'); -require('~/filtered_search/filtered_search_tokenizer'); -require('~/filtered_search/filtered_search_dropdown_manager'); - -(() => { - describe('Filtered Search Dropdown Manager', () => { - describe('addWordToInput', () => { - function getInputValue() { - return document.querySelector('.filtered-search').value; - } - - function setInputValue(value) { - document.querySelector('.filtered-search').value = value; - } - - beforeEach(() => { - setFixtures(` - <ul class="tokens-container"> - <li class="input-token"> - <input class="filtered-search"> - </li> - </ul> - `); - }); +import '~/extensions/array'; +import '~/filtered_search/filtered_search_visual_tokens'; +import '~/filtered_search/filtered_search_tokenizer'; +import '~/filtered_search/filtered_search_dropdown_manager'; + +describe('Filtered Search Dropdown Manager', () => { + describe('addWordToInput', () => { + function getInputValue() { + return document.querySelector('.filtered-search').value; + } + + function setInputValue(value) { + document.querySelector('.filtered-search').value = value; + } + + beforeEach(() => { + setFixtures(` + <ul class="tokens-container"> + <li class="input-token"> + <input class="filtered-search"> + </li> + </ul> + `); + }); - describe('input has no existing value', () => { - it('should add just tokenName', () => { - gl.FilteredSearchDropdownManager.addWordToInput('milestone'); + describe('input has no existing value', () => { + it('should add just tokenName', () => { + gl.FilteredSearchDropdownManager.addWordToInput('milestone'); - const token = document.querySelector('.tokens-container .js-visual-token'); + 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(''); - }); + expect(token.classList.contains('filtered-search-token')).toEqual(true); + expect(token.querySelector('.name').innerText).toBe('milestone'); + expect(getInputValue()).toBe(''); + }); - it('should add tokenName and tokenValue', () => { - gl.FilteredSearchDropdownManager.addWordToInput('label'); + it('should add tokenName and tokenValue', () => { + gl.FilteredSearchDropdownManager.addWordToInput('label'); - let token = document.querySelector('.tokens-container .js-visual-token'); + 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(''); + expect(token.classList.contains('filtered-search-token')).toEqual(true); + expect(token.querySelector('.name').innerText).toBe('label'); + expect(getInputValue()).toBe(''); - gl.FilteredSearchDropdownManager.addWordToInput('label', 'none'); - // We have to get that reference again - // Because gl.FilteredSearchDropdownManager deletes the previous token - token = document.querySelector('.tokens-container .js-visual-token'); + gl.FilteredSearchDropdownManager.addWordToInput('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(''); - }); + 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(''); }); + }); - describe('input has existing value', () => { - it('should be able to just add tokenName', () => { - setInputValue('a'); - gl.FilteredSearchDropdownManager.addWordToInput('author'); + describe('input has existing value', () => { + it('should be able to just add tokenName', () => { + setInputValue('a'); + gl.FilteredSearchDropdownManager.addWordToInput('author'); - const token = document.querySelector('.tokens-container .js-visual-token'); + 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(''); - }); + expect(token.classList.contains('filtered-search-token')).toEqual(true); + expect(token.querySelector('.name').innerText).toBe('author'); + expect(getInputValue()).toBe(''); + }); - it('should replace tokenValue', () => { - gl.FilteredSearchDropdownManager.addWordToInput('author'); + it('should replace tokenValue', () => { + gl.FilteredSearchDropdownManager.addWordToInput('author'); - setInputValue('roo'); - gl.FilteredSearchDropdownManager.addWordToInput(null, '@root'); + setInputValue('roo'); + gl.FilteredSearchDropdownManager.addWordToInput(null, '@root'); - const token = document.querySelector('.tokens-container .js-visual-token'); + 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(''); - }); + 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', () => { - gl.FilteredSearchDropdownManager.addWordToInput('label'); + it('should add tokenValues containing spaces', () => { + gl.FilteredSearchDropdownManager.addWordToInput('label'); - setInputValue('"test '); - gl.FilteredSearchDropdownManager.addWordToInput('label', '~\'"test me"\''); + setInputValue('"test '); + gl.FilteredSearchDropdownManager.addWordToInput('label', '~\'"test me"\''); - const token = document.querySelector('.tokens-container .js-visual-token'); + 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(''); - }); + 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(''); }); }); }); -})(); +}); diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js index 5f7c05e9014..7c7def3470d 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js @@ -1,276 +1,362 @@ -require('~/lib/utils/url_utility'); -require('~/lib/utils/common_utils'); -require('~/filtered_search/filtered_search_token_keys'); -require('~/filtered_search/filtered_search_tokenizer'); -require('~/filtered_search/filtered_search_dropdown_manager'); -require('~/filtered_search/filtered_search_manager'); -const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper'); - -(() => { - describe('Filtered Search Manager', () => { - let input; - let manager; - let tokensContainer; - const placeholder = 'Search or filter results...'; - - function dispatchBackspaceEvent(element, eventType) { - 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); - } +import * as recentSearchesStoreSrc from '~/filtered_search/stores/recent_searches_store'; +import RecentSearchesService from '~/filtered_search/services/recent_searches_service'; +import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error'; +import '~/lib/utils/url_utility'; +import '~/lib/utils/common_utils'; +import '~/filtered_search/filtered_search_token_keys'; +import '~/filtered_search/filtered_search_tokenizer'; +import '~/filtered_search/filtered_search_dropdown_manager'; +import '~/filtered_search/filtered_search_manager'; +import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper'; + +describe('Filtered Search Manager', () => { + let input; + let manager; + let tokensContainer; + const placeholder = 'Search or filter results...'; + + function dispatchBackspaceEvent(element, eventType) { + 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); + } + + function getVisualTokens() { + return tokensContainer.querySelectorAll('.js-visual-token'); + } + + beforeEach(() => { + setFixtures(` + <div class="filtered-search-box"> + <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, 'loadSearchParamsFromURL').and.callFake(() => {}); + spyOn(gl.FilteredSearchManager.prototype, 'tokenChange').and.callFake(() => {}); + spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {}); + spyOn(gl.FilteredSearchDropdownManager.prototype, 'updateDropdownOffset').and.callFake(() => {}); + spyOn(gl.utils, 'getParameterByName').and.returnValue(null); + spyOn(gl.FilteredSearchVisualTokens, 'unselectTokens').and.callThrough(); + + input = document.querySelector('.filtered-search'); + tokensContainer = document.querySelector('.tokens-container'); + manager = new gl.FilteredSearchManager(); + }); + + afterEach(() => { + manager.cleanup(); + }); + + describe('class constructor', () => { + const isLocalStorageAvailable = 'isLocalStorageAvailable'; + let filteredSearchManager; beforeEach(() => { - setFixtures(` - <div class="filtered-search-input-container"> - <form> - <ul class="tokens-container list-unstyled"> - ${FilteredSearchSpecHelper.createInputHTML(placeholder)} - </ul> - <button class="clear-search" type="button"> - <i class="fa fa-times"></i> - </button> - </form> - </div> - `); + spyOn(RecentSearchesService, 'isAvailable').and.returnValue(isLocalStorageAvailable); + spyOn(recentSearchesStoreSrc, 'default'); - spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {}); - spyOn(gl.FilteredSearchManager.prototype, 'tokenChange').and.callFake(() => {}); - spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {}); - spyOn(gl.FilteredSearchDropdownManager.prototype, 'updateDropdownOffset').and.callFake(() => {}); - spyOn(gl.utils, 'getParameterByName').and.returnValue(null); - spyOn(gl.FilteredSearchVisualTokens, 'unselectTokens').and.callThrough(); + filteredSearchManager = new gl.FilteredSearchManager(); - input = document.querySelector('.filtered-search'); - tokensContainer = document.querySelector('.tokens-container'); - manager = new gl.FilteredSearchManager(); + return filteredSearchManager; }); - afterEach(() => { - manager.cleanup(); + it('should instantiate RecentSearchesStore with isLocalStorageAvailable', () => { + expect(RecentSearchesService.isAvailable).toHaveBeenCalled(); + expect(recentSearchesStoreSrc.default).toHaveBeenCalledWith({ + isLocalStorageAvailable, + }); }); - describe('search', () => { - const defaultParams = '?scope=all&utf8=%E2%9C%93&state=opened'; + it('should not instantiate Flash if an RecentSearchesServiceError is caught', () => { + spyOn(RecentSearchesService.prototype, 'fetch').and.callFake(() => Promise.reject(new RecentSearchesServiceError())); + spyOn(window, 'Flash'); - it('should search with a single word', (done) => { - input.value = 'searchTerm'; + filteredSearchManager = new gl.FilteredSearchManager(); - spyOn(gl.utils, 'visitUrl').and.callFake((url) => { - expect(url).toEqual(`${defaultParams}&search=searchTerm`); - done(); - }); - - manager.search(); - }); + expect(window.Flash).not.toHaveBeenCalled(); + }); + }); - it('should search with multiple words', (done) => { - input.value = 'awesome search terms'; + describe('search', () => { + const defaultParams = '?scope=all&utf8=%E2%9C%93&state=opened'; - spyOn(gl.utils, 'visitUrl').and.callFake((url) => { - expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`); - done(); - }); + it('should search with a single word', (done) => { + input.value = 'searchTerm'; - manager.search(); + spyOn(gl.utils, 'visitUrl').and.callFake((url) => { + expect(url).toEqual(`${defaultParams}&search=searchTerm`); + done(); }); - it('should search with special characters', (done) => { - input.value = '~!@#$%^&*()_+{}:<>,.?/'; + manager.search(); + }); - spyOn(gl.utils, 'visitUrl').and.callFake((url) => { - expect(url).toEqual(`${defaultParams}&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`); - done(); - }); + it('should search with multiple words', (done) => { + input.value = 'awesome search terms'; - manager.search(); + spyOn(gl.utils, 'visitUrl').and.callFake((url) => { + expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`); + done(); }); - it('removes duplicated tokens', (done) => { - tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` - ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} - ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} - `); + manager.search(); + }); - spyOn(gl.utils, 'visitUrl').and.callFake((url) => { - expect(url).toEqual(`${defaultParams}&label_name[]=bug`); - done(); - }); + it('should search with special characters', (done) => { + input.value = '~!@#$%^&*()_+{}:<>,.?/'; - manager.search(); + spyOn(gl.utils, 'visitUrl').and.callFake((url) => { + expect(url).toEqual(`${defaultParams}&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`); + done(); }); + + manager.search(); }); - describe('handleInputPlaceholder', () => { - it('should render placeholder when there is no input', () => { - expect(input.placeholder).toEqual(placeholder); - }); + it('removes duplicated tokens', (done) => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} + `); - it('should not render placeholder when there is input', () => { - input.value = 'test words'; + spyOn(gl.utils, 'visitUrl').and.callFake((url) => { + expect(url).toEqual(`${defaultParams}&label_name[]=bug`); + done(); + }); - const event = new Event('input'); - input.dispatchEvent(event); + manager.search(); + }); + }); - expect(input.placeholder).toEqual(''); - }); + describe('handleInputPlaceholder', () => { + it('should render placeholder when there is no input', () => { + expect(input.placeholder).toEqual(placeholder); + }); - it('should not render placeholder when there are tokens and no input', () => { - tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( - FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'), - ); + it('should not render placeholder when there is input', () => { + input.value = 'test words'; - const event = new Event('input'); - input.dispatchEvent(event); + const event = new Event('input'); + input.dispatchEvent(event); - expect(input.placeholder).toEqual(''); - }); + expect(input.placeholder).toEqual(''); }); - describe('checkForBackspace', () => { - describe('tokens and no input', () => { - beforeEach(() => { - tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( - FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'), - ); - }); + it('should not render placeholder when there are tokens and no input', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'), + ); - it('removes last token', () => { - spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough(); - dispatchBackspaceEvent(input, 'keyup'); + const event = new Event('input'); + input.dispatchEvent(event); - expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).toHaveBeenCalled(); - }); - - it('sets the input', () => { - spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough(); - dispatchDeleteEvent(input, 'keyup'); + expect(input.placeholder).toEqual(''); + }); + }); - expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).toHaveBeenCalled(); - expect(input.value).toEqual('~bug'); - }); + describe('checkForBackspace', () => { + describe('tokens and no input', () => { + beforeEach(() => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'), + ); }); - it('does not remove token or change input when there is existing input', () => { + it('removes last token', () => { spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough(); - spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough(); + dispatchBackspaceEvent(input, 'keyup'); - input.value = 'text'; + expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).toHaveBeenCalled(); + }); + + it('sets the input', () => { + spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough(); dispatchDeleteEvent(input, 'keyup'); - expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).not.toHaveBeenCalled(); - expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).not.toHaveBeenCalled(); - expect(input.value).toEqual('text'); + expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).toHaveBeenCalled(); + expect(input.value).toEqual('~bug'); }); }); - describe('removeSelectedToken', () => { - function getVisualTokens() { - return tokensContainer.querySelectorAll('.js-visual-token'); - } + 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('removeToken', () => { + it('removes token even when it is already selected', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true), + ); + + tokensContainer.querySelector('.js-visual-token .remove-token').click(); + expect(tokensContainer.querySelector('.js-visual-token')).toEqual(null); + }); + describe('unselected token', () => { beforeEach(() => { + spyOn(gl.FilteredSearchManager.prototype, 'removeSelectedToken').and.callThrough(); + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( - FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true), + FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none'), ); + tokensContainer.querySelector('.js-visual-token .remove-token').click(); }); - it('removes selected token when the backspace key is pressed', () => { - expect(getVisualTokens().length).toEqual(1); - - dispatchBackspaceEvent(document, 'keydown'); + it('removes token when remove button is selected', () => { + expect(tokensContainer.querySelector('.js-visual-token')).toEqual(null); + }); - expect(getVisualTokens().length).toEqual(0); + it('calls removeSelectedToken', () => { + expect(manager.removeSelectedToken).toHaveBeenCalled(); }); + }); + }); - it('removes selected token when the delete key is pressed', () => { - expect(getVisualTokens().length).toEqual(1); + describe('removeSelectedTokenKeydown', () => { + beforeEach(() => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true), + ); + }); - dispatchDeleteEvent(document, 'keydown'); + it('removes selected token when the backspace key is pressed', () => { + expect(getVisualTokens().length).toEqual(1); - expect(getVisualTokens().length).toEqual(0); - }); + dispatchBackspaceEvent(document, 'keydown'); - it('updates the input placeholder after removal', () => { - manager.handleInputPlaceholder(); + expect(getVisualTokens().length).toEqual(0); + }); - expect(input.placeholder).toEqual(''); - expect(getVisualTokens().length).toEqual(1); + it('removes selected token when the delete key is pressed', () => { + expect(getVisualTokens().length).toEqual(1); - dispatchBackspaceEvent(document, 'keydown'); + dispatchDeleteEvent(document, 'keydown'); - expect(input.placeholder).not.toEqual(''); - expect(getVisualTokens().length).toEqual(0); - }); + expect(getVisualTokens().length).toEqual(0); + }); - it('updates the clear button after removal', () => { - manager.toggleClearSearchButton(); + it('updates the input placeholder after removal', () => { + manager.handleInputPlaceholder(); - const clearButton = document.querySelector('.clear-search'); + expect(input.placeholder).toEqual(''); + expect(getVisualTokens().length).toEqual(1); - expect(clearButton.classList.contains('hidden')).toEqual(false); - expect(getVisualTokens().length).toEqual(1); + dispatchBackspaceEvent(document, 'keydown'); - dispatchBackspaceEvent(document, 'keydown'); + expect(input.placeholder).not.toEqual(''); + expect(getVisualTokens().length).toEqual(0); + }); - expect(clearButton.classList.contains('hidden')).toEqual(true); - 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')} - `); - }); + describe('removeSelectedToken', () => { + beforeEach(() => { + spyOn(gl.FilteredSearchVisualTokens, 'removeSelectedToken').and.callThrough(); + spyOn(gl.FilteredSearchManager.prototype, 'handleInputPlaceholder').and.callThrough(); + spyOn(gl.FilteredSearchManager.prototype, 'toggleClearSearchButton').and.callThrough(); + manager.removeSelectedToken(); + }); - it('unselects token when input is clicked', () => { - const selectedToken = tokensContainer.querySelector('.js-visual-token .selected'); + it('calls FilteredSearchVisualTokens.removeSelectedToken', () => { + expect(gl.FilteredSearchVisualTokens.removeSelectedToken).toHaveBeenCalled(); + }); - expect(selectedToken.classList.contains('selected')).toEqual(true); - expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled(); + it('calls handleInputPlaceholder', () => { + expect(manager.handleInputPlaceholder).toHaveBeenCalled(); + }); - // Click directly on input attached to document - // so that the click event will propagate properly - document.querySelector('.filtered-search').click(); + it('calls toggleClearSearchButton', () => { + expect(manager.toggleClearSearchButton).toHaveBeenCalled(); + }); - expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled(); - expect(selectedToken.classList.contains('selected')).toEqual(false); - }); + it('calls update dropdown offset', () => { + expect(manager.dropdownManager.updateDropdownOffset).toHaveBeenCalled(); + }); + }); - it('unselects token when document.body is clicked', () => { - const selectedToken = tokensContainer.querySelector('.js-visual-token .selected'); + describe('unselects token', () => { + beforeEach(() => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug', true)} + ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')} + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~awesome')} + `); + }); - expect(selectedToken.classList.contains('selected')).toEqual(true); - expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled(); + it('unselects token when input is clicked', () => { + const selectedToken = tokensContainer.querySelector('.js-visual-token .selected'); - document.body.click(); + expect(selectedToken.classList.contains('selected')).toEqual(true); + expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled(); - expect(selectedToken.classList.contains('selected')).toEqual(false); - expect(gl.FilteredSearchVisualTokens.unselectTokens).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); }); - describe('toggleInputContainerFocus', () => { - it('toggles on focus', () => { - input.focus(); - expect(document.querySelector('.filtered-search-input-container').classList.contains('focus')).toEqual(true); - }); + it('unselects token when document.body is clicked', () => { + const selectedToken = tokensContainer.querySelector('.js-visual-token .selected'); - it('toggles on blur', () => { - input.blur(); - expect(document.querySelector('.filtered-search-input-container').classList.contains('focus')).toEqual(false); - }); + 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(); + }); + }); + + describe('toggleInputContainerFocus', () => { + it('toggles on focus', () => { + input.focus(); + expect(document.querySelector('.filtered-search-box').classList.contains('focus')).toEqual(true); + }); + + it('toggles on blur', () => { + input.blur(); + expect(document.querySelector('.filtered-search-box').classList.contains('focus')).toEqual(false); }); }); -})(); +}); diff --git a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js index cf409a7e509..1a7631994b4 100644 --- a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js @@ -1,110 +1,108 @@ -require('~/extensions/array'); -require('~/filtered_search/filtered_search_token_keys'); +import '~/extensions/array'; +import '~/filtered_search/filtered_search_token_keys'; -(() => { - describe('Filtered Search Token Keys', () => { - describe('get', () => { - let tokenKeys; - - beforeEach(() => { - tokenKeys = gl.FilteredSearchTokenKeys.get(); - }); - - it('should return tokenKeys', () => { - expect(tokenKeys !== null).toBe(true); - }); - - it('should return tokenKeys as an array', () => { - expect(tokenKeys instanceof Array).toBe(true); - }); - }); - - describe('getConditions', () => { - let conditions; - - beforeEach(() => { - conditions = gl.FilteredSearchTokenKeys.getConditions(); - }); - - it('should return conditions', () => { - expect(conditions !== null).toBe(true); - }); - - it('should return conditions as an array', () => { - expect(conditions instanceof Array).toBe(true); - }); - }); - - describe('searchByKey', () => { - it('should return null when key not found', () => { - const tokenKey = gl.FilteredSearchTokenKeys.searchByKey('notakey'); - expect(tokenKey === null).toBe(true); - }); - - it('should return tokenKey when found by key', () => { - const tokenKeys = gl.FilteredSearchTokenKeys.get(); - const result = gl.FilteredSearchTokenKeys.searchByKey(tokenKeys[0].key); - expect(result).toEqual(tokenKeys[0]); - }); - }); - - describe('searchBySymbol', () => { - it('should return null when symbol not found', () => { - const tokenKey = gl.FilteredSearchTokenKeys.searchBySymbol('notasymbol'); - expect(tokenKey === null).toBe(true); - }); - - it('should return tokenKey when found by symbol', () => { - const tokenKeys = gl.FilteredSearchTokenKeys.get(); - const result = gl.FilteredSearchTokenKeys.searchBySymbol(tokenKeys[0].symbol); - expect(result).toEqual(tokenKeys[0]); - }); - }); - - describe('searchByKeyParam', () => { - it('should return null when key param not found', () => { - const tokenKey = gl.FilteredSearchTokenKeys.searchByKeyParam('notakeyparam'); - expect(tokenKey === null).toBe(true); - }); - - it('should return tokenKey when found by key param', () => { - const tokenKeys = gl.FilteredSearchTokenKeys.get(); - const result = gl.FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`); - expect(result).toEqual(tokenKeys[0]); - }); - - it('should return alternative tokenKey when found by key param', () => { - const tokenKeys = gl.FilteredSearchTokenKeys.getAlternatives(); - const result = gl.FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`); - expect(result).toEqual(tokenKeys[0]); - }); - }); - - describe('searchByConditionUrl', () => { - it('should return null when condition url not found', () => { - const condition = gl.FilteredSearchTokenKeys.searchByConditionUrl(null); - expect(condition === null).toBe(true); - }); - - it('should return condition when found by url', () => { - const conditions = gl.FilteredSearchTokenKeys.getConditions(); - const result = gl.FilteredSearchTokenKeys.searchByConditionUrl(conditions[0].url); - expect(result).toBe(conditions[0]); - }); - }); - - describe('searchByConditionKeyValue', () => { - it('should return null when condition tokenKey and value not found', () => { - const condition = gl.FilteredSearchTokenKeys.searchByConditionKeyValue(null, null); - expect(condition === null).toBe(true); - }); - - it('should return condition when found by tokenKey and value', () => { - const conditions = gl.FilteredSearchTokenKeys.getConditions(); - const result = gl.FilteredSearchTokenKeys - .searchByConditionKeyValue(conditions[0].tokenKey, conditions[0].value); - expect(result).toEqual(conditions[0]); - }); +describe('Filtered Search Token Keys', () => { + describe('get', () => { + let tokenKeys; + + beforeEach(() => { + tokenKeys = gl.FilteredSearchTokenKeys.get(); + }); + + it('should return tokenKeys', () => { + expect(tokenKeys !== null).toBe(true); + }); + + it('should return tokenKeys as an array', () => { + expect(tokenKeys instanceof Array).toBe(true); + }); + }); + + describe('getConditions', () => { + let conditions; + + beforeEach(() => { + conditions = gl.FilteredSearchTokenKeys.getConditions(); + }); + + it('should return conditions', () => { + expect(conditions !== null).toBe(true); + }); + + it('should return conditions as an array', () => { + expect(conditions instanceof Array).toBe(true); + }); + }); + + describe('searchByKey', () => { + it('should return null when key not found', () => { + const tokenKey = gl.FilteredSearchTokenKeys.searchByKey('notakey'); + expect(tokenKey === null).toBe(true); + }); + + it('should return tokenKey when found by key', () => { + const tokenKeys = gl.FilteredSearchTokenKeys.get(); + const result = gl.FilteredSearchTokenKeys.searchByKey(tokenKeys[0].key); + expect(result).toEqual(tokenKeys[0]); + }); + }); + + describe('searchBySymbol', () => { + it('should return null when symbol not found', () => { + const tokenKey = gl.FilteredSearchTokenKeys.searchBySymbol('notasymbol'); + expect(tokenKey === null).toBe(true); + }); + + it('should return tokenKey when found by symbol', () => { + const tokenKeys = gl.FilteredSearchTokenKeys.get(); + const result = gl.FilteredSearchTokenKeys.searchBySymbol(tokenKeys[0].symbol); + expect(result).toEqual(tokenKeys[0]); + }); + }); + + describe('searchByKeyParam', () => { + it('should return null when key param not found', () => { + const tokenKey = gl.FilteredSearchTokenKeys.searchByKeyParam('notakeyparam'); + expect(tokenKey === null).toBe(true); + }); + + it('should return tokenKey when found by key param', () => { + const tokenKeys = gl.FilteredSearchTokenKeys.get(); + const result = gl.FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`); + expect(result).toEqual(tokenKeys[0]); + }); + + it('should return alternative tokenKey when found by key param', () => { + const tokenKeys = gl.FilteredSearchTokenKeys.getAlternatives(); + const result = gl.FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`); + expect(result).toEqual(tokenKeys[0]); + }); + }); + + describe('searchByConditionUrl', () => { + it('should return null when condition url not found', () => { + const condition = gl.FilteredSearchTokenKeys.searchByConditionUrl(null); + expect(condition === null).toBe(true); + }); + + it('should return condition when found by url', () => { + const conditions = gl.FilteredSearchTokenKeys.getConditions(); + const result = gl.FilteredSearchTokenKeys.searchByConditionUrl(conditions[0].url); + expect(result).toBe(conditions[0]); + }); + }); + + describe('searchByConditionKeyValue', () => { + it('should return null when condition tokenKey and value not found', () => { + const condition = gl.FilteredSearchTokenKeys.searchByConditionKeyValue(null, null); + expect(condition === null).toBe(true); + }); + + it('should return condition when found by tokenKey and value', () => { + const conditions = gl.FilteredSearchTokenKeys.getConditions(); + const result = gl.FilteredSearchTokenKeys + .searchByConditionKeyValue(conditions[0].tokenKey, conditions[0].value); + expect(result).toEqual(conditions[0]); }); }); -})(); +}); diff --git a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js index cabbc694ec4..9561580c839 100644 --- a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js @@ -1,135 +1,133 @@ -require('~/extensions/array'); -require('~/filtered_search/filtered_search_token_keys'); -require('~/filtered_search/filtered_search_tokenizer'); - -(() => { - describe('Filtered Search Tokenizer', () => { - describe('processTokens', () => { - it('returns for input containing only search value', () => { - const results = gl.FilteredSearchTokenizer.processTokens('searchTerm'); - expect(results.searchToken).toBe('searchTerm'); - expect(results.tokens.length).toBe(0); - expect(results.lastToken).toBe(results.searchToken); - }); - - it('returns for input containing only tokens', () => { - const results = gl.FilteredSearchTokenizer - .processTokens('author:@root label:~"Very Important" milestone:%v1.0 assignee:none'); - expect(results.searchToken).toBe(''); - expect(results.tokens.length).toBe(4); - expect(results.tokens[3]).toBe(results.lastToken); - - expect(results.tokens[0].key).toBe('author'); - expect(results.tokens[0].value).toBe('root'); - expect(results.tokens[0].symbol).toBe('@'); - - expect(results.tokens[1].key).toBe('label'); - expect(results.tokens[1].value).toBe('"Very Important"'); - expect(results.tokens[1].symbol).toBe('~'); - - expect(results.tokens[2].key).toBe('milestone'); - expect(results.tokens[2].value).toBe('v1.0'); - expect(results.tokens[2].symbol).toBe('%'); - - expect(results.tokens[3].key).toBe('assignee'); - expect(results.tokens[3].value).toBe('none'); - expect(results.tokens[3].symbol).toBe(''); - }); - - it('returns for input starting with search value and ending with tokens', () => { - const results = gl.FilteredSearchTokenizer - .processTokens('searchTerm anotherSearchTerm milestone:none'); - expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); - expect(results.tokens.length).toBe(1); - expect(results.tokens[0]).toBe(results.lastToken); - expect(results.tokens[0].key).toBe('milestone'); - expect(results.tokens[0].value).toBe('none'); - expect(results.tokens[0].symbol).toBe(''); - }); - - it('returns for input starting with tokens and ending with search value', () => { - const results = gl.FilteredSearchTokenizer - .processTokens('assignee:@user searchTerm'); - - expect(results.searchToken).toBe('searchTerm'); - expect(results.tokens.length).toBe(1); - expect(results.tokens[0].key).toBe('assignee'); - expect(results.tokens[0].value).toBe('user'); - expect(results.tokens[0].symbol).toBe('@'); - expect(results.lastToken).toBe(results.searchToken); - }); - - it('returns for input containing search value wrapped between tokens', () => { - const results = gl.FilteredSearchTokenizer - .processTokens('author:@root label:~"Won\'t fix" searchTerm anotherSearchTerm milestone:none'); - - expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); - expect(results.tokens.length).toBe(3); - expect(results.tokens[2]).toBe(results.lastToken); - - expect(results.tokens[0].key).toBe('author'); - expect(results.tokens[0].value).toBe('root'); - expect(results.tokens[0].symbol).toBe('@'); - - expect(results.tokens[1].key).toBe('label'); - expect(results.tokens[1].value).toBe('"Won\'t fix"'); - expect(results.tokens[1].symbol).toBe('~'); - - expect(results.tokens[2].key).toBe('milestone'); - expect(results.tokens[2].value).toBe('none'); - expect(results.tokens[2].symbol).toBe(''); - }); - - it('returns for input containing search value in between tokens', () => { - const results = gl.FilteredSearchTokenizer - .processTokens('author:@root searchTerm assignee:none anotherSearchTerm label:~Doing'); - expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); - expect(results.tokens.length).toBe(3); - expect(results.tokens[2]).toBe(results.lastToken); - - expect(results.tokens[0].key).toBe('author'); - expect(results.tokens[0].value).toBe('root'); - expect(results.tokens[0].symbol).toBe('@'); - - expect(results.tokens[1].key).toBe('assignee'); - expect(results.tokens[1].value).toBe('none'); - expect(results.tokens[1].symbol).toBe(''); - - expect(results.tokens[2].key).toBe('label'); - expect(results.tokens[2].value).toBe('Doing'); - expect(results.tokens[2].symbol).toBe('~'); - }); - - it('returns search value for invalid tokens', () => { - const results = gl.FilteredSearchTokenizer.processTokens('fake:token'); - expect(results.lastToken).toBe('fake:token'); - expect(results.searchToken).toBe('fake:token'); - expect(results.tokens.length).toEqual(0); - }); - - it('returns search value and token for mix of valid and invalid tokens', () => { - const results = gl.FilteredSearchTokenizer.processTokens('label:real fake:token'); - expect(results.tokens.length).toEqual(1); - expect(results.tokens[0].key).toBe('label'); - expect(results.tokens[0].value).toBe('real'); - expect(results.tokens[0].symbol).toBe(''); - expect(results.lastToken).toBe('fake:token'); - expect(results.searchToken).toBe('fake:token'); - }); - - it('returns search value for invalid symbols', () => { - const results = gl.FilteredSearchTokenizer.processTokens('std::includes'); - expect(results.lastToken).toBe('std::includes'); - expect(results.searchToken).toBe('std::includes'); - }); - - it('removes duplicated values', () => { - const results = gl.FilteredSearchTokenizer.processTokens('label:~foo label:~foo'); - expect(results.tokens.length).toBe(1); - expect(results.tokens[0].key).toBe('label'); - expect(results.tokens[0].value).toBe('foo'); - expect(results.tokens[0].symbol).toBe('~'); - }); +import '~/extensions/array'; +import '~/filtered_search/filtered_search_token_keys'; +import '~/filtered_search/filtered_search_tokenizer'; + +describe('Filtered Search Tokenizer', () => { + describe('processTokens', () => { + it('returns for input containing only search value', () => { + const results = gl.FilteredSearchTokenizer.processTokens('searchTerm'); + expect(results.searchToken).toBe('searchTerm'); + expect(results.tokens.length).toBe(0); + expect(results.lastToken).toBe(results.searchToken); + }); + + it('returns for input containing only tokens', () => { + const results = gl.FilteredSearchTokenizer + .processTokens('author:@root label:~"Very Important" milestone:%v1.0 assignee:none'); + expect(results.searchToken).toBe(''); + expect(results.tokens.length).toBe(4); + expect(results.tokens[3]).toBe(results.lastToken); + + expect(results.tokens[0].key).toBe('author'); + expect(results.tokens[0].value).toBe('root'); + expect(results.tokens[0].symbol).toBe('@'); + + expect(results.tokens[1].key).toBe('label'); + expect(results.tokens[1].value).toBe('"Very Important"'); + expect(results.tokens[1].symbol).toBe('~'); + + expect(results.tokens[2].key).toBe('milestone'); + expect(results.tokens[2].value).toBe('v1.0'); + expect(results.tokens[2].symbol).toBe('%'); + + expect(results.tokens[3].key).toBe('assignee'); + expect(results.tokens[3].value).toBe('none'); + expect(results.tokens[3].symbol).toBe(''); + }); + + it('returns for input starting with search value and ending with tokens', () => { + const results = gl.FilteredSearchTokenizer + .processTokens('searchTerm anotherSearchTerm milestone:none'); + expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); + expect(results.tokens.length).toBe(1); + expect(results.tokens[0]).toBe(results.lastToken); + expect(results.tokens[0].key).toBe('milestone'); + expect(results.tokens[0].value).toBe('none'); + expect(results.tokens[0].symbol).toBe(''); + }); + + it('returns for input starting with tokens and ending with search value', () => { + const results = gl.FilteredSearchTokenizer + .processTokens('assignee:@user searchTerm'); + + expect(results.searchToken).toBe('searchTerm'); + expect(results.tokens.length).toBe(1); + expect(results.tokens[0].key).toBe('assignee'); + expect(results.tokens[0].value).toBe('user'); + expect(results.tokens[0].symbol).toBe('@'); + expect(results.lastToken).toBe(results.searchToken); + }); + + it('returns for input containing search value wrapped between tokens', () => { + const results = gl.FilteredSearchTokenizer + .processTokens('author:@root label:~"Won\'t fix" searchTerm anotherSearchTerm milestone:none'); + + expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); + expect(results.tokens.length).toBe(3); + expect(results.tokens[2]).toBe(results.lastToken); + + expect(results.tokens[0].key).toBe('author'); + expect(results.tokens[0].value).toBe('root'); + expect(results.tokens[0].symbol).toBe('@'); + + expect(results.tokens[1].key).toBe('label'); + expect(results.tokens[1].value).toBe('"Won\'t fix"'); + expect(results.tokens[1].symbol).toBe('~'); + + expect(results.tokens[2].key).toBe('milestone'); + expect(results.tokens[2].value).toBe('none'); + expect(results.tokens[2].symbol).toBe(''); + }); + + it('returns for input containing search value in between tokens', () => { + const results = gl.FilteredSearchTokenizer + .processTokens('author:@root searchTerm assignee:none anotherSearchTerm label:~Doing'); + expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); + expect(results.tokens.length).toBe(3); + expect(results.tokens[2]).toBe(results.lastToken); + + expect(results.tokens[0].key).toBe('author'); + expect(results.tokens[0].value).toBe('root'); + expect(results.tokens[0].symbol).toBe('@'); + + expect(results.tokens[1].key).toBe('assignee'); + expect(results.tokens[1].value).toBe('none'); + expect(results.tokens[1].symbol).toBe(''); + + expect(results.tokens[2].key).toBe('label'); + expect(results.tokens[2].value).toBe('Doing'); + expect(results.tokens[2].symbol).toBe('~'); + }); + + it('returns search value for invalid tokens', () => { + const results = gl.FilteredSearchTokenizer.processTokens('fake:token'); + expect(results.lastToken).toBe('fake:token'); + expect(results.searchToken).toBe('fake:token'); + expect(results.tokens.length).toEqual(0); + }); + + it('returns search value and token for mix of valid and invalid tokens', () => { + const results = gl.FilteredSearchTokenizer.processTokens('label:real fake:token'); + expect(results.tokens.length).toEqual(1); + expect(results.tokens[0].key).toBe('label'); + expect(results.tokens[0].value).toBe('real'); + expect(results.tokens[0].symbol).toBe(''); + expect(results.lastToken).toBe('fake:token'); + expect(results.searchToken).toBe('fake:token'); + }); + + it('returns search value for invalid symbols', () => { + const results = gl.FilteredSearchTokenizer.processTokens('std::includes'); + expect(results.lastToken).toBe('std::includes'); + expect(results.searchToken).toBe('std::includes'); + }); + + it('removes duplicated values', () => { + const results = gl.FilteredSearchTokenizer.processTokens('label:~foo label:~foo'); + expect(results.tokens.length).toBe(1); + expect(results.tokens[0].key).toBe('label'); + expect(results.tokens[0].value).toBe('foo'); + expect(results.tokens[0].symbol).toBe('~'); }); }); -})(); +}); diff --git a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js index bbda1476fed..c5fa2b17106 100644 --- a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js @@ -1,5 +1,7 @@ -require('~/filtered_search/filtered_search_visual_tokens'); -const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper'); +import AjaxCache from '~/lib/utils/ajax_cache'; + +import '~/filtered_search/filtered_search_visual_tokens'; +import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper'; describe('Filtered Search Visual Tokens', () => { let tokensContainer; @@ -214,8 +216,12 @@ describe('Filtered Search Visual Tokens', () => { expect(tokenElement.querySelector('.name')).toEqual(jasmine.anything()); }); + it('contains value container div', () => { + expect(tokenElement.querySelector('.value-container')).toEqual(jasmine.anything()); + }); + it('contains value div', () => { - expect(tokenElement.querySelector('.value')).toEqual(jasmine.anything()); + expect(tokenElement.querySelector('.value-container .value')).toEqual(jasmine.anything()); }); it('contains selectable class', () => { @@ -225,6 +231,16 @@ describe('Filtered Search Visual Tokens', () => { it('contains button role', () => { expect(tokenElement.getAttribute('role')).toEqual('button'); }); + + describe('remove token', () => { + it('contains remove-token button', () => { + expect(tokenElement.querySelector('.value-container .remove-token')).toEqual(jasmine.anything()); + }); + + it('contains fa-close icon', () => { + expect(tokenElement.querySelector('.remove-token .fa-close')).toEqual(jasmine.anything()); + }); + }); }); describe('addVisualTokenElement', () => { @@ -597,4 +613,103 @@ describe('Filtered Search Visual Tokens', () => { expect(token.querySelector('.value').innerText).toEqual('~bug'); }); }); + + describe('renderVisualTokenValue', () => { + let searchTokens; + + beforeEach(() => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')} + ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search')} + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'upcoming')} + `); + + searchTokens = document.querySelectorAll('.filtered-search-token'); + }); + + it('renders a token value element', () => { + spyOn(gl.FilteredSearchVisualTokens, 'updateLabelTokenColor'); + const updateLabelTokenColorSpy = gl.FilteredSearchVisualTokens.updateLabelTokenColor; + + expect(searchTokens.length).toBe(2); + Array.prototype.forEach.call(searchTokens, (token) => { + updateLabelTokenColorSpy.calls.reset(); + + const tokenName = token.querySelector('.name').innerText; + const tokenValue = 'new value'; + gl.FilteredSearchVisualTokens.renderVisualTokenValue(token, tokenName, tokenValue); + + const tokenValueElement = token.querySelector('.value'); + expect(tokenValueElement.innerText).toBe(tokenValue); + + if (tokenName.toLowerCase() === 'label') { + const tokenValueContainer = token.querySelector('.value-container'); + expect(updateLabelTokenColorSpy.calls.count()).toBe(1); + const expectedArgs = [tokenValueContainer, tokenValue]; + expect(updateLabelTokenColorSpy.calls.argsFor(0)).toEqual(expectedArgs); + } else { + expect(updateLabelTokenColorSpy.calls.count()).toBe(0); + } + }); + }); + }); + + describe('updateLabelTokenColor', () => { + const jsonFixtureName = 'labels/project_labels.json'; + const dummyEndpoint = '/dummy/endpoint'; + + preloadFixtures(jsonFixtureName); + const labelData = getJSONFixture(jsonFixtureName); + const findLabel = tokenValue => labelData.find( + label => tokenValue === `~${gl.DropdownUtils.getEscapedText(label.title)}`, + ); + + const bugLabelToken = FilteredSearchSpecHelper.createFilterVisualToken('label', '~bug'); + const missingLabelToken = FilteredSearchSpecHelper.createFilterVisualToken('label', '~doesnotexist'); + const spaceLabelToken = FilteredSearchSpecHelper.createFilterVisualToken('label', '~"some space"'); + + const parseColor = (color) => { + const dummyElement = document.createElement('div'); + dummyElement.style.color = color; + return dummyElement.style.color; + }; + + beforeEach(() => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` + ${bugLabelToken.outerHTML} + ${missingLabelToken.outerHTML} + ${spaceLabelToken.outerHTML} + `); + + const filteredSearchInput = document.querySelector('.filtered-search'); + filteredSearchInput.dataset.baseEndpoint = dummyEndpoint; + + AjaxCache.internalStorage = { }; + AjaxCache.internalStorage[`${dummyEndpoint}/labels.json`] = labelData; + }); + + const testCase = (token, done) => { + const tokenValueContainer = token.querySelector('.value-container'); + const tokenValue = token.querySelector('.value').innerText; + const label = findLabel(tokenValue); + + gl.FilteredSearchVisualTokens.updateLabelTokenColor(tokenValueContainer, tokenValue) + .then(() => { + if (label) { + expect(tokenValueContainer.getAttribute('style')).not.toBe(null); + expect(tokenValueContainer.style.backgroundColor).toBe(parseColor(label.color)); + expect(tokenValueContainer.style.color).toBe(parseColor(label.text_color)); + } else { + expect(token).toBe(missingLabelToken); + expect(tokenValueContainer.getAttribute('style')).toBe(null); + } + }) + .then(done) + .catch(fail); + }; + + it('updates the color of a label token', done => testCase(bugLabelToken, done)); + it('updates the color of a label token with spaces', done => testCase(spaceLabelToken, done)); + it('does not change color of a missing label', done => testCase(missingLabelToken, done)); + }); }); diff --git a/spec/javascripts/filtered_search/recent_searches_root_spec.js b/spec/javascripts/filtered_search/recent_searches_root_spec.js new file mode 100644 index 00000000000..d8ba6de5f45 --- /dev/null +++ b/spec/javascripts/filtered_search/recent_searches_root_spec.js @@ -0,0 +1,31 @@ +import RecentSearchesRoot from '~/filtered_search/recent_searches_root'; +import * as vueSrc from 'vue'; + +describe('RecentSearchesRoot', () => { + describe('render', () => { + let recentSearchesRoot; + let data; + let template; + + beforeEach(() => { + recentSearchesRoot = { + store: { + state: 'state', + }, + }; + + spyOn(vueSrc, 'default').and.callFake((options) => { + data = options.data; + template = options.template; + }); + + RecentSearchesRoot.prototype.render.call(recentSearchesRoot); + }); + + it('should instantiate Vue', () => { + expect(vueSrc.default).toHaveBeenCalled(); + expect(data()).toBe(recentSearchesRoot.store.state); + expect(template).toContain(':is-local-storage-available="isLocalStorageAvailable"'); + }); + }); +}); diff --git a/spec/javascripts/filtered_search/services/recent_searches_service_error_spec.js b/spec/javascripts/filtered_search/services/recent_searches_service_error_spec.js new file mode 100644 index 00000000000..ea7c146fa4f --- /dev/null +++ b/spec/javascripts/filtered_search/services/recent_searches_service_error_spec.js @@ -0,0 +1,18 @@ +import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error'; + +describe('RecentSearchesServiceError', () => { + let recentSearchesServiceError; + + beforeEach(() => { + recentSearchesServiceError = new RecentSearchesServiceError(); + }); + + it('instantiates an instance of RecentSearchesServiceError and not an Error', () => { + expect(recentSearchesServiceError).toEqual(jasmine.any(RecentSearchesServiceError)); + expect(recentSearchesServiceError.name).toBe('RecentSearchesServiceError'); + }); + + it('should set a default message', () => { + expect(recentSearchesServiceError.message).toBe('Recent Searches Service is unavailable'); + }); +}); diff --git a/spec/javascripts/filtered_search/services/recent_searches_service_spec.js b/spec/javascripts/filtered_search/services/recent_searches_service_spec.js new file mode 100644 index 00000000000..31fa478804a --- /dev/null +++ b/spec/javascripts/filtered_search/services/recent_searches_service_spec.js @@ -0,0 +1,147 @@ +/* eslint-disable promise/catch-or-return */ + +import RecentSearchesService from '~/filtered_search/services/recent_searches_service'; +import AccessorUtilities from '~/lib/utils/accessor'; + +describe('RecentSearchesService', () => { + let service; + + beforeEach(() => { + service = new RecentSearchesService(); + window.localStorage.removeItem(service.localStorageKey); + }); + + describe('fetch', () => { + beforeEach(() => { + spyOn(RecentSearchesService, 'isAvailable').and.returnValue(true); + }); + + it('should default to empty array', (done) => { + const fetchItemsPromise = service.fetch(); + + fetchItemsPromise + .then((items) => { + expect(items).toEqual([]); + done(); + }) + .catch((err) => { + done.fail('Shouldn\'t reject with empty localStorage key', err); + }); + }); + + it('should reject when unable to parse', (done) => { + window.localStorage.setItem(service.localStorageKey, 'fail'); + const fetchItemsPromise = service.fetch(); + + fetchItemsPromise + .catch((error) => { + expect(error).toEqual(jasmine.any(SyntaxError)); + done(); + }); + }); + + it('should reject when service is unavailable', (done) => { + RecentSearchesService.isAvailable.and.returnValue(false); + + service.fetch().catch((error) => { + expect(error).toEqual(jasmine.any(Error)); + done(); + }); + }); + + it('should return items from localStorage', (done) => { + window.localStorage.setItem(service.localStorageKey, '["foo", "bar"]'); + const fetchItemsPromise = service.fetch(); + + fetchItemsPromise + .then((items) => { + expect(items).toEqual(['foo', 'bar']); + done(); + }); + }); + + describe('if .isAvailable returns `false`', () => { + beforeEach(() => { + RecentSearchesService.isAvailable.and.returnValue(false); + + spyOn(window.localStorage, 'getItem'); + + RecentSearchesService.prototype.fetch(); + }); + + it('should not call .getItem', () => { + expect(window.localStorage.getItem).not.toHaveBeenCalled(); + }); + }); + }); + + describe('setRecentSearches', () => { + beforeEach(() => { + spyOn(RecentSearchesService, 'isAvailable').and.returnValue(true); + }); + + it('should save things in localStorage', () => { + const items = ['foo', 'bar']; + service.save(items); + const newLocalStorageValue = window.localStorage.getItem(service.localStorageKey); + expect(JSON.parse(newLocalStorageValue)).toEqual(items); + }); + }); + + describe('save', () => { + beforeEach(() => { + spyOn(window.localStorage, 'setItem'); + spyOn(RecentSearchesService, 'isAvailable'); + }); + + describe('if .isAvailable returns `true`', () => { + const searchesString = 'searchesString'; + const localStorageKey = 'localStorageKey'; + const recentSearchesService = { + localStorageKey, + }; + + beforeEach(() => { + RecentSearchesService.isAvailable.and.returnValue(true); + + spyOn(JSON, 'stringify').and.returnValue(searchesString); + + RecentSearchesService.prototype.save.call(recentSearchesService); + }); + + it('should call .setItem', () => { + expect(window.localStorage.setItem).toHaveBeenCalledWith(localStorageKey, searchesString); + }); + }); + + describe('if .isAvailable returns `false`', () => { + beforeEach(() => { + RecentSearchesService.isAvailable.and.returnValue(false); + + RecentSearchesService.prototype.save(); + }); + + it('should not call .setItem', () => { + expect(window.localStorage.setItem).not.toHaveBeenCalled(); + }); + }); + }); + + describe('isAvailable', () => { + let isAvailable; + + beforeEach(() => { + spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').and.callThrough(); + + isAvailable = RecentSearchesService.isAvailable(); + }); + + it('should call .isLocalStorageAccessSafe', () => { + expect(AccessorUtilities.isLocalStorageAccessSafe).toHaveBeenCalled(); + }); + + it('should return a boolean', () => { + expect(typeof isAvailable).toBe('boolean'); + }); + }); +}); diff --git a/spec/javascripts/filtered_search/stores/recent_searches_store_spec.js b/spec/javascripts/filtered_search/stores/recent_searches_store_spec.js new file mode 100644 index 00000000000..1eebc6f2367 --- /dev/null +++ b/spec/javascripts/filtered_search/stores/recent_searches_store_spec.js @@ -0,0 +1,59 @@ +import RecentSearchesStore from '~/filtered_search/stores/recent_searches_store'; + +describe('RecentSearchesStore', () => { + let store; + + beforeEach(() => { + store = new RecentSearchesStore(); + }); + + describe('addRecentSearch', () => { + it('should add to the front of the list', () => { + store.addRecentSearch('foo'); + store.addRecentSearch('bar'); + + expect(store.state.recentSearches).toEqual(['bar', 'foo']); + }); + + it('should deduplicate', () => { + store.addRecentSearch('foo'); + store.addRecentSearch('bar'); + store.addRecentSearch('foo'); + + expect(store.state.recentSearches).toEqual(['foo', 'bar']); + }); + + it('only keeps track of 5 items', () => { + store.addRecentSearch('1'); + store.addRecentSearch('2'); + store.addRecentSearch('3'); + store.addRecentSearch('4'); + store.addRecentSearch('5'); + store.addRecentSearch('6'); + store.addRecentSearch('7'); + + expect(store.state.recentSearches).toEqual(['7', '6', '5', '4', '3']); + }); + }); + + describe('setRecentSearches', () => { + it('should override list', () => { + store.setRecentSearches([ + 'foo', + 'bar', + ]); + store.setRecentSearches([ + 'baz', + 'qux', + ]); + + expect(store.state.recentSearches).toEqual(['baz', 'qux']); + }); + + it('only keeps track of 5 items', () => { + store.setRecentSearches(['1', '2', '3', '4', '5', '6', '7']); + + expect(store.state.recentSearches).toEqual(['1', '2', '3', '4', '5']); + }); + }); +}); |