summaryrefslogtreecommitdiff
path: root/spec/frontend/filtered_search/filtered_search_manager_spec.js
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/filtered_search/filtered_search_manager_spec.js')
-rw-r--r--spec/frontend/filtered_search/filtered_search_manager_spec.js587
1 files changed, 587 insertions, 0 deletions
diff --git a/spec/frontend/filtered_search/filtered_search_manager_spec.js b/spec/frontend/filtered_search/filtered_search_manager_spec.js
new file mode 100644
index 00000000000..ef87662a1ef
--- /dev/null
+++ b/spec/frontend/filtered_search/filtered_search_manager_spec.js
@@ -0,0 +1,587 @@
+import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
+import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error';
+import RecentSearchesRoot from '~/filtered_search/recent_searches_root';
+import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
+import '~/lib/utils/common_utils';
+import DropdownUtils from '~/filtered_search/dropdown_utils';
+import FilteredSearchVisualTokens from '~/filtered_search/filtered_search_visual_tokens';
+import FilteredSearchDropdownManager from '~/filtered_search/filtered_search_dropdown_manager';
+import FilteredSearchManager from '~/filtered_search/filtered_search_manager';
+import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper';
+import { BACKSPACE_KEY_CODE, DELETE_KEY_CODE } from '~/lib/utils/keycodes';
+import { visitUrl } from '~/lib/utils/url_utility';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ visitUrl: jest.fn(),
+}));
+
+describe('Filtered Search Manager', () => {
+ let input;
+ let manager;
+ let tokensContainer;
+ const page = 'issues';
+ const placeholder = 'Search or filter results...';
+
+ function dispatchBackspaceEvent(element, eventType) {
+ const event = new Event(eventType);
+ event.keyCode = BACKSPACE_KEY_CODE;
+ element.dispatchEvent(event);
+ }
+
+ function dispatchDeleteEvent(element, eventType) {
+ const event = new Event(eventType);
+ event.keyCode = DELETE_KEY_CODE;
+ element.dispatchEvent(event);
+ }
+
+ function dispatchAltBackspaceEvent(element, eventType) {
+ const event = new Event(eventType);
+ event.altKey = true;
+ event.keyCode = BACKSPACE_KEY_CODE;
+ element.dispatchEvent(event);
+ }
+
+ function dispatchCtrlBackspaceEvent(element, eventType) {
+ const event = new Event(eventType);
+ event.ctrlKey = true;
+ event.keyCode = BACKSPACE_KEY_CODE;
+ element.dispatchEvent(event);
+ }
+
+ function dispatchMetaBackspaceEvent(element, eventType) {
+ const event = new Event(eventType);
+ event.metaKey = true;
+ event.keyCode = BACKSPACE_KEY_CODE;
+ 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>
+ `);
+
+ jest.spyOn(FilteredSearchDropdownManager.prototype, 'setDropdown').mockImplementation();
+ });
+
+ const initializeManager = () => {
+ jest.spyOn(FilteredSearchManager.prototype, 'loadSearchParamsFromURL').mockImplementation();
+ jest.spyOn(FilteredSearchManager.prototype, 'tokenChange').mockImplementation();
+ jest
+ .spyOn(FilteredSearchDropdownManager.prototype, 'updateDropdownOffset')
+ .mockImplementation();
+ jest.spyOn(gl.utils, 'getParameterByName').mockReturnValue(null);
+ jest.spyOn(FilteredSearchVisualTokens, 'unselectTokens');
+
+ input = document.querySelector('.filtered-search');
+ tokensContainer = document.querySelector('.tokens-container');
+ manager = new FilteredSearchManager({ page });
+ manager.setup();
+ };
+
+ afterEach(() => {
+ manager.cleanup();
+ });
+
+ describe('class constructor', () => {
+ const isLocalStorageAvailable = 'isLocalStorageAvailable';
+
+ beforeEach(() => {
+ jest.spyOn(RecentSearchesService, 'isAvailable').mockReturnValue(isLocalStorageAvailable);
+ jest.spyOn(RecentSearchesRoot.prototype, 'render').mockImplementation();
+ });
+
+ it('should instantiate RecentSearchesStore with isLocalStorageAvailable', () => {
+ manager = new FilteredSearchManager({ page });
+
+ expect(RecentSearchesService.isAvailable).toHaveBeenCalled();
+ expect(manager.recentSearchesStore.state).toEqual(
+ expect.objectContaining({
+ isLocalStorageAvailable,
+ allowedKeys: IssuableFilteredSearchTokenKeys.getKeys(),
+ }),
+ );
+ });
+ });
+
+ describe('setup', () => {
+ beforeEach(() => {
+ manager = new FilteredSearchManager({ page });
+ });
+
+ it('should not instantiate Flash if an RecentSearchesServiceError is caught', () => {
+ jest
+ .spyOn(RecentSearchesService.prototype, 'fetch')
+ .mockImplementation(() => Promise.reject(new RecentSearchesServiceError()));
+ jest.spyOn(window, 'Flash').mockImplementation();
+
+ manager.setup();
+
+ expect(window.Flash).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('searchState', () => {
+ beforeEach(() => {
+ jest.spyOn(FilteredSearchManager.prototype, 'search').mockImplementation();
+ initializeManager();
+ });
+
+ it('should blur button', () => {
+ const e = {
+ preventDefault: () => {},
+ currentTarget: {
+ blur: () => {},
+ },
+ };
+ jest.spyOn(e.currentTarget, 'blur');
+ manager.searchState(e);
+
+ expect(e.currentTarget.blur).toHaveBeenCalled();
+ });
+
+ it('should not call search if there is no state', () => {
+ const e = {
+ preventDefault: () => {},
+ currentTarget: {
+ blur: () => {},
+ },
+ };
+
+ manager.searchState(e);
+
+ expect(FilteredSearchManager.prototype.search).not.toHaveBeenCalled();
+ });
+
+ it('should call search when there is state', () => {
+ const e = {
+ preventDefault: () => {},
+ currentTarget: {
+ blur: () => {},
+ dataset: {
+ state: 'opened',
+ },
+ },
+ };
+
+ manager.searchState(e);
+
+ expect(FilteredSearchManager.prototype.search).toHaveBeenCalledWith('opened');
+ });
+ });
+
+ describe('search', () => {
+ const defaultParams = '?scope=all&utf8=%E2%9C%93&state=opened';
+
+ beforeEach(() => {
+ initializeManager();
+ });
+
+ it('should search with a single word', done => {
+ input.value = 'searchTerm';
+
+ visitUrl.mockImplementation(url => {
+ expect(url).toEqual(`${defaultParams}&search=searchTerm`);
+ done();
+ });
+
+ manager.search();
+ });
+
+ it('should search with multiple words', done => {
+ input.value = 'awesome search terms';
+
+ visitUrl.mockImplementation(url => {
+ expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`);
+ done();
+ });
+
+ manager.search();
+ });
+
+ it('should search with special characters', done => {
+ input.value = '~!@#$%^&*()_+{}:<>,.?/';
+
+ visitUrl.mockImplementation(url => {
+ expect(url).toEqual(
+ `${defaultParams}&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`,
+ );
+ done();
+ });
+
+ manager.search();
+ });
+
+ it('removes duplicated tokens', done => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '=', '~bug')}
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '=', '~bug')}
+ `);
+
+ visitUrl.mockImplementation(url => {
+ expect(url).toEqual(`${defaultParams}&label_name[]=bug`);
+ done();
+ });
+
+ manager.search();
+ });
+ });
+
+ describe('handleInputPlaceholder', () => {
+ beforeEach(() => {
+ initializeManager();
+ });
+
+ it('should render placeholder when there is no input', () => {
+ expect(input.placeholder).toEqual(placeholder);
+ });
+
+ it('should not render placeholder when there is input', () => {
+ input.value = 'test words';
+
+ const event = new Event('input');
+ input.dispatchEvent(event);
+
+ expect(input.placeholder).toEqual('');
+ });
+
+ it('should not render placeholder when there are tokens and no input', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '=', '~bug'),
+ );
+
+ const event = new Event('input');
+ input.dispatchEvent(event);
+
+ expect(input.placeholder).toEqual('');
+ });
+ });
+
+ describe('checkForBackspace', () => {
+ beforeEach(() => {
+ initializeManager();
+ });
+
+ describe('tokens and no input', () => {
+ beforeEach(() => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '=', '~bug'),
+ );
+ });
+
+ it('removes last token', () => {
+ jest.spyOn(FilteredSearchVisualTokens, 'removeLastTokenPartial');
+ dispatchBackspaceEvent(input, 'keyup');
+ dispatchBackspaceEvent(input, 'keyup');
+
+ expect(FilteredSearchVisualTokens.removeLastTokenPartial).toHaveBeenCalled();
+ });
+
+ it('sets the input', () => {
+ jest.spyOn(FilteredSearchVisualTokens, 'getLastTokenPartial');
+ dispatchDeleteEvent(input, 'keyup');
+ dispatchDeleteEvent(input, 'keyup');
+
+ expect(FilteredSearchVisualTokens.getLastTokenPartial).toHaveBeenCalled();
+ expect(input.value).toEqual('~bug');
+ });
+ });
+
+ it('does not remove token or change input when there is existing input', () => {
+ jest.spyOn(FilteredSearchVisualTokens, 'removeLastTokenPartial');
+ jest.spyOn(FilteredSearchVisualTokens, 'getLastTokenPartial');
+
+ input.value = 'text';
+ dispatchDeleteEvent(input, 'keyup');
+
+ expect(FilteredSearchVisualTokens.removeLastTokenPartial).not.toHaveBeenCalled();
+ expect(FilteredSearchVisualTokens.getLastTokenPartial).not.toHaveBeenCalled();
+ expect(input.value).toEqual('text');
+ });
+
+ it('does not remove previous token on single backspace press', () => {
+ jest.spyOn(FilteredSearchVisualTokens, 'removeLastTokenPartial');
+ jest.spyOn(FilteredSearchVisualTokens, 'getLastTokenPartial');
+
+ input.value = 't';
+ dispatchDeleteEvent(input, 'keyup');
+
+ expect(FilteredSearchVisualTokens.removeLastTokenPartial).not.toHaveBeenCalled();
+ expect(FilteredSearchVisualTokens.getLastTokenPartial).not.toHaveBeenCalled();
+ expect(input.value).toEqual('t');
+ });
+ });
+
+ describe('checkForAltOrCtrlBackspace', () => {
+ beforeEach(() => {
+ initializeManager();
+ jest.spyOn(FilteredSearchVisualTokens, 'removeLastTokenPartial');
+ });
+
+ describe('tokens and no input', () => {
+ beforeEach(() => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '=', '~bug'),
+ );
+ });
+
+ it('removes last token via alt-backspace', () => {
+ dispatchAltBackspaceEvent(input, 'keydown');
+
+ expect(FilteredSearchVisualTokens.removeLastTokenPartial).toHaveBeenCalled();
+ });
+
+ it('removes last token via ctrl-backspace', () => {
+ dispatchCtrlBackspaceEvent(input, 'keydown');
+
+ expect(FilteredSearchVisualTokens.removeLastTokenPartial).toHaveBeenCalled();
+ });
+ });
+
+ describe('tokens and input', () => {
+ beforeEach(() => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '=', '~bug'),
+ );
+ });
+
+ it('does not remove token or change input via alt-backspace when there is existing input', () => {
+ input = manager.filteredSearchInput;
+ input.value = 'text';
+ dispatchAltBackspaceEvent(input, 'keydown');
+
+ expect(FilteredSearchVisualTokens.removeLastTokenPartial).not.toHaveBeenCalled();
+ expect(input.value).toEqual('text');
+ });
+
+ it('does not remove token or change input via ctrl-backspace when there is existing input', () => {
+ input = manager.filteredSearchInput;
+ input.value = 'text';
+ dispatchCtrlBackspaceEvent(input, 'keydown');
+
+ expect(FilteredSearchVisualTokens.removeLastTokenPartial).not.toHaveBeenCalled();
+ expect(input.value).toEqual('text');
+ });
+ });
+ });
+
+ describe('checkForMetaBackspace', () => {
+ beforeEach(() => {
+ initializeManager();
+ });
+
+ beforeEach(() => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '=', '~bug'),
+ );
+ });
+
+ it('removes all tokens and input', () => {
+ jest.spyOn(FilteredSearchManager.prototype, 'clearSearch');
+ dispatchMetaBackspaceEvent(input, 'keydown');
+
+ expect(manager.clearSearch).toHaveBeenCalled();
+ expect(manager.filteredSearchInput.value).toEqual('');
+ expect(DropdownUtils.getSearchQuery()).toEqual('');
+ });
+ });
+
+ describe('removeToken', () => {
+ beforeEach(() => {
+ initializeManager();
+ });
+
+ 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(() => {
+ jest.spyOn(FilteredSearchManager.prototype, 'removeSelectedToken');
+
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', '=', 'none'),
+ );
+ tokensContainer.querySelector('.js-visual-token .remove-token').click();
+ });
+
+ it('removes token when remove button is selected', () => {
+ expect(tokensContainer.querySelector('.js-visual-token')).toEqual(null);
+ });
+
+ it('calls removeSelectedToken', () => {
+ expect(manager.removeSelectedToken).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('removeSelectedTokenKeydown', () => {
+ beforeEach(() => {
+ initializeManager();
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', '=', 'none', true),
+ );
+ });
+
+ it('removes selected token when the backspace key is pressed', () => {
+ expect(getVisualTokens().length).toEqual(1);
+
+ dispatchBackspaceEvent(document, 'keydown');
+
+ expect(getVisualTokens().length).toEqual(0);
+ });
+
+ it('removes selected token when the delete key is pressed', () => {
+ expect(getVisualTokens().length).toEqual(1);
+
+ dispatchDeleteEvent(document, 'keydown');
+
+ expect(getVisualTokens().length).toEqual(0);
+ });
+
+ it('updates the input placeholder after removal', () => {
+ manager.handleInputPlaceholder();
+
+ expect(input.placeholder).toEqual('');
+ expect(getVisualTokens().length).toEqual(1);
+
+ dispatchBackspaceEvent(document, 'keydown');
+
+ expect(input.placeholder).not.toEqual('');
+ expect(getVisualTokens().length).toEqual(0);
+ });
+
+ it('updates the clear button after removal', () => {
+ manager.toggleClearSearchButton();
+
+ const clearButton = document.querySelector('.clear-search');
+
+ expect(clearButton.classList.contains('hidden')).toEqual(false);
+ expect(getVisualTokens().length).toEqual(1);
+
+ dispatchBackspaceEvent(document, 'keydown');
+
+ expect(clearButton.classList.contains('hidden')).toEqual(true);
+ expect(getVisualTokens().length).toEqual(0);
+ });
+ });
+
+ describe('removeSelectedToken', () => {
+ beforeEach(() => {
+ jest.spyOn(FilteredSearchVisualTokens, 'removeSelectedToken');
+ jest.spyOn(FilteredSearchManager.prototype, 'handleInputPlaceholder');
+ jest.spyOn(FilteredSearchManager.prototype, 'toggleClearSearchButton');
+ initializeManager();
+ });
+
+ it('calls FilteredSearchVisualTokens.removeSelectedToken', () => {
+ manager.removeSelectedToken();
+
+ expect(FilteredSearchVisualTokens.removeSelectedToken).toHaveBeenCalled();
+ });
+
+ it('calls handleInputPlaceholder', () => {
+ manager.removeSelectedToken();
+
+ expect(manager.handleInputPlaceholder).toHaveBeenCalled();
+ });
+
+ it('calls toggleClearSearchButton', () => {
+ manager.removeSelectedToken();
+
+ expect(manager.toggleClearSearchButton).toHaveBeenCalled();
+ });
+
+ it('calls update dropdown offset', () => {
+ manager.removeSelectedToken();
+
+ expect(manager.dropdownManager.updateDropdownOffset).toHaveBeenCalled();
+ });
+ });
+
+ describe('Clearing search', () => {
+ beforeEach(() => {
+ initializeManager();
+ });
+
+ it('Clicking the "x" clear button, clears the input', () => {
+ const inputValue = 'label:=~bug';
+ manager.filteredSearchInput.value = inputValue;
+ manager.filteredSearchInput.dispatchEvent(new Event('input'));
+
+ expect(DropdownUtils.getSearchQuery()).toEqual(inputValue);
+
+ manager.clearSearchButton.click();
+
+ expect(manager.filteredSearchInput.value).toEqual('');
+ expect(DropdownUtils.getSearchQuery()).toEqual('');
+ });
+ });
+
+ describe('toggleInputContainerFocus', () => {
+ beforeEach(() => {
+ initializeManager();
+ });
+
+ 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,
+ );
+ });
+ });
+
+ describe('getAllParams', () => {
+ let paramsArr;
+ beforeEach(() => {
+ paramsArr = ['key=value', 'otherkey=othervalue'];
+
+ initializeManager();
+ });
+
+ it('correctly modifies params when custom modifier is passed', () => {
+ const modifedParams = manager.getAllParams.call(
+ {
+ modifyUrlParams: params => params.reverse(),
+ },
+ [].concat(paramsArr),
+ );
+
+ expect(modifedParams[0]).toBe(paramsArr[1]);
+ });
+
+ it('does not modify params when no custom modifier is passed', () => {
+ const modifedParams = manager.getAllParams.call({}, paramsArr);
+
+ expect(modifedParams[1]).toBe(paramsArr[1]);
+ });
+ });
+});