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).toHaveAttribute('id', 'dummy-searchbox'); }); it('should filter the options with the default filter function', 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, '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, '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 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); 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 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 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'); 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(); }); 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('[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); 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('[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 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 }); }); }); }); });