import React, { useState } from 'react'; import { waitFor, fireEvent } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import renderWithTheme from '../../../testUtils/renderWithTheme'; import SingleSelect from '..'; import Icon from '../../Icon'; describe('rendering', () => { it('renders input and not render list when disabled', async () => { const options = [ { value: 1, text: 'Item 1' }, { value: 2, text: 'Item 2' }, ]; const { getByText, getByPlaceholderText } = renderWithTheme( ); await waitFor(() => { expect(getByPlaceholderText('Select an item')).toBeInTheDocument(); expect(getByText('Item 1')).not.toBeVisible(); expect(getByText('Item 2')).not.toBeVisible(); }); fireEvent.click(getByPlaceholderText('Select an item')); await waitFor(() => { expect(getByText('Item 1')).not.toBeVisible(); expect(getByText('Item 2')).not.toBeVisible(); }); }); it('renders input and option list', async () => { const options = [ { value: 1, text: 'Item 1' }, { value: 2, text: 'Item 2' }, ]; const { getByText, getByPlaceholderText } = renderWithTheme( ); await waitFor(() => { expect(getByPlaceholderText('Select an item')).toBeInTheDocument(); expect(getByText('Item 1')).not.toBeVisible(); expect(getByText('Item 2')).not.toBeVisible(); }); fireEvent.click(getByPlaceholderText('Select an item')); await waitFor(() => { expect(getByText('Item 1')).toBeVisible(); expect(getByText('Item 2')).toBeVisible(); }); }); it('renders input and option list with category names', async () => { const options = [ { category: 'Teams', options: [{ value: 'team-1', text: 'Team 1', helpText: '5 members' }], }, { category: 'Locations', options: [{ value: 'location-1', text: 'Location 1' }], }, { category: 'Individual', options: [{ value: 'person-1', text: 'Person 1' }], }, ]; const { getByText, getByPlaceholderText } = renderWithTheme( ); await waitFor(() => { expect(getByPlaceholderText('Select an item')).toBeInTheDocument(); expect(getByText('Teams')).not.toBeVisible(); expect(getByText('Locations')).not.toBeVisible(); expect(getByText('Individual')).not.toBeVisible(); expect(getByText('Team 1')).not.toBeVisible(); expect(getByText('Location 1')).not.toBeVisible(); expect(getByText('Person 1')).not.toBeVisible(); expect(getByText('5 members')).not.toBeVisible(); }); fireEvent.click(getByPlaceholderText('Select an item')); await waitFor(() => { expect(getByText('Teams')).toBeVisible(); expect(getByText('Locations')).toBeVisible(); expect(getByText('Individual')).toBeVisible(); expect(getByText('Team 1')).toBeVisible(); expect(getByText('Location 1')).toBeVisible(); expect(getByText('Person 1')).toBeVisible(); expect(getByText('5 members')).toBeVisible(); }); }); it('renders custom option renderer with additional option props', async () => { const options = [ { value: 'item-1', text: 'Item 1', icon: 'add-person' } as const, { value: 'item-2', text: 'Item 2', icon: 'alignment' } as const, ]; const { getByText, getByPlaceholderText } = renderWithTheme( ( <> {`${ index + 1 }: ${text}`} )} onChange={jest.fn()} placeholder="Select an item" /> ); await waitFor(() => { expect(getByPlaceholderText('Select an item')).toBeInTheDocument(); }); fireEvent.click(getByPlaceholderText('Select an item')); await waitFor(() => { expect(getByText('1: Item 1')).toBeVisible(); expect(getByText('2: Item 2')).toBeVisible(); }); expect( getByText('1: Item 1').parentElement?.querySelector('i') ).toHaveClass('hero-icon-add-person'); expect( getByText('2: Item 2').parentElement?.querySelector('i') ).toHaveClass('hero-icon-alignment'); }); }); describe('interaction', () => { it('allows to select an item', async () => { const options = [ { value: 1, text: 'Item 1' }, { value: 2, text: 'Item 2' }, ]; const onChange = jest.fn(); const { getByText, getByPlaceholderText } = renderWithTheme( ); fireEvent.click(getByPlaceholderText('Select an item')); fireEvent.click(getByText('Item 1')); await waitFor(() => { expect(onChange).toHaveBeenCalledTimes(1); expect(onChange).toHaveBeenCalledWith(1); }); }); it('allows to select an item from grouped options', async () => { const options = [ { category: 'Teams', options: [ { value: 'team-1', text: 'Team 1', helpText: '5 members' }, { value: 'team-2', text: 'Team 2', helpText: '3 members' }, ], }, { category: 'Locations', options: [{ value: 'location-1', text: 'Location 1' }], }, { category: 'Individual', options: [{ value: 'person-1', text: 'Person 1' }], }, ]; const onChange = jest.fn(); const { getByText, getByPlaceholderText } = renderWithTheme( ); fireEvent.click(getByPlaceholderText('Select an item')); fireEvent.click(getByText('Location 1')); await waitFor(() => { expect(onChange).toHaveBeenCalledTimes(1); expect(onChange).toHaveBeenCalledWith('location-1'); }); }); it('allows to search for an item', async () => { const options = [ { value: 1, text: 'Item 1' }, { value: 2, text: 'Item 2' }, ]; const onChange = jest.fn(); const onQueryChange = jest.fn(); const { getByText, getByPlaceholderText, queryByText } = renderWithTheme( ); await waitFor(() => { expect(getByText('Item 1')).toBeInTheDocument(); expect(queryByText('Item 2')).not.toBeInTheDocument(); }); fireEvent.change(getByPlaceholderText('Select an item'), { target: { value: '2' }, }); await waitFor(() => { expect(onQueryChange).toHaveBeenCalledTimes(1); expect(onQueryChange).toHaveBeenCalledWith('2'); }); }); it('allows to create new item', async () => { const options = [ { value: 1, text: 'Item 1' }, { value: 2, text: 'Item 2' }, ]; const onChange = jest.fn(); const onQueryChange = jest.fn(); const onCreateNewOption = jest.fn(); const { getByText, getByPlaceholderText, queryByText } = renderWithTheme( ); await waitFor(() => { expect(queryByText('Item 1')).not.toBeInTheDocument(); expect(queryByText('Item 2')).not.toBeInTheDocument(); expect(getByText('New Item')).not.toBeVisible(); }); fireEvent.click(getByPlaceholderText('Select an item')); await waitFor(() => { expect(getByText('New Item')).toBeVisible(); }); fireEvent.click(getByText('New Item')); await waitFor(() => { expect(onCreateNewOption).toHaveBeenCalledTimes(1); expect(onCreateNewOption).toHaveBeenCalledWith('New Item'); }); }); it('allows to call callback when scrolling to bottom of the list', async () => { const options = [ { value: 1, text: 'Item 1' }, { value: 2, text: 'Item 2' }, { value: 3, text: 'Item 3' }, { value: 4, text: 'Item 4' }, { value: 5, text: 'Item 5' }, { value: 6, text: 'Item 6' }, { value: 7, text: 'Item 7' }, { value: 8, text: 'Item 8' }, { value: 9, text: 'Item 9' }, { value: 10, text: 'Item 10' }, ]; const onChange = jest.fn(); const onScrollListToBottom = jest.fn(); const { getByText, getByRole, getByPlaceholderText } = renderWithTheme( ); fireEvent.click(getByPlaceholderText('Select an item')); await waitFor(() => { expect(getByText('Item 10')).toBeVisible(); expect(onScrollListToBottom).not.toHaveBeenCalled(); }); fireEvent.scroll(getByRole('listbox'), { y: 200 }); await waitFor(() => { expect(onScrollListToBottom).toHaveBeenCalledTimes(1); }); }); it('allows to clear selected item', async () => { const options = [ { value: 1, text: 'Item 1' }, { value: 2, text: 'Item 2' }, ]; const onChange = jest.fn(); const { getByTestId } = renderWithTheme( ); userEvent.hover(getByTestId('query-input')); userEvent.click(getByTestId('remove-icon')); await waitFor(() => { expect(onChange).toHaveBeenCalledWith(undefined); }); }); it('only calls onQueryChange when query is not undefined', async () => { const options = [ { value: 1, text: 'Item 1' }, { value: 2, text: 'Item 2' }, ]; const onChange = jest.fn(); const onQueryChange = jest.fn(); const { getByText, findByText, getByTestId } = renderWithTheme( option.text.substring(4)} onChange={onChange} onQueryChange={onQueryChange} clearable /> ); const input = getByTestId('query-input'); // open select dropdown userEvent.click(input); expect(onQueryChange).not.toHaveBeenCalled(); // select item 1 const itemOne = await findByText('Item 1'); expect(itemOne).toBeVisible(); userEvent.click(itemOne); expect(onQueryChange).not.toHaveBeenCalled(); // open select dropdown again userEvent.click(input); expect(onQueryChange).not.toHaveBeenCalled(); // select item 2 const itemTwo = await findByText('Item 2'); expect(itemTwo).toBeVisible(); userEvent.click(itemTwo); expect(onQueryChange).not.toHaveBeenCalled(); // open select dropdown again userEvent.click(input); // close dropdown by clicking outside Select userEvent.click(document.body); expect(getByText('Item 1')).not.toBeVisible(); expect(getByText('Item 2')).not.toBeVisible(); expect(onQueryChange).not.toHaveBeenCalled(); // Change query await waitFor(() => { userEvent.type(input, 'Item'); }); expect(onQueryChange).toHaveBeenCalled(); expect(onQueryChange).not.toHaveBeenCalledWith(undefined); }); it('makes onQueryChange to have been called with undefined when onBlur is undefined', async () => { const options = [{ value: 1, text: 'Item 1' }]; const onChange = jest.fn(); const onFocus = jest.fn(); const onQueryChange = jest.fn(); const { getByPlaceholderText } = renderWithTheme( ); const input = getByPlaceholderText('Select an item'); // click on select input userEvent.click(input); await waitFor(() => { expect(onFocus).toHaveBeenCalledTimes(1); }); // click outside userEvent.click(document.body); expect(onQueryChange).toHaveBeenCalledWith(undefined); }); it('calls onBlur and onFocus when they are not undefined', async () => { const options = [{ value: 1, text: 'Item 1' }]; const onChange = jest.fn(); const onBlur = jest.fn(); const onFocus = jest.fn(); const onQueryChange = jest.fn(); const { getByTestId } = renderWithTheme( ); const input = getByTestId('query-input'); // click on select input userEvent.click(input); await waitFor(() => { expect(onFocus).toHaveBeenCalledTimes(1); expect(onBlur).not.toHaveBeenCalled(); }); // click outside userEvent.click(document.body); expect(onBlur).toHaveBeenCalledTimes(1); expect(onQueryChange).not.toHaveBeenCalledWith(undefined); }); it('shows noResult text even when loading', async () => { const onChange = jest.fn(); const { getByPlaceholderText, getByText } = renderWithTheme( ); const input = getByPlaceholderText('Select items'); // click on select input userEvent.click(input); await waitFor(() => { expect(getByText('Not found')).toBeInTheDocument(); }); }); it('works well when value got changed outside', () => { const options = [ { value: 'item-1', text: 'Item 1' }, { value: 'item-2', text: 'Item 2' }, ]; const Example = () => { const [value, setValue] = useState(); return ( <> setValue(e.target.value)} data-test-id="input" /> ); }; const { getByTestId, getByText } = renderWithTheme(); const select = getByTestId('select'); userEvent.click(select); userEvent.click(getByText('Item 1')); expect(select).toSelectItem('Item 1'); const input = getByTestId('input'); userEvent.click(input); userEvent.clear(input); expect(select).toSelectItem(''); userEvent.click(select); userEvent.click(getByText('Item 1')); expect(select).toSelectItem('Item 1'); }); it('shows spinner when loading', () => { const options = [ { value: 'item-1', text: 'Item 1' }, { value: 'item-2', text: 'Item 2' }, ]; const { getByTestId } = renderWithTheme( ); expect(getByTestId('loading-icon')).toBeVisible(); }); });