import { useLocalStorageSpy } from 'helpers/local_storage_helper'; import AccessorUtilities from '~/lib/utils/accessor'; import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants'; import { stripQuotes, uniqueTokens, prepareTokens, processFilters, filterToQueryObject, urlQueryToFilter, getRecentlyUsedSuggestions, setTokenValueToRecentlyUsed, } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils'; import { tokenValueAuthor, tokenValueLabel, tokenValueMilestone, tokenValuePlain, } from './mock_data'; const mockStorageKey = 'recent-tokens'; function setLocalStorageAvailability(isAvailable) { jest.spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').mockReturnValue(isAvailable); } describe('Filtered Search Utils', () => { describe('stripQuotes', () => { it.each` inputValue | outputValue ${'"Foo Bar"'} | ${'Foo Bar'} ${"'Foo Bar'"} | ${'Foo Bar'} ${'FooBar'} | ${'FooBar'} ${"Foo'Bar"} | ${"Foo'Bar"} ${'Foo"Bar'} | ${'Foo"Bar'} ${'Foo Bar'} | ${'Foo Bar'} `( 'returns string $outputValue when called with string $inputValue', ({ inputValue, outputValue }) => { expect(stripQuotes(inputValue)).toBe(outputValue); }, ); }); describe('uniqueTokens', () => { it('returns tokens array with duplicates removed', () => { expect( uniqueTokens([ tokenValueAuthor, tokenValueLabel, tokenValueMilestone, tokenValueLabel, tokenValuePlain, ]), ).toHaveLength(4); // Removes 2nd instance of tokenValueLabel }); it('returns tokens array as it is if it does not have duplicates', () => { expect( uniqueTokens([tokenValueAuthor, tokenValueLabel, tokenValueMilestone, tokenValuePlain]), ).toHaveLength(4); }); }); }); describe('prepareTokens', () => { describe('with empty data', () => { it('returns an empty array', () => { expect(prepareTokens()).toEqual([]); expect(prepareTokens({})).toEqual([]); expect(prepareTokens({ milestone: null, author: null, assignees: [], labels: [] })).toEqual( [], ); }); }); it.each([ [ 'milestone', { value: 'v1.0', operator: '=' }, [{ type: 'milestone', value: { data: 'v1.0', operator: '=' } }], ], [ 'author', { value: 'mr.popo', operator: '!=' }, [{ type: 'author', value: { data: 'mr.popo', operator: '!=' } }], ], [ 'labels', [{ value: 'z-fighters', operator: '=' }], [{ type: 'labels', value: { data: 'z-fighters', operator: '=' } }], ], [ 'assignees', [ { value: 'krillin', operator: '=' }, { value: 'piccolo', operator: '!=' }, ], [ { type: 'assignees', value: { data: 'krillin', operator: '=' } }, { type: 'assignees', value: { data: 'piccolo', operator: '!=' } }, ], ], [ 'foo', [ { value: 'bar', operator: '!=' }, { value: 'baz', operator: '!=' }, ], [ { type: 'foo', value: { data: 'bar', operator: '!=' } }, { type: 'foo', value: { data: 'baz', operator: '!=' } }, ], ], ])('gathers %s=%j into result=%j', (token, value, result) => { const res = prepareTokens({ [token]: value }); expect(res).toEqual(result); }); }); describe('processFilters', () => { it('processes multiple filter values', () => { const result = processFilters([ { type: 'foo', value: { data: 'foo', operator: '=' } }, { type: 'bar', value: { data: 'bar1', operator: '=' } }, { type: 'bar', value: { data: 'bar2', operator: '!=' } }, ]); expect(result).toStrictEqual({ foo: [{ value: 'foo', operator: '=' }], bar: [ { value: 'bar1', operator: '=' }, { value: 'bar2', operator: '!=' }, ], }); }); it('does not remove wrapping double quotes from the data', () => { const result = processFilters([ { type: 'foo', value: { data: '"value with spaces"', operator: '=' } }, ]); expect(result).toStrictEqual({ foo: [{ value: '"value with spaces"', operator: '=' }], }); }); }); describe('filterToQueryObject', () => { describe('with empty data', () => { it('returns an empty object', () => { expect(filterToQueryObject()).toEqual({}); expect(filterToQueryObject({})).toEqual({}); expect(filterToQueryObject({ author_username: null, label_name: [] })).toEqual({ author_username: null, label_name: null, 'not[author_username]': null, 'not[label_name]': null, }); }); }); it.each([ [ 'author_username', { value: 'v1.0', operator: '=' }, { author_username: 'v1.0', 'not[author_username]': null }, ], [ 'author_username', { value: 'v1.0', operator: '!=' }, { author_username: null, 'not[author_username]': 'v1.0' }, ], [ 'label_name', [{ value: 'z-fighters', operator: '=' }], { label_name: ['z-fighters'], 'not[label_name]': null }, ], [ 'label_name', [{ value: 'z-fighters', operator: '!=' }], { label_name: null, 'not[label_name]': ['z-fighters'] }, ], [ 'foo', [ { value: 'bar', operator: '=' }, { value: 'baz', operator: '=' }, ], { foo: ['bar', 'baz'], 'not[foo]': null }, ], [ 'foo', [ { value: 'bar', operator: '!=' }, { value: 'baz', operator: '!=' }, ], { foo: null, 'not[foo]': ['bar', 'baz'] }, ], [ 'foo', [ { value: 'bar', operator: '!=' }, { value: 'baz', operator: '=' }, ], { foo: ['baz'], 'not[foo]': ['bar'] }, ], ])('gathers filter values %s=%j into query object=%j', (token, value, result) => { const res = filterToQueryObject({ [token]: value }); expect(res).toEqual(result); }); it.each([ [FILTERED_SEARCH_TERM, [{ value: '' }], { search: '' }], [FILTERED_SEARCH_TERM, [{ value: 'bar' }], { search: 'bar' }], [FILTERED_SEARCH_TERM, [{ value: 'bar' }, { value: '' }], { search: 'bar' }], [FILTERED_SEARCH_TERM, [{ value: 'bar' }, { value: 'baz' }], { search: 'bar baz' }], ])( 'when filteredSearchTermKey=search gathers filter values %s=%j into query object=%j', (token, value, result) => { const res = filterToQueryObject({ [token]: value }, { filteredSearchTermKey: 'search' }); expect(res).toEqual(result); }, ); }); describe('urlQueryToFilter', () => { describe('with empty data', () => { it('returns an empty object', () => { expect(urlQueryToFilter()).toEqual({}); expect(urlQueryToFilter('')).toEqual({}); expect(urlQueryToFilter('author_username=&milestone_title=&')).toEqual({}); }); }); it.each([ ['author_username=v1.0', { author_username: { value: 'v1.0', operator: '=' } }], ['not[author_username]=v1.0', { author_username: { value: 'v1.0', operator: '!=' } }], ['foo=bar&foo=baz', { foo: { value: 'baz', operator: '=' } }], ['foo=bar&foo[]=baz', { foo: [{ value: 'baz', operator: '=' }] }], ['not[foo]=bar&foo=baz', { foo: { value: 'baz', operator: '=' } }], [ 'foo[]=bar&foo[]=baz¬[foo]=', { foo: [ { value: 'bar', operator: '=' }, { value: 'baz', operator: '=' }, ], }, ], [ 'foo[]=¬[foo][]=bar¬[foo][]=baz', { foo: [ { value: 'bar', operator: '!=' }, { value: 'baz', operator: '!=' }, ], }, ], [ 'foo[]=baz¬[foo][]=bar', { foo: [ { value: 'baz', operator: '=' }, { value: 'bar', operator: '!=' }, ], }, ], ['not[foo][]=bar', { foo: [{ value: 'bar', operator: '!=' }] }], ['nop=1¬[nop]=2', {}, { filterNamesAllowList: ['foo'] }], [ 'foo[]=bar¬[foo][]=baz&nop=xxx¬[nop]=yyy', { foo: [ { value: 'bar', operator: '=' }, { value: 'baz', operator: '!=' }, ], }, { filterNamesAllowList: ['foo'] }, ], [ 'search=term&foo=bar', { [FILTERED_SEARCH_TERM]: [{ value: 'term' }], foo: { value: 'bar', operator: '=' }, }, { filteredSearchTermKey: 'search' }, ], [ 'search=my terms', { [FILTERED_SEARCH_TERM]: [{ value: 'my' }, { value: 'terms' }], }, { filteredSearchTermKey: 'search' }, ], [ 'search[]=my&search[]=terms', { [FILTERED_SEARCH_TERM]: [{ value: 'my' }, { value: 'terms' }], }, { filteredSearchTermKey: 'search' }, ], [ 'search=my+terms', { [FILTERED_SEARCH_TERM]: [{ value: 'my' }, { value: 'terms' }], }, { filteredSearchTermKey: 'search', legacySpacesDecode: false }, ], [ 'search=my terms&foo=bar&nop=xxx', { [FILTERED_SEARCH_TERM]: [{ value: 'my' }, { value: 'terms' }], foo: { value: 'bar', operator: '=' }, }, { filteredSearchTermKey: 'search', filterNamesAllowList: ['foo'] }, ], ])( 'gathers filter values %s into query object=%j when options %j', (query, result, options = undefined) => { const res = urlQueryToFilter(query, options); expect(res).toEqual(result); }, ); }); describe('getRecentlyUsedSuggestions', () => { useLocalStorageSpy(); beforeEach(() => { localStorage.removeItem(mockStorageKey); }); it('returns array containing recently used token values from provided recentSuggestionsStorageKey', () => { setLocalStorageAvailability(true); const mockExpectedArray = [{ foo: 'bar' }]; localStorage.setItem(mockStorageKey, JSON.stringify(mockExpectedArray)); expect(getRecentlyUsedSuggestions(mockStorageKey)).toEqual(mockExpectedArray); }); it('returns empty array when provided recentSuggestionsStorageKey does not have anything in localStorage', () => { setLocalStorageAvailability(true); expect(getRecentlyUsedSuggestions(mockStorageKey)).toEqual([]); }); it('returns empty array when when access to localStorage is not available', () => { setLocalStorageAvailability(false); expect(getRecentlyUsedSuggestions(mockStorageKey)).toEqual([]); }); }); describe('setTokenValueToRecentlyUsed', () => { const mockTokenValue1 = { foo: 'bar' }; const mockTokenValue2 = { bar: 'baz' }; useLocalStorageSpy(); beforeEach(() => { localStorage.removeItem(mockStorageKey); }); it('adds provided tokenValue to localStorage for recentSuggestionsStorageKey', () => { setLocalStorageAvailability(true); setTokenValueToRecentlyUsed(mockStorageKey, mockTokenValue1); expect(JSON.parse(localStorage.getItem(mockStorageKey))).toEqual([mockTokenValue1]); }); it('adds provided tokenValue to localStorage at the top of existing values (i.e. Stack order)', () => { setLocalStorageAvailability(true); setTokenValueToRecentlyUsed(mockStorageKey, mockTokenValue1); setTokenValueToRecentlyUsed(mockStorageKey, mockTokenValue2); expect(JSON.parse(localStorage.getItem(mockStorageKey))).toEqual([ mockTokenValue2, mockTokenValue1, ]); }); it('ensures that provided tokenValue is not added twice', () => { setLocalStorageAvailability(true); setTokenValueToRecentlyUsed(mockStorageKey, mockTokenValue1); setTokenValueToRecentlyUsed(mockStorageKey, mockTokenValue1); expect(JSON.parse(localStorage.getItem(mockStorageKey))).toEqual([mockTokenValue1]); }); it('does not add any value when acess to localStorage is not available', () => { setLocalStorageAvailability(false); setTokenValueToRecentlyUsed(mockStorageKey, mockTokenValue1); expect(JSON.parse(localStorage.getItem(mockStorageKey))).toBeNull(); }); });