@gitlab/ui
Version:
GitLab UI Components
951 lines (796 loc) • 33.7 kB
JavaScript
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { autoUpdate } from '@floating-ui/dom';
import { useMockIntersectionObserver } from '~/utils/use_mock_intersection_observer';
import GlBaseDropdown from '../base_dropdown/base_dropdown.vue';
import {
GL_DROPDOWN_SHOWN,
GL_DROPDOWN_HIDDEN,
ARROW_DOWN,
ARROW_UP,
HOME,
END,
ENTER,
POSITION_ABSOLUTE,
POSITION_FIXED,
} from '../constants';
import GlIntersectionObserver from '../../../utilities/intersection_observer/intersection_observer.vue';
import GlCollapsibleListbox, { ITEM_SELECTOR } from './listbox.vue';
import GlListboxItem from './listbox_item.vue';
import GlListboxGroup from './listbox_group.vue';
import {
mockOptions,
mockOptionsValues,
mockGroups,
mockGroupOptionsValues,
mockGroupsWithTextSrOnly,
} from './mock_data';
jest.mock('@floating-ui/dom');
autoUpdate.mockImplementation(() => {
return () => {};
});
describe('GlCollapsibleListbox', () => {
let wrapper;
const buildWrapper = (propsData, slots = {}) => {
wrapper = mount(GlCollapsibleListbox, {
propsData,
slots,
attachTo: document.body,
});
};
useMockIntersectionObserver();
const findBaseDropdown = () => wrapper.findComponent(GlBaseDropdown);
const findListContainer = () => wrapper.find('[role="listbox"]');
const findListboxItems = (root = wrapper) => root.findAllComponents(GlListboxItem);
const findListboxGroups = () => wrapper.findAllComponents(GlListboxGroup);
// eslint-disable-next-line unicorn/no-array-callback-reference
const findListItem = (index) => findListboxItems().at(index).find(ITEM_SELECTOR);
const findHeaderText = () => wrapper.find("[data-testid='listbox-header-text']");
const findSearchBox = () => wrapper.find("[data-testid='listbox-search-input']");
const findNoResultsText = () => wrapper.find("[data-testid='listbox-no-results-text']");
const findLoadingIcon = () => wrapper.find("[data-testid='listbox-search-loader']");
const findSRNumberOfResultsText = () => wrapper.find("[data-testid='listbox-number-of-results']");
const findResetButton = () => wrapper.find("[data-testid='listbox-reset-button']");
const findSelectAllButton = () => wrapper.find("[data-testid='listbox-select-all-button']");
const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
const findDropdownMenu = () => wrapper.find("[data-testid='base-dropdown-menu']");
const findDropdownToggle = () => wrapper.find("[data-testid='base-dropdown-toggle'");
it('passes custom offset to the base dropdown', () => {
const dropdownOffset = { mainAxis: 10, crossAxis: 40 };
buildWrapper({ dropdownOffset });
expect(findBaseDropdown().props('offset')).toEqual(dropdownOffset);
});
it('passes custom placement to the base dropdown', () => {
buildWrapper({ placement: 'bottom-end' });
expect(findBaseDropdown().props('placement')).toEqual('bottom-end');
});
describe('toggle text', () => {
describe.each`
toggleText | multiple | selected | expectedToggleText
${'Toggle caption'} | ${true} | ${[mockOptions[0].value]} | ${'Toggle caption'}
${''} | ${true} | ${[mockOptions[0]].value} | ${''}
${''} | ${false} | ${mockOptions[0].value} | ${mockOptions[0].text}
${''} | ${false} | ${''} | ${''}
`('when listbox', ({ toggleText, multiple, selected, expectedToggleText }) => {
beforeEach(() => {
buildWrapper({ items: mockOptions, toggleText, multiple, selected });
});
it(`is ${multiple ? 'multi' : 'single'}-select, toggleText is ${
toggleText.length ? '' : 'not '
}provided and ${selected ? 'has' : 'does not have'} selected`, () => {
expect(findBaseDropdown().props('toggleText')).toBe(expectedToggleText);
});
});
});
describe('toggle classes', () => {
describe.each`
toggleClass | multiple | selected | expectedToggleClasses
${'customClass'} | ${false} | ${mockOptions[0].value} | ${['customClass']}
${{ customClass: true }} | ${false} | ${mockOptions[0].value} | ${[{ customClass: true }]}
${['customClass']} | ${false} | ${mockOptions[0].value} | ${[['customClass']]}
${'customClass'} | ${false} | ${null} | ${['customClass', 'gl-text-gray-500!']}
${{ customClass: true }} | ${false} | ${undefined} | ${[{ customClass: true }, 'gl-text-gray-500!']}
${['customClass']} | ${false} | ${null} | ${[['customClass'], 'gl-text-gray-500!']}
${'customClass'} | ${true} | ${[mockOptions[0].value]} | ${['customClass']}
${{ customClass: true }} | ${true} | ${[mockOptions[0].value]} | ${[{ customClass: true }]}
${['customClass']} | ${true} | ${[mockOptions[0].value]} | ${[['customClass']]}
${'customClass'} | ${true} | ${null} | ${['customClass', 'gl-text-gray-500!']}
${{ customClass: true }} | ${true} | ${undefined} | ${[{ customClass: true }, 'gl-text-gray-500!']}
${['customClass']} | ${true} | ${null} | ${[['customClass'], 'gl-text-gray-500!']}
`('when listbox', ({ toggleClass, multiple, selected, expectedToggleClasses }) => {
beforeEach(() => {
buildWrapper({ items: mockOptions, toggleClass, multiple, selected });
});
it(`is ${multiple ? 'multi' : 'single'}-select and ${
selected
? 'has selected - does not set non-selected styles'
: 'does not have selected - sets the non-selected styles'
}`, () => {
expect(findBaseDropdown().props('toggleClass')).toEqual(expectedToggleClasses);
});
});
});
describe('ARIA attributes', () => {
it('should provide `toggleId` to the base dropdown and reference it in`aria-labelledby` attribute of the list container`', async () => {
await buildWrapper({ items: mockOptions });
expect(findBaseDropdown().props('toggleId')).toBe(
findListContainer().attributes('aria-labelledby')
);
});
it('should reference `listAriaLabelledby`', async () => {
const listAriaLabelledBy = 'first-label-id second-label-id';
await buildWrapper({ items: mockOptions, listAriaLabelledBy });
expect(findListContainer().attributes('aria-labelledby')).toBe(listAriaLabelledBy);
});
});
describe('selecting items', () => {
describe('multi-select', () => {
beforeEach(() => {
buildWrapper({
multiple: true,
selected: [mockOptions[1].value, mockOptions[2].value],
items: mockOptions,
});
});
it('should render items as selected when `selected` provided', () => {
expect(findListboxItems().at(1).props('isSelected')).toBe(true);
expect(findListboxItems().at(2).props('isSelected')).toBe(true);
});
it('should deselect previously selected', async () => {
findListboxItems().at(1).vm.$emit('select', false);
await nextTick();
expect(wrapper.emitted('select')[0][0]).toEqual([mockOptions[2].value]);
});
it('should add to selection', async () => {
findListboxItems().at(0).vm.$emit('select', true);
await nextTick();
expect(wrapper.emitted('select')[0][0]).toEqual(
// The first three items should now be selected.
expect.arrayContaining(mockOptions.slice(0, 3).map(({ value }) => value))
);
});
});
describe('single-select', () => {
beforeEach(() => {
buildWrapper({ selected: mockOptions[1].value, items: mockOptions });
});
it('should throw an error when array of selections is provided', () => {
expect(() => {
buildWrapper({
selected: [mockOptions[1].value, mockOptions[2].value],
items: mockOptions,
});
}).toThrow('To allow multi-selection, please, set "multiple" property to "true"');
expect(wrapper).toHaveLoggedVueErrors();
});
it('should render item as selected when `selected` provided', () => {
expect(findListboxItems().at(1).props('isSelected')).toBe(true);
});
it('should deselect previously selected and select a new item', async () => {
findListboxItems().at(2).vm.$emit('select', true);
await nextTick();
expect(wrapper.emitted('select')[0][0]).toEqual(mockOptions[2].value);
});
it('close dropdown for single selection', () => {
jest.spyOn(wrapper.vm, 'closeAndFocus');
expect(wrapper.vm.closeAndFocus).not.toHaveBeenCalled();
findListboxItems().at(2).vm.$emit('select', true);
expect(wrapper.vm.closeAndFocus).toHaveBeenCalled();
});
});
describe('with groups', () => {
const selected = mockGroups[1].options[1].value;
beforeEach(() => {
buildWrapper({ selected, items: mockGroups });
});
it('should render item as selected when `selected` provided', () => {
expect(findListboxItems().at(3).props('isSelected')).toBe(true);
});
it('should deselect previously selected and select a new item', async () => {
findListboxItems().at(0).vm.$emit('select', true);
await nextTick();
expect(wrapper.emitted('select')[0][0]).toEqual(mockGroups[0].options[0].value);
});
});
});
describe('onShow', () => {
let focusSpy;
const showDropdown = async ({ searchable = false } = {}) => {
buildWrapper({
multiple: true,
items: mockOptions,
selected: [mockOptions[2].value, mockOptions[1].value],
searchable,
});
if (searchable) {
focusSpy = jest.spyOn(wrapper.vm.$refs.searchBox, 'focusInput');
}
findBaseDropdown().vm.$emit(GL_DROPDOWN_SHOWN);
await nextTick();
await nextTick();
};
it('should re-emit the event', async () => {
await showDropdown();
expect(wrapper.emitted(GL_DROPDOWN_SHOWN)).toHaveLength(1);
});
it('should focus the first selected item', async () => {
await showDropdown();
// eslint-disable-next-line unicorn/no-array-callback-reference
expect(findListboxItems().at(1).find(ITEM_SELECTOR).element).toHaveFocus();
});
it('should focus the search input when search is enabled', async () => {
await showDropdown({ searchable: true });
expect(focusSpy).toHaveBeenCalled();
});
});
describe('onHide', () => {
beforeEach(() => {
buildWrapper();
findBaseDropdown().vm.$emit(GL_DROPDOWN_HIDDEN);
});
it('should re-emit the event', () => {
expect(wrapper.emitted(GL_DROPDOWN_HIDDEN)).toHaveLength(1);
});
});
describe('navigating the items', () => {
let firstItem;
let secondItem;
let thirdItem;
beforeEach(() => {
// These tests are more easily written with a small list of items.
buildWrapper({ items: mockOptions.slice(0, 3) });
findBaseDropdown().vm.$emit(GL_DROPDOWN_SHOWN);
firstItem = findListItem(0);
secondItem = findListItem(1);
thirdItem = findListItem(2);
});
it('should move the focus down the list of items on `ARROW_DOWN` and stop on the last item', async () => {
expect(firstItem.element).toHaveFocus();
await firstItem.trigger('keydown', { code: ARROW_DOWN });
expect(secondItem.element).toHaveFocus();
await secondItem.trigger('keydown', { code: ARROW_DOWN });
expect(thirdItem.element).toHaveFocus();
await thirdItem.trigger('keydown', { code: ARROW_DOWN });
expect(thirdItem.element).toHaveFocus();
});
it('should move the focus up the list of items on `ARROW_UP` and stop on the first item', async () => {
await firstItem.trigger('keydown', { code: ARROW_DOWN });
await secondItem.trigger('keydown', { code: ARROW_DOWN });
expect(thirdItem.element).toHaveFocus();
await thirdItem.trigger('keydown', { code: ARROW_UP });
expect(secondItem.element).toHaveFocus();
await secondItem.trigger('keydown', { code: ARROW_UP });
expect(firstItem.element).toHaveFocus();
await firstItem.trigger('keydown', { code: ARROW_UP });
expect(firstItem.element).toHaveFocus();
});
it('should move focus to the last item on `END` keydown', async () => {
expect(firstItem.element).toHaveFocus();
await firstItem.trigger('keydown', { code: END });
expect(thirdItem.element).toHaveFocus();
await thirdItem.trigger('keydown', { code: END });
expect(thirdItem.element).toHaveFocus();
});
it('should move focus to the first item on `HOME` keydown', async () => {
await firstItem.trigger('keydown', { code: ARROW_DOWN });
await secondItem.trigger('keydown', { code: ARROW_DOWN });
expect(thirdItem.element).toHaveFocus();
await thirdItem.trigger('keydown', { code: HOME });
expect(firstItem.element).toHaveFocus();
await thirdItem.trigger('keydown', { code: HOME });
expect(firstItem.element).toHaveFocus();
});
describe('when `searchable` is enabled', () => {
let searchboxInput;
beforeEach(() => {
buildWrapper({ items: mockOptions, searchable: true });
findBaseDropdown().vm.$emit(GL_DROPDOWN_SHOWN);
firstItem = findListItem(0);
searchboxInput = findSearchBox().find('input');
});
it('should move focus to the first item on search input `ARROW_DOWN`', async () => {
expect(searchboxInput.element).toHaveFocus();
searchboxInput.trigger('keydown', { code: ARROW_DOWN });
expect(firstItem.element).toHaveFocus();
});
it('should move focus to the search input on first item `ARROW_UP', async () => {
searchboxInput.trigger('keydown', { code: ARROW_DOWN });
firstItem.trigger('keydown', { code: ARROW_UP });
expect(searchboxInput.element).toHaveFocus();
});
it('should not move focus away from the input on `HOME` and `END`', async () => {
expect(searchboxInput.element).toHaveFocus();
await searchboxInput.trigger('keydown', { code: HOME });
expect(searchboxInput.element).toHaveFocus();
await searchboxInput.trigger('keydown', { code: END });
expect(searchboxInput.element).toHaveFocus();
});
describe('pressing Enter on the input', () => {
const keydownSpy = jest.fn();
beforeEach(() => {
keydownSpy.mockClear();
wrapper.element.addEventListener('keydown', keydownSpy);
});
afterEach(() => {
wrapper.element.removeEventListener('keydown', keydownSpy);
});
it('does not submit any ancestor form', async () => {
searchboxInput.trigger('keydown', { key: ENTER });
const [event] = keydownSpy.mock.calls[0];
expect(event.key).toBe(ENTER);
expect(event.defaultPrevented).toBe(true);
});
});
});
});
describe('when the header prop is provided', () => {
const headerContent = 'Header Content';
it('renders it', () => {
buildWrapper({ headerText: headerContent, items: mockOptions });
expect(findHeaderText().text()).toContain(headerContent);
});
it("uses the generated header ID as the list's aria-labelledby attribute", () => {
buildWrapper({ headerText: headerContent, items: mockOptions });
expect(findListContainer().attributes('aria-labelledby')).toBe(
findHeaderText().attributes('id')
);
});
it('if a custom list label is passed, it overrides the header ID', () => {
const listAriaLabelledBy = 'listAriaLabelledBy';
buildWrapper({ listAriaLabelledBy, headerText: headerContent, items: mockOptions });
expect(findListContainer().attributes('aria-labelledby')).toBe(listAriaLabelledBy);
});
});
describe('when the footer slot content is provided', () => {
const footerContent = 'Footer Content';
const slots = { footer: footerContent };
it('renders it', () => {
buildWrapper({}, slots);
expect(wrapper.text()).toContain(footerContent);
});
});
describe('with groups', () => {
it('renders groups of items', () => {
buildWrapper({ items: mockGroups });
const groups = findListboxGroups();
expect(groups.length).toBe(mockGroups.length);
const expectedNameProps = mockGroups.map((group) => group.text);
const actualNameProps = groups.wrappers.map((group) => group.props('name'));
expect(actualNameProps).toEqual(expectedNameProps);
mockGroups.forEach((group, i) => {
expect(findListboxItems(groups.at(i))).toHaveLength(group.options.length);
});
});
it('passes the `textSrOnly` prop', () => {
buildWrapper({ items: mockGroupsWithTextSrOnly });
const groups = findListboxGroups();
const expectedTextSrOnlyProps = mockGroupsWithTextSrOnly.map(
(group) => group.textSrOnly ?? false
);
const actualTextSrOnlyProps = groups.wrappers.map((group) => group.props('textSrOnly'));
expect(actualTextSrOnlyProps).toEqual(expectedTextSrOnlyProps);
});
});
describe('when `searchable` is enabled', () => {
it('should render the search box', () => {
buildWrapper({ items: mockOptions, searchable: true });
expect(findSearchBox().exists()).toBe(true);
});
it('should emit the search value when typing in the search box', async () => {
buildWrapper({ items: mockOptions, searchable: true });
const searchStr = 'search value';
findSearchBox().vm.$emit('input', searchStr);
await nextTick();
expect(wrapper.emitted('search')[0][0]).toEqual(searchStr);
});
it('should not render the loading icon and render the list if NOT searching', () => {
buildWrapper({ items: mockOptions, searchable: true });
expect(findLoadingIcon().exists()).toBe(false);
expect(findListContainer().exists()).toBe(true);
});
it('should render the loading icon and NOT render the list when searching', () => {
buildWrapper({ items: mockOptions, searchable: true, searching: true });
expect(findLoadingIcon().exists()).toBe(true);
expect(findListContainer().exists()).toBe(false);
});
it('should display `noResultText` if no items found', () => {
const noResultsText = 'Nothing found';
buildWrapper({ items: [], searchable: true, searching: false, noResultsText });
expect(findLoadingIcon().exists()).toBe(false);
expect(findListContainer().exists()).toBe(false);
expect(findNoResultsText().text()).toBe(noResultsText);
});
describe('Screen reader text with number of search results', () => {
it('when the #search-summary-sr-only slot content is empty', () => {
buildWrapper({ items: mockOptions, searchable: true, searching: false });
expect(findSRNumberOfResultsText().text()).toBe('12 results');
});
it('when the #search-summary-sr-only slot content is provided', () => {
const searchResultsContent = 'Found 5 results';
const slots = { 'search-summary-sr-only': searchResultsContent };
buildWrapper({ items: mockOptions, searchable: true, searching: false }, slots);
expect(findSRNumberOfResultsText().text()).toBe(searchResultsContent);
});
it('should not display SR text when no matching results', () => {
buildWrapper({ items: [], searchable: true, searching: false });
expect(findSRNumberOfResultsText().exists()).toBe(false);
});
});
});
describe('with a reset action', () => {
it('throws when enabling the reset action without a header', () => {
expect(() => {
buildWrapper({ resetButtonLabel: 'Unassign' });
}).toThrow(
'The reset button cannot be rendered without a header. Either provide a header via the headerText prop, or do not provide the resetButtonLabel prop.'
);
expect(wrapper).toHaveLoggedVueErrors();
});
it.each`
description | props
${'multi-select'} | ${{ multiple: true, selected: mockOptionsValues }}
${'single-select'} | ${{ multiple: false, selected: mockOptions[1].value }}
`(
'shows the button if the label is provided and the selection is complete in $description mode',
({ props }) => {
buildWrapper({
headerText: 'Select assignee',
resetButtonLabel: 'Unassign',
items: mockOptions,
...props,
});
expect(findResetButton().exists()).toBe(true);
}
);
it('shows the button if the label is provided and only one option is selected in multi select', () => {
buildWrapper({
headerText: 'Select assignee',
resetButtonLabel: 'Unassign',
items: mockOptions,
multiple: true,
selected: mockOptionsValues.slice(0, 1),
});
expect(findResetButton().exists()).toBe(true);
});
it.each`
description | props
${'multi-select'} | ${{ multiple: true, selected: [] }}
${'single-select'} | ${{ multiple: false, selected: [] }}
${'single-select'} | ${{ multiple: false, selected: undefined }}
`('hides the button if the selection is empty in $description mode', ({ props }) => {
buildWrapper({
headerText: 'Select assignee',
resetButtonLabel: 'Unassign',
items: mockOptions,
...props,
});
expect(findResetButton().exists()).toBe(false);
});
it('hides reset button if dropdown has no items', () => {
buildWrapper({
headerText: 'Select assignee',
resetButtonLabel: 'Unassign',
selected: mockOptions[1].value,
items: [],
multiple: true,
});
expect(findResetButton().exists()).toBe(false);
});
it('on click, emits the `reset` event but does not call `closeAndFocus`', () => {
buildWrapper({
headerText: 'Select assignee',
resetButtonLabel: 'Unassign',
selected: mockOptionsValues,
items: mockOptions,
multiple: true,
});
jest.spyOn(wrapper.vm, 'closeAndFocus');
expect(wrapper.emitted('reset')).toBe(undefined);
expect(wrapper.vm.closeAndFocus).not.toHaveBeenCalled();
findResetButton().trigger('click');
expect(wrapper.emitted('reset')).toHaveLength(1);
expect(wrapper.vm.closeAndFocus).not.toHaveBeenCalled();
});
describe('with groups', () => {
it.each`
description | props
${'multi-select'} | ${{ multiple: true, selected: mockGroupOptionsValues }}
${'single-select'} | ${{ multiple: false, selected: mockGroups[0].options[0].value }}
`(
'shows the button if the label is provided and the selection is complete in $description mode',
({ props }) => {
buildWrapper({
headerText: 'Select assignee',
resetButtonLabel: 'Unassign',
items: mockGroups,
...props,
});
expect(findResetButton().exists()).toBe(true);
}
);
});
});
describe('with select all action', () => {
it('throws an error when enabling the select action without a header', () => {
expect(() => {
buildWrapper({ showSelectAllButtonLabel: 'Select All' });
}).toThrow(
'The select all button cannot be rendered without a header. Either provide a header via the headerText prop, or do not provide the showSelectAllButtonLabel prop.'
);
expect(wrapper).toHaveLoggedVueErrors();
});
describe.each`
multiple | resetVisible | selectAllVisible | selected
${false} | ${false} | ${false} | ${[]}
${false} | ${false} | ${false} | ${undefined}
${true} | ${false} | ${true} | ${[]}
`(
'when label is provided, selection is empty and multiple option is $multiple',
({ multiple, resetVisible, selectAllVisible, selected }) => {
beforeEach(() => {
buildWrapper({
headerText: 'Select assignee',
resetButtonLabel: 'Unassign',
showSelectAllButtonLabel: 'Select All',
items: mockOptions,
selected,
multiple,
});
});
it(`${selectAllVisible ? 'shows' : 'does not show'} the Select all button`, () => {
expect(findSelectAllButton().exists()).toBe(selectAllVisible);
});
it(`${resetVisible ? 'shows' : 'does not show'} the Reset button`, () => {
expect(findResetButton().exists()).toBe(resetVisible);
});
}
);
it('when label is provided and one option is selected in multiselect', () => {
buildWrapper({
headerText: 'Select assignee',
resetButtonLabel: 'Unassign',
showSelectAllButtonLabel: 'Select All',
items: mockOptions,
selected: mockOptionsValues.slice(0, 1),
multiple: true,
});
expect(findSelectAllButton().exists()).toBe(true);
expect(findResetButton().exists()).toBe(false);
});
it('hides select all button if dropdown has no items', () => {
buildWrapper({
headerText: 'Select assignee',
resetButtonLabel: 'Unassign',
showSelectAllButtonLabel: 'Select All',
items: [],
multiple: true,
});
expect(findSelectAllButton().exists()).toBe(false);
});
it('hides select all button if all items are selected', () => {
buildWrapper({
headerText: 'Select assignee',
resetButtonLabel: 'Unassign',
showSelectAllButtonLabel: 'Select All',
selected: mockOptionsValues,
items: mockOptions,
multiple: true,
});
expect(findSelectAllButton().exists()).toBe(false);
});
it('has the label text "Select All" if the label is provided and some items are selected', () => {
buildWrapper({
headerText: 'Select assignee',
resetButtonLabel: 'Unassign',
showSelectAllButtonLabel: 'Select All',
selected: [mockOptions[1].value],
items: mockOptions,
multiple: true,
});
expect(findSelectAllButton().text()).toBe('Select All');
});
it('has the label text "Select All" if the label is provided and the selection is empty', () => {
buildWrapper({
headerText: 'Select assignee',
resetButtonLabel: 'Unassign',
showSelectAllButtonLabel: 'Select All',
selected: [],
items: mockOptions,
multiple: true,
});
expect(findSelectAllButton().text()).toBe('Select All');
});
it('on click, emits the `select-all` event and calls `closeAndFocus`', () => {
buildWrapper({
headerText: 'Select assignee',
resetButtonLabel: 'Unassign',
showSelectAllButtonLabel: 'Select All',
selected: [],
items: mockOptions,
multiple: true,
});
expect(wrapper.emitted('select-all')).toBe(undefined);
findSelectAllButton().trigger('click');
expect(wrapper.emitted('select-all')).toHaveLength(1);
});
describe('with groups', () => {
it('hides select all button if all items are selected', () => {
buildWrapper({
headerText: 'Select assignee',
resetButtonLabel: 'Unassign',
showSelectAllButtonLabel: 'Select All',
selected: mockGroupOptionsValues,
items: mockGroups,
multiple: true,
});
expect(findSelectAllButton().exists()).toBe(false);
});
});
});
describe('when `infiniteScroll` prop is `true`', () => {
it('should throw an error when items are groups', () => {
expect(() => {
buildWrapper({
items: mockGroups,
infiniteScroll: true,
});
}).toThrow(
'Infinite scroll does not support groups. Please set the "infiniteScroll" prop to "false"'
);
expect(wrapper).toHaveLoggedVueErrors();
});
it('renders `GlIntersectionObserver` component', () => {
buildWrapper({
headerText: 'Select assignee',
items: mockOptions,
infiniteScroll: true,
});
expect(findIntersectionObserver().exists()).toBe(true);
});
describe('when bottom of listbox is reached', () => {
it('emits `bottom-reached` event', () => {
buildWrapper({
items: mockOptions,
infiniteScroll: true,
});
findIntersectionObserver().vm.$emit('appear');
expect(wrapper.emitted('bottom-reached')).toEqual([[]]);
});
});
describe('when `loading` prop is `true`', () => {
it('does not render `GlIntersectionObserver` component', () => {
buildWrapper({
items: mockOptions,
infiniteScroll: true,
loading: true,
});
expect(findIntersectionObserver().exists()).toBe(false);
});
});
describe('when `searching` prop is `true`', () => {
it('does not render `GlIntersectionObserver` component', () => {
buildWrapper({
items: mockOptions,
infiniteScroll: true,
searching: true,
});
expect(findIntersectionObserver().exists()).toBe(false);
});
});
describe('when `infiniteScrollLoading` prop is `true`', () => {
beforeEach(() => {
buildWrapper({
items: mockOptions,
infiniteScroll: true,
infiniteScrollLoading: true,
});
});
it('shows loading icon', () => {
expect(wrapper.find('[data-testid="listbox-infinite-scroll-loader"]').exists()).toBe(true);
});
it('does not render `GlIntersectionObserver` component', () => {
expect(findIntersectionObserver().exists()).toBe(false);
});
});
describe('when `totalItems` prop is set', () => {
it('adds `aria-setsize` and `aria-posinset` attributes to listbox items', () => {
const totalItems = mockOptions.length;
buildWrapper({
items: mockOptions,
infiniteScroll: true,
totalItems,
});
findListboxItems().wrappers.forEach((listboxItem, index) => {
expect(listboxItem.attributes('aria-setsize')).toBe(totalItems.toString());
expect(listboxItem.attributes('aria-posinset')).toBe((index + 1).toString());
});
});
});
describe('when `totalItems` prop is not set', () => {
it('does not add `aria-setsize` and `aria-posinset` attributes to listbox items', () => {
buildWrapper({
items: mockOptions,
infiniteScroll: true,
});
findListboxItems().wrappers.forEach((listboxItem) => {
expect(listboxItem.attributes('aria-setsize')).toBe(undefined);
expect(listboxItem.attributes('aria-posinset')).toBe(undefined);
});
});
});
});
describe('when `infiniteScroll` prop is `false`', () => {
it('does not render `GlIntersectionObserver` component', () => {
buildWrapper({
items: mockOptions,
infiniteScroll: false,
});
expect(findIntersectionObserver().exists()).toBe(false);
});
});
describe('items test IDs without groups', () => {
beforeEach(() => {
buildWrapper({
items: mockOptions,
});
});
it.each(mockOptions)('sets the test ID for %s', (option) => {
expect(wrapper.find(`[data-testid="listbox-item-${option.value}"]`).exists()).toBe(true);
});
});
describe('items test IDs with groups', () => {
beforeEach(() => {
buildWrapper({
items: mockGroups,
});
});
it.each([...mockGroups[0].options, ...mockGroups[1].options])(
'sets the test ID for %s',
(option) => {
expect(wrapper.find(`[data-testid="listbox-item-${option.value}"]`).exists()).toBe(true);
}
);
});
describe('fluid width', () => {
it('is disabled by default', () => {
buildWrapper();
expect(findBaseDropdown().props('fluidWidth')).toBe(false);
});
it('is enabled when `fluidWidth` is `true`', () => {
buildWrapper({ fluidWidth: true });
expect(findBaseDropdown().props('fluidWidth')).toBe(true);
});
});
describe('positioningStrategy', () => {
it.each([POSITION_ABSOLUTE, POSITION_FIXED])(
'passes the %s positioning strategy to the base dropdown',
(positioningStrategy) => {
buildWrapper({ positioningStrategy });
expect(findBaseDropdown().props('positioningStrategy')).toBe(positioningStrategy);
}
);
});
describe('startOpened', () => {
it('should open dropdown on render when startOpened is true', async () => {
buildWrapper({ items: mockOptions, startOpened: true });
await nextTick();
expect(findDropdownMenu().classes()).toContain('gl-display-block!');
});
it('should not open dropdown on render as default', async () => {
buildWrapper({ items: mockOptions });
await nextTick();
expect(findDropdownMenu().classes()).not.toContain('gl-display-block!');
});
});
it('focuses the toggle when closed by ESC key while item had focus', async () => {
buildWrapper({
selected: mockOptions[1].value,
items: mockOptions,
startOpened: true,
});
await nextTick();
findListItem(1).trigger('keydown.esc');
await nextTick();
expect(findDropdownToggle().element).toHaveFocus();
});
});