import { h } from 'preact'; import { render, waitFor } from '@testing-library/preact'; import userEvent from '@testing-library/user-event'; import '@testing-library/jest-dom'; import { MultiSelectAutocomplete } from '../MultiSelectAutocomplete'; describe('', () => { it('renders default UI', () => { const { container } = render( {}} />, ); expect(container.innerHTML).toMatchSnapshot(); }); it('renders with customisation', () => { const { container } = render( {}} />, ); expect(container.innerHTML).toMatchSnapshot(); }); it('renders with default values', () => { const { container } = render( {}} />, ); expect(container.innerHTML).toMatchSnapshot(); }); it('shows static suggestions on focus, if provided', async () => { const { getByLabelText, getByText, getByRole } = render( {}} />, ); getByLabelText('Example label').focus(); await waitFor(() => expect(getByText('static suggestions heading')).toBeInTheDocument(), ); expect(getByRole('option', { name: 'one' })).toBeInTheDocument(); expect(getByRole('option', { name: 'two' })).toBeInTheDocument(); }); it('fetches suggestions and displays in dropdown', async () => { const mockFetchSuggestions = jest.fn(async () => [ { name: 'option1' }, { name: 'option2' }, ]); const { getByLabelText, getByText, getByRole, queryByText } = render( , ); const input = getByLabelText('Example label'); input.focus(); await waitFor(() => expect(getByText('static suggestions heading')).toBeInTheDocument(), ); userEvent.type(input, 'a'); // Check static suggestions are gone and expected options have appeared await waitFor(() => expect(queryByText('static suggestions heading')).toBeNull(), ); expect(getByRole('option', { name: 'option1' })).toBeInTheDocument(); expect(getByRole('option', { name: 'option2' })).toBeInTheDocument(); }); it('displays suggestions in a custom template, if provided', async () => { const Suggestion = ({ name }) => custom suggestion: {name}; const mockFetchSuggestions = jest.fn(async () => [ { name: 'option1' }, { name: 'option2' }, ]); const { getByLabelText, getByRole } = render( , ); const input = getByLabelText('Example label'); input.focus(); await userEvent.type(input, 'a'); await waitFor(() => expect( getByRole('option', { name: 'custom suggestion: option1' }), ).toBeInTheDocument(), ); expect( getByRole('option', { name: 'custom suggestion: option2' }), ).toBeInTheDocument(); }); it('when user-generated selections are allowed, displays search term as an option if no suggestions', async () => { const mockFetchSuggestions = jest.fn(async () => []); const { getByLabelText, getByRole } = render( , ); const input = getByLabelText('Example label'); input.focus(); userEvent.type(input, 'a'); await waitFor(() => expect(getByRole('option', { name: 'a' })).toBeInTheDocument(), ); }); it('selects an option by clicking', async () => { const mockFetchSuggestions = jest.fn(async () => [ { name: 'option1' }, { name: 'option2' }, ]); const { getByLabelText, getByRole } = render( , ); const input = getByLabelText('Example label'); input.focus(); userEvent.type(input, 'a'); await waitFor(() => expect(getByRole('option', { name: 'option1' })).toBeInTheDocument(), ); userEvent.click(getByRole('option', { name: 'option1' })); // It should now be added to the list of selected items await waitFor(() => expect(getByRole('button', { name: 'Edit option1' })).toBeInTheDocument(), ); expect(getByRole('button', { name: 'Remove option1' })).toBeInTheDocument(); }); it('selects an option by keyboard enter press', async () => { const mockFetchSuggestions = jest.fn(async () => [ { name: 'option1' }, { name: 'option2' }, ]); const { getByLabelText, getByRole } = render( , ); const input = getByLabelText('Example label'); input.focus(); userEvent.type(input, 'a'); await waitFor(() => expect(getByRole('option', { name: 'option1' })).toBeInTheDocument(), ); userEvent.type(input, '{arrowdown}'); await waitFor(() => expect(getByRole('option', { name: 'option1' })).toHaveAttribute( 'aria-selected', 'true', ), ); userEvent.type(input, '{enter}'); // It should now be added to the list of selected items await waitFor(() => expect(getByRole('button', { name: 'Edit option1' })).toBeInTheDocument(), ); expect(getByRole('button', { name: 'Remove option1' })).toBeInTheDocument(); }); it('should select current text on spacebar press, when it matches a suggestion', async () => { const mockFetchSuggestions = jest.fn(async () => [ { name: 'option1' }, { name: 'option2' }, ]); const { getByLabelText, getByRole } = render( , ); const input = getByLabelText('Example label'); input.focus(); await userEvent.type(input, 'option1 '); // It should now be added to the list of selected items await waitFor(() => expect(getByRole('button', { name: 'Edit option1' })).toBeInTheDocument(), ); expect(getByRole('button', { name: 'Remove option1' })).toBeInTheDocument(); }); it('should select current text on comma press, when it matches a suggestion', async () => { const mockFetchSuggestions = jest.fn(async () => [ { name: 'option1' }, { name: 'option2' }, ]); const { getByLabelText, getByRole } = render( , ); const input = getByLabelText('Example label'); input.focus(); userEvent.type(input, 'option1,'); // It should now be added to the list of selected items await waitFor(() => expect(getByRole('button', { name: 'Edit option1' })).toBeInTheDocument(), ); expect(getByRole('button', { name: 'Remove option1' })).toBeInTheDocument(); }); it('should select current text, when it does not match a suggestion, but user generated selections are allowed', async () => { const mockFetchSuggestions = jest.fn(async () => [ { name: 'option1' }, { name: 'option2' }, ]); const { getByLabelText, getByRole } = render( , ); const input = getByLabelText('Example label'); input.focus(); await userEvent.type(input, 'example '); // It should now be added to the list of selected items await waitFor(() => expect(getByRole('button', { name: 'Edit example' })).toBeInTheDocument(), ); expect(getByRole('button', { name: 'Remove example' })).toBeInTheDocument(); }); it("doesn't select manual text entry if already selected", async () => { const mockFetchSuggestions = jest.fn(async () => [ { name: 'option1' }, { name: 'option2' }, ]); const { getByLabelText, getByRole, getAllByRole } = render( , ); // Item should already be selected, as passed as a default value expect(getByRole('button', { name: 'Edit option1' })).toBeInTheDocument(); const input = getByLabelText('Example label'); input.focus(); // Try to select the same value by manually typing await userEvent.type(input, 'option1,'); // Verify there is still only one selected 'option1', and text field has not been cleared expect(input).toHaveValue('option1'); expect(getAllByRole('button', { name: 'Edit option1' })).toHaveLength(1); }); it('displays selections in a custom template, if provided', async () => { const Selection = ({ name }) => ; const mockFetchSuggestions = jest.fn(async () => [ { name: 'option1' }, { name: 'option2' }, ]); const { getByLabelText, getByRole } = render( , ); const input = getByLabelText('Example label'); await userEvent.type(input, 'option1,'); // It should now be added to the list of selected items using the custom template await waitFor(() => expect( getByRole('button', { name: 'Selected: option1' }), ).toBeInTheDocument(), ); }); it('edits a selection', async () => { const { getByLabelText, getByRole, queryByRole } = render( []} defaultValue={[{ name: 'one' }]} />, ); getByRole('button', { name: 'Edit one' }).click(); // Selection disappears await waitFor(() => expect( queryByRole('button', { name: 'Edit one' }), ).not.toBeInTheDocument(), ); // Input is focused and pre-filled with value const input = getByLabelText('Example label'); await waitFor(() => expect(input).toHaveValue('one')); expect(input).toHaveFocus(); }); it('passes an edit callback to custom selection template', async () => { const Selection = ({ name, onEdit }) => ( ); const { getByLabelText, getByRole, queryByRole } = render( []} SelectionTemplate={Selection} />, ); getByRole('button', { name: 'Selected: one' }).click(); // Selection disappears await waitFor(() => expect( queryByRole('button', { name: 'Selected: one' }), ).not.toBeInTheDocument(), ); // Input is focused and pre-filled with value const input = getByLabelText('Example label'); await waitFor(() => expect(input).toHaveValue('one')); expect(input).toHaveFocus(); }); it('deletes a selection', async () => { const { getByLabelText, getByRole, queryByRole } = render( []} defaultValue={[{ name: 'one' }]} />, ); getByRole('button', { name: 'Remove one' }).click(); // Selection disappears await waitFor(() => expect( queryByRole('button', { name: 'Remove one' }), ).not.toBeInTheDocument(), ); // Input is re-focused expect(getByLabelText('Example label')).toHaveFocus(); }); it('passes a deselect callback to custom selection template', async () => { const Selection = ({ name, onDeselect }) => ( ); const { getByLabelText, getByRole, queryByRole } = render( []} SelectionTemplate={Selection} />, ); getByRole('button', { name: 'Selected: one' }).click(); // Selection disappears await waitFor(() => expect( queryByRole('button', { name: 'Selected: one' }), ).not.toBeInTheDocument(), ); // Input is re-focused expect(getByLabelText('Example label')).toHaveFocus(); }); it('closes dropdown and selects current input value on blur if it matches a suggestion', async () => { const mockFetchSuggestions = jest.fn(async () => [ { name: 'option1' }, { name: 'option2' }, ]); const { getByLabelText, getByRole, queryByRole } = render( , ); const input = getByLabelText('Example label'); input.focus(); await userEvent.type(input, 'option1'); await waitFor(() => expect(getByRole('option', { name: 'option1' })).toBeInTheDocument(), ); input.blur(); // Dropdown should no longer be visible await waitFor(() => expect(queryByRole('option', { name: 'option1' })).toBeNull(), ); // Text value should be selected expect(getByRole('button', { name: 'Edit option1' })).toBeInTheDocument(); expect(getByRole('button', { name: 'Remove option1' })).toBeInTheDocument(); }); it('clears input without selecting text on blur if no matching suggestion', async () => { const { getByLabelText, queryByRole } = render( []} />, ); const input = getByLabelText('Example label'); input.focus(); await userEvent.type(input, 'example'); input.blur(); // Text value should not be selected expect( queryByRole('button', { name: 'Edit example' }), ).not.toBeInTheDocument(); expect( queryByRole('button', { name: 'Remove example' }), ).not.toBeInTheDocument(); // Input should be cleared expect(input).toHaveValue(''); }); it('clears input and selects current text on blur if no matching suggestion and user-defined selections are permitted', async () => { const { getByLabelText, queryByRole } = render( []} allowUserDefinedSelections={true} />, ); const input = getByLabelText('Example label'); input.focus(); await userEvent.type(input, 'example'); input.blur(); await waitFor(() => { // Text value should be selected expect( queryByRole('button', { name: 'Edit example' }), ).toBeInTheDocument(); expect( queryByRole('button', { name: 'Remove example' }), ).toBeInTheDocument(); // Input should be cleared expect(input).toHaveValue(''); }); }); it('clears the input on Escape press', async () => { const mockFetchSuggestions = jest.fn(async () => [ { name: 'option1' }, { name: 'option2' }, ]); const { getByLabelText, getByRole, queryByRole } = render( , ); const input = getByLabelText('Example label'); input.focus(); await userEvent.type(input, 'a'); await waitFor(() => expect(getByRole('option', { name: 'option1' })).toBeInTheDocument(), ); userEvent.keyboard('{Escape}'); await waitFor(() => expect(queryByRole('option', { name: 'option1' })).toBeNull(), ); expect(input).toHaveValue(''); }); it('Edits previous selection (if exists) on backspace press in empty input', async () => { const { getByLabelText, getByRole, queryByRole } = render( []} />, ); // Selection should be present as passed as a default value expect(getByRole('button', { name: 'Edit example' })).toBeInTheDocument(); const input = getByLabelText('Example label'); input.focus(); await userEvent.type(input, '{backspace}'); await waitFor(() => expect(input).toHaveValue('example')); expect(queryByRole('button', { name: 'Edit example' })).toBeNull(); }); it('Prohibits entering special characters', async () => { const { getByLabelText } = render( []} />, ); const input = getByLabelText('Example label'); await userEvent.type(input, '!@£$%^&*()a1'); expect(input).toHaveValue('a1'); }); it('shows a message and prevents selections when maximum is reached', async () => { const mockFetchSuggestions = jest.fn(async () => [ { name: 'option1' }, { name: 'option2' }, ]); const { getByLabelText, getByRole, queryByText, getByText, queryAllByRole, } = render( , ); const input = getByLabelText('Example label'); input.focus(); // Max selections not yet reached expect(queryByText('Only 2 selections allowed')).toBeNull(); expect(input).toHaveAttribute('aria-disabled', 'false'); await userEvent.type(input, 'option1,'); // Make sure option has been selected await waitFor(() => getByRole('button', { name: 'Edit option1' })); // Check input is in the max reached state expect(input).toHaveAttribute('placeholder', ''); expect(getByText('Only 2 selections allowed')).toBeInTheDocument(); expect(input).toHaveAttribute('aria-disabled', 'true'); // Start typing and make sure further options not shown await userEvent.type(input, 'option2'); expect(queryAllByRole('option')).toHaveLength(0); expect(getByText('Only 2 selections allowed')).toBeInTheDocument(); // Try to select a value by typing a comma await userEvent.type(input, ','); // Verify the selection wasn't made, and the text is still in the input expect(input).toHaveValue('option2'); }); });