diff options
Diffstat (limited to 'spec/frontend/vue_shared/components/entity_select/entity_select_spec.js')
-rw-r--r-- | spec/frontend/vue_shared/components/entity_select/entity_select_spec.js | 268 |
1 files changed, 268 insertions, 0 deletions
diff --git a/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js b/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js new file mode 100644 index 00000000000..6b98f6c5e89 --- /dev/null +++ b/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js @@ -0,0 +1,268 @@ +import { nextTick } from 'vue'; +import { GlCollapsibleListbox, GlFormGroup } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import EntitySelect from '~/vue_shared/components/entity_select/entity_select.vue'; +import { QUERY_TOO_SHORT_MESSAGE } from '~/vue_shared/components/entity_select/constants'; +import waitForPromises from 'helpers/wait_for_promises'; + +describe('EntitySelect', () => { + let wrapper; + let fetchItemsMock; + let fetchInitialSelectionTextMock; + + // Mocks + const itemMock = { + text: 'selectedGroup', + value: '1', + }; + + // Stubs + const GlAlert = { + template: '<div><slot /></div>', + }; + + // Props + const label = 'label'; + const inputName = 'inputName'; + const inputId = 'inputId'; + const headerText = 'headerText'; + const defaultToggleText = 'defaultToggleText'; + + // Finders + const findListbox = () => wrapper.findComponent(GlCollapsibleListbox); + const findInput = () => wrapper.findByTestId('input'); + + // Helpers + const createComponent = ({ props = {}, slots = {}, stubs = {} } = {}) => { + wrapper = shallowMountExtended(EntitySelect, { + propsData: { + label, + inputName, + inputId, + headerText, + defaultToggleText, + fetchItems: fetchItemsMock, + ...props, + }, + stubs: { + GlAlert, + EntitySelect, + ...stubs, + }, + slots, + }); + }; + const openListbox = () => findListbox().vm.$emit('shown'); + const search = (searchString) => findListbox().vm.$emit('search', searchString); + const selectGroup = async () => { + openListbox(); + await nextTick(); + findListbox().vm.$emit('select', itemMock.value); + return nextTick(); + }; + + beforeEach(() => { + fetchItemsMock = jest.fn().mockImplementation(() => ({ items: [itemMock], totalPages: 1 })); + }); + + describe('on mount', () => { + it('calls the fetch function when the listbox is opened', async () => { + createComponent(); + openListbox(); + await nextTick(); + + expect(fetchItemsMock).toHaveBeenCalledTimes(1); + }); + + it("fetches the initially selected value's name", async () => { + fetchInitialSelectionTextMock = jest.fn().mockImplementation(() => itemMock.text); + createComponent({ + props: { + fetchInitialSelectionText: fetchInitialSelectionTextMock, + initialSelection: itemMock.value, + }, + }); + await nextTick(); + + expect(fetchInitialSelectionTextMock).toHaveBeenCalledTimes(1); + expect(findListbox().props('toggleText')).toBe(itemMock.text); + }); + }); + + it("renders the error slot's content", () => { + const selector = 'data-test-id="error-element"'; + createComponent({ + slots: { + error: `<div ${selector} />`, + }, + }); + + expect(wrapper.find(`[${selector}]`).exists()).toBe(true); + }); + + it('renders the label slot if provided', () => { + const testid = 'label-slot'; + createComponent({ + slots: { + label: `<div data-testid="${testid}" />`, + }, + stubs: { + GlFormGroup, + }, + }); + + expect(wrapper.findByTestId(testid).exists()).toBe(true); + }); + + describe('selection', () => { + it('uses the default toggle text while no group is selected', () => { + createComponent(); + + expect(findListbox().props('toggleText')).toBe(defaultToggleText); + }); + + describe('once a group is selected', () => { + it(`uses the selected group's name as the toggle text`, async () => { + createComponent(); + await selectGroup(); + + expect(findListbox().props('toggleText')).toBe(itemMock.text); + }); + + it(`uses the selected group's ID as the listbox' and input value`, async () => { + createComponent(); + await selectGroup(); + + expect(findListbox().attributes('selected')).toBe(itemMock.value); + expect(findInput().attributes('value')).toBe(itemMock.value); + }); + + it(`on reset, falls back to the default toggle text`, async () => { + createComponent(); + await selectGroup(); + + findListbox().vm.$emit('reset'); + await nextTick(); + + expect(findListbox().props('toggleText')).toBe(defaultToggleText); + }); + }); + }); + + describe('search', () => { + it('sets `searching` to `true` when first opening the dropdown', async () => { + createComponent(); + + expect(findListbox().props('searching')).toBe(false); + + openListbox(); + await nextTick(); + + expect(findListbox().props('searching')).toBe(true); + }); + + it('sets `searching` to `true` while searching', async () => { + createComponent(); + + expect(findListbox().props('searching')).toBe(false); + + search('foo'); + await nextTick(); + + expect(findListbox().props('searching')).toBe(true); + }); + + it('fetches groups matching the search string', async () => { + const searchString = 'searchString'; + createComponent(); + openListbox(); + + expect(fetchItemsMock).toHaveBeenCalledTimes(1); + + fetchItemsMock.mockImplementation(() => ({ items: [], totalPages: 1 })); + search(searchString); + await nextTick(); + + expect(fetchItemsMock).toHaveBeenCalledTimes(2); + }); + + it('shows a notice if the search query is too short', async () => { + const searchString = 'a'; + createComponent(); + openListbox(); + search(searchString); + await nextTick(); + + expect(fetchItemsMock).toHaveBeenCalledTimes(1); + expect(findListbox().props('noResultsText')).toBe(QUERY_TOO_SHORT_MESSAGE); + }); + }); + + describe('pagination', () => { + const searchString = 'searchString'; + + beforeEach(async () => { + let requestCount = 0; + fetchItemsMock.mockImplementation((searchQuery, page) => { + requestCount += 1; + return { + items: [ + { + text: `Group [page: ${page} - search: ${searchQuery}]`, + value: `id:${requestCount}`, + }, + ], + totalPages: 3, + }; + }); + createComponent(); + openListbox(); + findListbox().vm.$emit('bottom-reached'); + return nextTick(); + }); + + it('fetches the next page when bottom is reached', () => { + expect(fetchItemsMock).toHaveBeenCalledTimes(2); + expect(fetchItemsMock).toHaveBeenLastCalledWith('', 2); + }); + + it('fetches the first page when the search query changes', async () => { + search(searchString); + await nextTick(); + + expect(fetchItemsMock).toHaveBeenCalledTimes(3); + expect(fetchItemsMock).toHaveBeenLastCalledWith(searchString, 1); + }); + + it('retains the search query when infinite scrolling', async () => { + search(searchString); + await nextTick(); + findListbox().vm.$emit('bottom-reached'); + await nextTick(); + + expect(fetchItemsMock).toHaveBeenCalledTimes(4); + expect(fetchItemsMock).toHaveBeenLastCalledWith(searchString, 2); + }); + + it('pauses infinite scroll after fetching the last page', async () => { + expect(findListbox().props('infiniteScroll')).toBe(true); + + findListbox().vm.$emit('bottom-reached'); + await waitForPromises(); + + expect(findListbox().props('infiniteScroll')).toBe(false); + }); + + it('resumes infinite scroll when search query changes', async () => { + findListbox().vm.$emit('bottom-reached'); + await waitForPromises(); + + expect(findListbox().props('infiniteScroll')).toBe(false); + + search(searchString); + await waitForPromises(); + + expect(findListbox().props('infiniteScroll')).toBe(true); + }); + }); +}); |