diff options
Diffstat (limited to 'spec/frontend/vue_shared/components/filtered_search_bar')
3 files changed, 473 insertions, 0 deletions
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js new file mode 100644 index 00000000000..eded5b87abc --- /dev/null +++ b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js @@ -0,0 +1,259 @@ +import { shallowMount } from '@vue/test-utils'; +import { + GlFilteredSearch, + GlButtonGroup, + GlButton, + GlNewDropdown as GlDropdown, + GlNewDropdownItem as GlDropdownItem, +} from '@gitlab/ui'; + +import FilteredSearchBarRoot from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; +import { SortDirection } from '~/vue_shared/components/filtered_search_bar/constants'; + +import RecentSearchesStore from '~/filtered_search/stores/recent_searches_store'; +import RecentSearchesService from '~/filtered_search/services/recent_searches_service'; + +import { mockAvailableTokens, mockSortOptions } from './mock_data'; + +const createComponent = ({ + namespace = 'gitlab-org/gitlab-test', + recentSearchesStorageKey = 'requirements', + tokens = mockAvailableTokens, + sortOptions = mockSortOptions, + searchInputPlaceholder = 'Filter requirements', +} = {}) => + shallowMount(FilteredSearchBarRoot, { + propsData: { + namespace, + recentSearchesStorageKey, + tokens, + sortOptions, + searchInputPlaceholder, + }, + }); + +describe('FilteredSearchBarRoot', () => { + let wrapper; + + beforeEach(() => { + wrapper = createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('data', () => { + it('initializes `filterValue`, `selectedSortOption` and `selectedSortDirection` data props', () => { + expect(wrapper.vm.filterValue).toEqual([]); + expect(wrapper.vm.selectedSortOption).toBe(mockSortOptions[0].sortDirection.descending); + expect(wrapper.vm.selectedSortDirection).toBe(SortDirection.descending); + }); + }); + + describe('computed', () => { + describe('tokenSymbols', () => { + it('returns array of map containing type and symbols from `tokens` prop', () => { + expect(wrapper.vm.tokenSymbols).toEqual({ author_username: '@' }); + }); + }); + + describe('sortDirectionIcon', () => { + it('returns string "sort-lowest" when `selectedSortDirection` is "ascending"', () => { + wrapper.setData({ + selectedSortDirection: SortDirection.ascending, + }); + + expect(wrapper.vm.sortDirectionIcon).toBe('sort-lowest'); + }); + + it('returns string "sort-highest" when `selectedSortDirection` is "descending"', () => { + wrapper.setData({ + selectedSortDirection: SortDirection.descending, + }); + + expect(wrapper.vm.sortDirectionIcon).toBe('sort-highest'); + }); + }); + + describe('sortDirectionTooltip', () => { + it('returns string "Sort direction: Ascending" when `selectedSortDirection` is "ascending"', () => { + wrapper.setData({ + selectedSortDirection: SortDirection.ascending, + }); + + expect(wrapper.vm.sortDirectionTooltip).toBe('Sort direction: Ascending'); + }); + + it('returns string "Sort direction: Descending" when `selectedSortDirection` is "descending"', () => { + wrapper.setData({ + selectedSortDirection: SortDirection.descending, + }); + + expect(wrapper.vm.sortDirectionTooltip).toBe('Sort direction: Descending'); + }); + }); + }); + + describe('watchers', () => { + describe('filterValue', () => { + it('emits component event `onFilter` with empty array when `filterValue` is cleared by GlFilteredSearch', () => { + wrapper.setData({ + initialRender: false, + filterValue: [ + { + type: 'filtered-search-term', + value: { data: '' }, + }, + ], + }); + + return wrapper.vm.$nextTick(() => { + expect(wrapper.emitted('onFilter')[0]).toEqual([[]]); + }); + }); + }); + }); + + describe('methods', () => { + describe('setupRecentSearch', () => { + it('initializes `recentSearchesService` and `recentSearchesStore` props when `recentSearchesStorageKey` is available', () => { + expect(wrapper.vm.recentSearchesService instanceof RecentSearchesService).toBe(true); + expect(wrapper.vm.recentSearchesStore instanceof RecentSearchesStore).toBe(true); + }); + + it('initializes `recentSearchesPromise` prop with a promise by using `recentSearchesService.fetch()`', () => { + jest + .spyOn(wrapper.vm.recentSearchesService, 'fetch') + .mockReturnValue(new Promise(() => [])); + + wrapper.vm.setupRecentSearch(); + + expect(wrapper.vm.recentSearchesPromise instanceof Promise).toBe(true); + }); + }); + + describe('getRecentSearches', () => { + it('returns array of strings representing recent searches', () => { + wrapper.vm.recentSearchesStore.setRecentSearches(['foo']); + + expect(wrapper.vm.getRecentSearches()).toEqual(['foo']); + }); + }); + + describe('handleSortOptionClick', () => { + it('emits component event `onSort` with selected sort by value', () => { + wrapper.vm.handleSortOptionClick(mockSortOptions[1]); + + expect(wrapper.vm.selectedSortOption).toBe(mockSortOptions[1]); + expect(wrapper.emitted('onSort')[0]).toEqual([mockSortOptions[1].sortDirection.descending]); + }); + }); + + describe('handleSortDirectionClick', () => { + beforeEach(() => { + wrapper.setData({ + selectedSortOption: mockSortOptions[0], + }); + }); + + it('sets `selectedSortDirection` to be opposite of its current value', () => { + expect(wrapper.vm.selectedSortDirection).toBe(SortDirection.descending); + + wrapper.vm.handleSortDirectionClick(); + + expect(wrapper.vm.selectedSortDirection).toBe(SortDirection.ascending); + }); + + it('emits component event `onSort` with opposite of currently selected sort by value', () => { + wrapper.vm.handleSortDirectionClick(); + + expect(wrapper.emitted('onSort')[0]).toEqual([mockSortOptions[0].sortDirection.ascending]); + }); + }); + + describe('handleFilterSubmit', () => { + const mockFilters = [ + { + type: 'author_username', + value: { + data: 'root', + operator: '=', + }, + }, + 'foo', + ]; + + it('calls `recentSearchesStore.addRecentSearch` with serialized value of provided `filters` param', () => { + jest.spyOn(wrapper.vm.recentSearchesStore, 'addRecentSearch'); + // jest.spyOn(wrapper.vm.recentSearchesService, 'save'); + + wrapper.vm.handleFilterSubmit(mockFilters); + + return wrapper.vm.recentSearchesPromise.then(() => { + expect(wrapper.vm.recentSearchesStore.addRecentSearch).toHaveBeenCalledWith( + 'author_username:=@root foo', + ); + }); + }); + + it('calls `recentSearchesService.save` with array of searches', () => { + jest.spyOn(wrapper.vm.recentSearchesService, 'save'); + + wrapper.vm.handleFilterSubmit(mockFilters); + + return wrapper.vm.recentSearchesPromise.then(() => { + expect(wrapper.vm.recentSearchesService.save).toHaveBeenCalledWith([ + 'author_username:=@root foo', + ]); + }); + }); + + it('emits component event `onFilter` with provided filters param', () => { + wrapper.vm.handleFilterSubmit(mockFilters); + + expect(wrapper.emitted('onFilter')[0]).toEqual([mockFilters]); + }); + }); + }); + + describe('template', () => { + beforeEach(() => { + wrapper.setData({ + selectedSortOption: mockSortOptions[0], + selectedSortDirection: SortDirection.descending, + }); + + return wrapper.vm.$nextTick(); + }); + + it('renders gl-filtered-search component', () => { + const glFilteredSearchEl = wrapper.find(GlFilteredSearch); + + expect(glFilteredSearchEl.props('placeholder')).toBe('Filter requirements'); + expect(glFilteredSearchEl.props('availableTokens')).toEqual(mockAvailableTokens); + }); + + it('renders sort dropdown component', () => { + expect(wrapper.find(GlButtonGroup).exists()).toBe(true); + expect(wrapper.find(GlDropdown).exists()).toBe(true); + expect(wrapper.find(GlDropdown).props('text')).toBe(mockSortOptions[0].title); + }); + + it('renders dropdown items', () => { + const dropdownItemsEl = wrapper.findAll(GlDropdownItem); + + expect(dropdownItemsEl).toHaveLength(mockSortOptions.length); + expect(dropdownItemsEl.at(0).text()).toBe(mockSortOptions[0].title); + expect(dropdownItemsEl.at(0).props('isChecked')).toBe(true); + expect(dropdownItemsEl.at(1).text()).toBe(mockSortOptions[1].title); + }); + + it('renders sort direction button', () => { + const sortButtonEl = wrapper.find(GlButton); + + expect(sortButtonEl.attributes('title')).toBe('Sort direction: Descending'); + expect(sortButtonEl.props('icon')).toBe('sort-highest'); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js new file mode 100644 index 00000000000..edc0f119262 --- /dev/null +++ b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js @@ -0,0 +1,64 @@ +import Api from '~/api'; +import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue'; + +export const mockAuthor1 = { + id: 1, + name: 'Administrator', + username: 'root', + state: 'active', + avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + web_url: 'http://0.0.0.0:3000/root', +}; + +export const mockAuthor2 = { + id: 2, + name: 'Claudio Beer', + username: 'ericka_terry', + state: 'active', + avatar_url: 'https://www.gravatar.com/avatar/12a89d115b5a398d5082897ebbcba9c2?s=80&d=identicon', + web_url: 'http://0.0.0.0:3000/ericka_terry', +}; + +export const mockAuthor3 = { + id: 6, + name: 'Shizue Hartmann', + username: 'junita.weimann', + state: 'active', + avatar_url: 'https://www.gravatar.com/avatar/9da1abb41b1d4c9c9e81030b71ea61a0?s=80&d=identicon', + web_url: 'http://0.0.0.0:3000/junita.weimann', +}; + +export const mockAuthors = [mockAuthor1, mockAuthor2, mockAuthor3]; + +export const mockAuthorToken = { + type: 'author_username', + icon: 'user', + title: 'Author', + unique: false, + symbol: '@', + token: AuthorToken, + operators: [{ value: '=', description: 'is', default: 'true' }], + fetchPath: 'gitlab-org/gitlab-test', + fetchAuthors: Api.projectUsers.bind(Api), +}; + +export const mockAvailableTokens = [mockAuthorToken]; + +export const mockSortOptions = [ + { + id: 1, + title: 'Created date', + sortDirection: { + descending: 'created_desc', + ascending: 'created_asc', + }, + }, + { + id: 2, + title: 'Last updated', + sortDirection: { + descending: 'updated_desc', + ascending: 'updated_asc', + }, + }, +]; diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js new file mode 100644 index 00000000000..3650ef79136 --- /dev/null +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js @@ -0,0 +1,150 @@ +import { mount } from '@vue/test-utils'; +import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui'; +import MockAdapter from 'axios-mock-adapter'; +import waitForPromises from 'helpers/wait_for_promises'; +import axios from '~/lib/utils/axios_utils'; + +import createFlash from '~/flash'; +import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue'; + +import { mockAuthorToken, mockAuthors } from '../mock_data'; + +jest.mock('~/flash'); + +const createComponent = ({ config = mockAuthorToken, value = { data: '' } } = {}) => + mount(AuthorToken, { + propsData: { + config, + value, + }, + provide: { + portalName: 'fake target', + alignSuggestions: function fakeAlignSuggestions() {}, + }, + stubs: { + Portal: { + template: '<div><slot></slot></div>', + }, + GlFilteredSearchSuggestionList: { + template: '<div></div>', + methods: { + getValue: () => '=', + }, + }, + }, + }); + +describe('AuthorToken', () => { + let mock; + let wrapper; + + beforeEach(() => { + mock = new MockAdapter(axios); + wrapper = createComponent(); + }); + + afterEach(() => { + mock.restore(); + wrapper.destroy(); + }); + + describe('computed', () => { + describe('currentValue', () => { + it('returns lowercase string for `value.data`', () => { + wrapper.setProps({ + value: { data: 'FOO' }, + }); + + return wrapper.vm.$nextTick(() => { + expect(wrapper.vm.currentValue).toBe('foo'); + }); + }); + }); + + describe('activeAuthor', () => { + it('returns object for currently present `value.data`', () => { + wrapper.setData({ + authors: mockAuthors, + }); + + wrapper.setProps({ + value: { data: mockAuthors[0].username }, + }); + + return wrapper.vm.$nextTick(() => { + expect(wrapper.vm.activeAuthor).toEqual(mockAuthors[0]); + }); + }); + }); + }); + + describe('fetchAuthorBySearchTerm', () => { + it('calls `config.fetchAuthors` with provided searchTerm param', () => { + jest.spyOn(wrapper.vm.config, 'fetchAuthors'); + + wrapper.vm.fetchAuthorBySearchTerm(mockAuthors[0].username); + + expect(wrapper.vm.config.fetchAuthors).toHaveBeenCalledWith( + mockAuthorToken.fetchPath, + mockAuthors[0].username, + ); + }); + + it('sets response to `authors` when request is succesful', () => { + jest.spyOn(wrapper.vm.config, 'fetchAuthors').mockResolvedValue(mockAuthors); + + wrapper.vm.fetchAuthorBySearchTerm('root'); + + return waitForPromises().then(() => { + expect(wrapper.vm.authors).toEqual(mockAuthors); + }); + }); + + it('calls `createFlash` with flash error message when request fails', () => { + jest.spyOn(wrapper.vm.config, 'fetchAuthors').mockRejectedValue({}); + + wrapper.vm.fetchAuthorBySearchTerm('root'); + + return waitForPromises().then(() => { + expect(createFlash).toHaveBeenCalledWith('There was a problem fetching users.'); + }); + }); + + it('sets `loading` to false when request completes', () => { + jest.spyOn(wrapper.vm.config, 'fetchAuthors').mockRejectedValue({}); + + wrapper.vm.fetchAuthorBySearchTerm('root'); + + return waitForPromises().then(() => { + expect(wrapper.vm.loading).toBe(false); + }); + }); + }); + + describe('template', () => { + beforeEach(() => { + wrapper.setData({ + authors: mockAuthors, + }); + + return wrapper.vm.$nextTick(); + }); + + it('renders gl-filtered-search-token component', () => { + expect(wrapper.find(GlFilteredSearchToken).exists()).toBe(true); + }); + + it('renders token item when value is selected', () => { + wrapper.setProps({ + value: { data: mockAuthors[0].username }, + }); + + return wrapper.vm.$nextTick(() => { + const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + + expect(tokenSegments).toHaveLength(3); // Author, =, "Administrator" + expect(tokenSegments.at(2).text()).toBe(mockAuthors[0].name); // "Administrator" + }); + }); + }); +}); |