import { Breakpoint } from '../common';
import { render, screen, userEvent, waitFor, mockMatchMedia } from '../test-utils';
import Select from '.';
import { SelectItemWithPlaceholder } from './Select';
mockMatchMedia();
function enableDesktopScreen() {
window.innerWidth = Breakpoint.LARGE;
}
function enableMobileScreen() {
window.innerWidth = Breakpoint.EXTRA_SMALL;
}
const mockScrollIntoView = jest.fn();
const originalRAF = global.requestAnimationFrame;
describe('Select', () => {
const props = {
onChange: jest.fn(),
options: [
{ value: 0, label: 'yo' },
{ value: 1, label: 'dawg' },
{ value: 2, label: 'boi' },
],
};
beforeEach(() => {
global.requestAnimationFrame = (callback) => setTimeout(callback, 16);
Element.prototype.scrollIntoView = mockScrollIntoView;
enableDesktopScreen();
jest.clearAllMocks();
});
afterEach(() => {
global.requestAnimationFrame = originalRAF;
});
const openSelect = async (container: HTMLElement) => {
const button = screen.getByRole('button');
await userEvent.click(button);
};
describe('search property', () => {
it('should focus input when dropdown is open', async () => {
const { container } = render();
await openSelect(container);
const input = screen.getByPlaceholderText('Search...');
expect(input).toHaveFocus();
});
it('should pass id to search box', async () => {
const { container } = render();
await openSelect(container);
const input = screen.getByPlaceholderText('Search...');
expect(input).toHaveAttribute('id', 'dummy-searchbox');
});
it('should filter the options with the default filter function', async () => {
const { container } = render();
await openSelect(container);
const input = screen.getByPlaceholderText('Search...');
await userEvent.type(input, 'yo');
// Not ideal, this higlights accessibility issues.
expect(container.querySelector('.np-dropdown-item.np-dropdown-item--focused')).toStrictEqual(
container.querySelector('.np-dropdown-item'),
);
expect(screen.getByText('yo')).toBeInTheDocument();
});
describe('when options contain searchables', () => {
let input: HTMLElement;
const searchableOptions = [
{
value: 0,
label: 'first_label',
note: 'first_note',
secondary: 'first_secondary',
currency: 'gbp',
searchStrings: ['first_search_string'],
},
{
value: 1,
label: 'second_label',
note: 'second_note',
secondary: 'second_secondary',
currency: 'usd',
searchStrings: ['second_search_string'],
},
{
value: 2,
label: 'third_label',
note: 'third_note',
secondary: 'third_secondary',
currency: 'eur',
searchStrings: ['third_search_string'],
},
];
beforeEach(async () => {
const { container } = render();
await openSelect(container);
input = screen.getByPlaceholderText('Search...');
});
describe('when searching by label', () => {
it('should display the search result', async () => {
await userEvent.type(input, 'second_label');
expect(screen.queryByText('first_label')).not.toBeInTheDocument();
expect(screen.getByText('second_label')).toBeInTheDocument();
expect(screen.queryByText('third_label')).not.toBeInTheDocument();
});
});
describe('when searching by note', () => {
it('should display the search result', async () => {
await userEvent.type(input, 'third_note');
expect(screen.queryByText('first_label')).not.toBeInTheDocument();
expect(screen.queryByText('second_label')).not.toBeInTheDocument();
expect(screen.getByText('third_label')).toBeInTheDocument();
});
});
describe('when searching by secondary', () => {
it('should display the search result', async () => {
await userEvent.type(input, 'first_secondary');
expect(screen.getByText('first_label')).toBeInTheDocument();
expect(screen.queryByText('second_label')).not.toBeInTheDocument();
expect(screen.queryByText('third_label')).not.toBeInTheDocument();
});
});
describe('when searching by currency', () => {
it('should display the search result', async () => {
await userEvent.type(input, 'usd');
expect(screen.queryByText('first_label')).not.toBeInTheDocument();
expect(screen.getByText('second_label')).toBeInTheDocument();
expect(screen.queryByText('third_label')).not.toBeInTheDocument();
});
});
describe('when searching by searchStrings', () => {
it('should display the search result', async () => {
await userEvent.type(input, 'third_search_string');
expect(screen.queryByText('first_label')).not.toBeInTheDocument();
expect(screen.queryByText('second_label')).not.toBeInTheDocument();
expect(screen.getByText('third_label')).toBeInTheDocument();
});
});
});
it('should be able to search disabled options', async () => {
const { container } = render(
,
);
await openSelect(container);
const input = screen.getByPlaceholderText('Search...');
await userEvent.type(input, 'Disabled');
expect(screen.queryByText('Enabled Option')).not.toBeInTheDocument();
expect(screen.getByText('Disabled Option')).toBeInTheDocument();
});
it('should filter the options with the default filter function in their currency attribute', async () => {
const { container } = render(
,
);
await openSelect(container);
const input = screen.getByPlaceholderText('Search...');
await userEvent.type(input, 'HUF');
expect(container.querySelector('.np-dropdown-item.np-dropdown-item--focused')).toStrictEqual(
container.querySelector('.np-dropdown-item'),
);
expect(
screen.getByText('Hungarian forint').parentElement?.parentElement?.parentElement
?.parentElement,
).toHaveClass('np-dropdown-item clickable np-dropdown-item--focused');
});
it('should include searchable strings in option search if present', async () => {
const { container } = render(
,
);
await openSelect(container);
const input = screen.getByPlaceholderText('Search...');
await userEvent.type(input, 'Tallinn');
expect(container.querySelector('.np-dropdown-item.np-dropdown-item--focused')).toStrictEqual(
container.querySelector('.np-dropdown-item'),
);
expect(screen.getByText('Estonia').parentElement?.parentElement).toHaveClass(
'np-dropdown-item clickable np-dropdown-item--focused',
);
});
it('should filter the options with a custom search function', async () => {
const searchFunction = jest.fn();
const { container } = render();
await openSelect(container);
const input = screen.getByPlaceholderText('Search...');
await userEvent.type(input, 'o');
expect(searchFunction).toHaveBeenCalledTimes(3);
});
it('should filter the options with a custom search function with 3 results', async () => {
const searchFunction = (option: SelectItemWithPlaceholder, searchValue: string) =>
typeof option.label === 'string' && option.label.includes(searchValue);
const { container } = render();
await openSelect(container);
const input = screen.getByPlaceholderText('Search...');
await userEvent.type(input, 'o');
expect(screen.getByRole('listbox').children).toHaveLength(3);
});
});
it('show list of options in dropdown (Panel) on desktop', async () => {
const { container } = render();
await openSelect(container);
const drawer = document.querySelector('.np-panel');
expect(drawer).toBeInTheDocument();
});
it('show list of options in Bottom Sheet when on mobile', async () => {
enableMobileScreen();
const { container } = render();
await openSelect(container);
const bottomSheet = await screen.findByRole('dialog');
expect(bottomSheet).toBeInTheDocument();
});
it('show list of options in Drawer when on mobile and search in enabled', async () => {
enableMobileScreen();
const { container } = render();
await openSelect(container);
await waitFor(() => {
const drawer = document.querySelector('.np-drawer');
expect(drawer).toBeInTheDocument();
});
});
it('focuses dropdown options when clicked', async () => {
const { container } = render();
await openSelect(container);
const dropdown = screen.getByRole('listbox');
expect(dropdown).toHaveFocus();
});
it("on touch device doesn't focus on the search box once opened", async () => {
enableMobileScreen();
const { container } = render();
await openSelect(container);
const input = await screen.findByPlaceholderText('Search...');
expect(input).not.toHaveFocus();
});
it('on not touch device focuses on the search box once opened', async () => {
const { container } = render();
await openSelect(container);
const input = screen.getByPlaceholderText('Search...');
expect(input).toHaveFocus();
});
it('renders controls with fallback id', async () => {
const { container } = render();
await openSelect(container);
const button = screen.getByRole('button');
const options = screen.getByRole('listbox');
expect(button).toHaveAttribute('id');
expect(options).toHaveAttribute('id');
});
it('renders controls with passed id', async () => {
const { container } = render();
await openSelect(container);
const button = screen.getByRole('button');
const options = screen.getByRole('listbox');
expect(button).toHaveAttribute('id', 'my-select-component');
expect(options).toHaveAttribute('id', 'my-select-component-listbox');
});
it('renders button with correct aria-controls attribute', async () => {
const { container } = render();
await openSelect(container);
const button = screen.getByRole('button');
const options = screen.getByRole('listbox');
const ariaControlsId = button.getAttribute('aria-controls');
const optionsId = options.getAttribute('id');
expect(ariaControlsId).toBe(optionsId);
});
describe('keyboard navigation on desktop', () => {
it('navigates to the next item', async () => {
const { container } = render();
await openSelect(container);
expect(screen.queryByRole('listbox')).toHaveFocus();
await userEvent.keyboard('[ArrowDown]');
await waitFor(() => {
expect(screen.queryByRole('option', { name: 'yo' })).toHaveFocus();
});
});
it('skips disabled items when navigating to the next item', async () => {
const { container } = render(
,
);
await openSelect(container);
expect(screen.queryByRole('listbox')).toHaveFocus();
await userEvent.keyboard('[ArrowDown]');
await waitFor(() => {
expect(screen.queryByRole('option', { name: 'yo' })).toHaveFocus();
});
await userEvent.keyboard('[ArrowDown]');
await waitFor(() => {
expect(screen.queryByRole('option', { name: 'boi' })).toHaveFocus();
});
});
it('cannot navigate past the first list item', async () => {
const { container } = render();
await openSelect(container);
expect(screen.queryByRole('listbox')).toHaveFocus();
await userEvent.keyboard('[ArrowUp]');
await waitFor(() => {
expect(screen.queryByRole('option', { name: 'yo' })).toHaveFocus();
});
await userEvent.keyboard('[ArrowUp]');
await waitFor(() => {
expect(screen.queryByRole('option', { name: 'yo' })).toHaveFocus();
});
});
it('cannot navigate past the last list item', async () => {
const { container } = render();
await openSelect(container);
expect(screen.queryByRole('listbox')).toHaveFocus();
await userEvent.keyboard('[ArrowDown]');
await waitFor(() => {
expect(screen.queryByRole('option', { name: 'yo' })).toHaveFocus();
});
await userEvent.keyboard('[ArrowDown]');
await waitFor(() => {
expect(screen.queryByRole('option', { name: 'dawg' })).toHaveFocus();
});
await userEvent.keyboard('[ArrowDown]');
await waitFor(() => {
expect(screen.queryByRole('option', { name: 'boi' })).toHaveFocus();
});
await userEvent.keyboard('[ArrowDown]');
await waitFor(() => {
expect(screen.queryByRole('option', { name: 'boi' })).toHaveFocus();
});
});
describe('with search enabled', () => {
it('maintains focus on search but visually indicates which item is focused with keyboard navigation and selects it on enter', async () => {
const { container } = render();
await openSelect(container);
expect(screen.queryByPlaceholderText('Search...')).toHaveFocus();
await userEvent.keyboard('[ArrowDown]');
await waitFor(() => {
expect(screen.queryByRole('option', { name: 'yo' })).toHaveClass(
'np-dropdown-item--focused',
{ exact: false },
);
});
expect(screen.queryByPlaceholderText('Search...')).toHaveFocus();
await userEvent.keyboard('[ArrowDown]');
await waitFor(() => {
expect(screen.queryByRole('option', { name: 'dawg' })).toHaveClass(
'np-dropdown-item--focused',
{ exact: false },
);
});
expect(screen.queryByPlaceholderText('Search...')).toHaveFocus();
await userEvent.keyboard('[Enter]');
expect(props.onChange).toHaveBeenCalledWith({ label: 'dawg', value: 1 });
});
});
});
describe('keyboard navigation on mobile', () => {
beforeEach(() => {
enableMobileScreen();
});
it('navigates to the next item', async () => {
const { container } = render();
await openSelect(container);
await expect(screen.findByRole('dialog')).resolves.toBeInTheDocument();
await userEvent.keyboard('[ArrowDown]');
await waitFor(() => {
expect(screen.queryByRole('option', { name: 'yo' })).toHaveFocus();
});
});
it('skips disabled items when navigating to the next item', async () => {
const { container } = render(
,
);
await openSelect(container);
await expect(screen.findByRole('dialog')).resolves.toBeInTheDocument();
await userEvent.keyboard('[ArrowDown]');
await waitFor(() => {
expect(screen.queryByRole('option', { name: 'yo' })).toHaveFocus();
});
await userEvent.keyboard('[ArrowDown]');
await waitFor(() => {
expect(screen.queryByRole('option', { name: 'boi' })).toHaveFocus();
});
});
it('cannot navigate past the first list item', async () => {
const { container } = render();
await openSelect(container);
await expect(screen.findByRole('dialog')).resolves.toBeInTheDocument();
await userEvent.keyboard('[ArrowUp]');
await waitFor(() => {
expect(screen.queryByRole('option', { name: 'yo' })).toHaveFocus();
});
await userEvent.keyboard('[ArrowUp]');
await waitFor(() => {
expect(screen.queryByRole('option', { name: 'yo' })).toHaveFocus();
});
});
it('cannot navigate past the last list item', async () => {
const { container } = render();
await openSelect(container);
await expect(screen.findByRole('dialog')).resolves.toBeInTheDocument();
await userEvent.keyboard('[ArrowDown]');
await waitFor(() => {
expect(screen.queryByRole('option', { name: 'yo' })).toHaveFocus();
});
await userEvent.keyboard('[ArrowDown]');
await waitFor(() => {
expect(screen.queryByRole('option', { name: 'dawg' })).toHaveFocus();
});
await userEvent.keyboard('[ArrowDown]');
await waitFor(() => {
expect(screen.queryByRole('option', { name: 'boi' })).toHaveFocus();
});
await userEvent.keyboard('[ArrowDown]');
await waitFor(() => {
expect(screen.queryByRole('option', { name: 'boi' })).toHaveFocus();
});
});
describe('with search enabled', () => {
beforeEach(() => {
enableMobileScreen();
});
it('visually indicates which item is focused with keyboard navigation and selects it on enter', async () => {
const { container } = render();
await openSelect(container);
await waitFor(() => {
expect(screen.queryByPlaceholderText('Search...')).not.toHaveFocus();
});
await userEvent.keyboard('[ArrowDown]');
await waitFor(() => {
expect(screen.queryByRole('option', { name: 'yo' })).toHaveClass(
'np-dropdown-item--focused',
{ exact: false },
);
});
expect(screen.queryByPlaceholderText('Search...')).not.toHaveFocus();
await userEvent.keyboard('[ArrowDown]');
await waitFor(() => {
expect(screen.queryByRole('option', { name: 'dawg' })).toHaveClass(
'np-dropdown-item--focused',
{ exact: false },
);
});
expect(screen.queryByPlaceholderText('Search...')).not.toHaveFocus();
await userEvent.keyboard('[Enter]');
expect(props.onChange).toHaveBeenCalledWith({ label: 'dawg', value: 1 });
});
});
});
});