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: '
',
};
// 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: ``,
},
});
expect(wrapper.find(`[${selector}]`).exists()).toBe(true);
});
it('renders the label slot if provided', () => {
const testid = 'label-slot';
createComponent({
slots: {
label: ``,
},
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('emits `input` event with the select value', async () => {
createComponent();
await selectGroup();
expect(wrapper.emitted('input')[0]).toEqual(['1']);
});
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);
});
it('emits `input` event with `null` on reset', async () => {
createComponent();
await selectGroup();
findListbox().vm.$emit('reset');
await nextTick();
expect(wrapper.emitted('input')[2]).toEqual([null]);
});
});
});
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(() => {
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);
});
});
});