import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import * as React from 'react'; import { Autocomplete } from '../Autocomplete'; import { ComponentClassNames, ComponentText } from '../../shared/constants'; import { classNameModifier } from '../../shared/utils'; import type { AutocompleteProps, ComboBoxOption, } from '../../types/autocomplete'; describe('Autocomplete:', () => { const label = 'Autocomplete'; const testId = 'autocomplete-test-id'; const options = [ { id: 'apple', label: 'apple' }, { id: 'banana', label: 'banana' }, { id: 'cherry', label: 'cherry' }, ]; const ControlledAutocomplete = ({ onSubmit }: Partial) => { const [value, setValue] = React.useState(''); const onChange: React.ChangeEventHandler = (event) => { setValue(event.target.value); }; // It is your responsibility to set up onSelect const onSelect = (option: ComboBoxOption) => { const { label } = option; setValue(label); }; // It is your responsibility to set up onClear const onClear = () => { setValue(''); }; return ( ); }; const expectWorkflow = async (onSubmit: jest.Mock) => { const textInput = await screen.findByRole('combobox'); userEvent.type(textInput, 'Hello world!'); // No options found const noOption = screen.queryByText(ComponentText.Autocomplete.emptyText); expect(noOption).toBeInTheDocument(); expect(textInput).toHaveValue('Hello world!'); // Close menu on ESC if menu is open userEvent.keyboard('{Esc}'); expect(textInput).toHaveValue('Hello world!'); expect(screen.queryByRole('listbox')).not.toBeInTheDocument(); // Clear text on ESC if menu is closed expect(textInput).toHaveFocus(); userEvent.keyboard('{Esc}'); expect(textInput).toHaveValue(''); // Focus will not open menu userEvent.click(document.body); expect(textInput).not.toHaveFocus(); textInput.focus(); expect(textInput).toHaveFocus(); expect(screen.queryByRole('listbox')).not.toBeInTheDocument(); // Click will open menu expect(textInput).toHaveFocus(); userEvent.click(textInput); expect(screen.queryByRole('listbox')).toBeInTheDocument(); // Close menu when lose focus userEvent.click(document.body); expect(screen.queryByRole('listbox')).not.toBeInTheDocument(); // Type random text and submit userEvent.type(textInput, 'Hello world!'); userEvent.keyboard('{Enter}'); expect(onSubmit).toHaveBeenCalledTimes(1); expect(onSubmit).toHaveBeenCalledWith('Hello world!'); expect(textInput).toHaveFocus(); // menu closed expect(noOption).not.toBeInTheDocument(); /** * Mouse testing section */ userEvent.clear(textInput); // Select via mouse and submit const listbox = await screen.findByRole('listbox'); const appleOption = screen.getByText('apple') .parentElement as HTMLLIElement; userEvent.selectOptions(listbox, appleOption); expect(textInput).toHaveFocus(); expect(textInput).toHaveValue('apple'); userEvent.keyboard('{Enter}'); expect(onSubmit).toHaveBeenCalledTimes(2); expect(onSubmit).toHaveBeenCalledWith('apple'); expect(textInput).toHaveFocus(); // menu closed expect(screen.queryByRole('listbox')).not.toBeInTheDocument(); /** * Keyboard testing section */ userEvent.clear(textInput); // menu open expect(screen.queryByRole('listbox')).toBeInTheDocument(); // Select via keyboard and submit userEvent.keyboard('{ArrowDown}'); expect(textInput).toHaveFocus(); userEvent.keyboard('{ArrowDown}'); expect(textInput).toHaveFocus(); userEvent.keyboard('{ArrowUp}'); expect(textInput).toHaveFocus(); userEvent.keyboard('{ArrowDown}'); expect(textInput).toHaveFocus(); userEvent.keyboard('{Enter}'); expect(textInput).toHaveFocus(); expect(textInput).toHaveValue('banana'); userEvent.keyboard('{Enter}'); expect(onSubmit).toHaveBeenCalledTimes(3); expect(onSubmit).toHaveBeenCalledWith('banana'); expect(textInput).toHaveFocus(); // menu closed expect(screen.queryByRole('listbox')).not.toBeInTheDocument(); }; it('should render classname', async () => { render(); const autocomplete = await screen.findByTestId(testId); expect(autocomplete).toHaveClass(ComponentClassNames.Autocomplete); }); it('should work in uncontrolled way', async () => { const onSubmit = jest.fn(); render( ); await expectWorkflow(onSubmit); }); it('should work in controlled way', async () => { const onSubmit = jest.fn(); render(); await expectWorkflow(onSubmit); }); it('should forward refs to DOM elements', async () => { const ref = React.createRef(); render(); await screen.findByRole('combobox'); expect(ref?.current?.nodeName).toBe('INPUT'); }); it('should be able to set a placeholder', async () => { const placeholder = 'This is a placeholder'; render( ); const textInput = await screen.findByRole('combobox'); expect(textInput).toHaveAttribute('placeholder', placeholder); }); it('should be able to set a size', async () => { render(); const textInput = await screen.findByRole('combobox'); expect(textInput).toHaveAttribute('data-size', 'large'); }); it('should render size classes for Autocomplete', async () => { render(
); const small = await screen.findByTestId('small'); const large = await screen.findByTestId('large'); expect(small.firstChild).toHaveClass( `${ComponentClassNames['Field']}--small` ); expect(large.firstChild).toHaveClass( `${ComponentClassNames['Field']}--large` ); }); it('should be able to set a quiet variation', async () => { render(); const textInput = await screen.findByRole('combobox'); expect(textInput).toHaveAttribute('data-variation', 'quiet'); }); it('should be in loading state when isLoading is set to true', async () => { render(); const textInput = await screen.findByRole('combobox'); userEvent.click(textInput); const listbox = screen.queryByRole('listbox'); expect(listbox).not.toBeInTheDocument(); const loading = screen.getByText(ComponentText.Autocomplete.loadingText); expect(loading).toHaveClass(ComponentClassNames.AutocompleteMenuLoading); }); it('should render classname on active option correctly', async () => { render(); const textInput = await screen.findByRole('combobox'); userEvent.click(textInput); const appleOption = screen.getByText('apple') .parentElement as HTMLLIElement; const bananaOption = screen.getByText('banana') .parentElement as HTMLLIElement; const cherryOption = screen.getByText('cherry') .parentElement as HTMLLIElement; expect(appleOption).not.toHaveClass( classNameModifier(ComponentClassNames.AutocompleteMenuOption, 'active') ); expect(bananaOption).not.toHaveClass( classNameModifier(ComponentClassNames.AutocompleteMenuOption, 'active') ); expect(cherryOption).not.toHaveClass( classNameModifier(ComponentClassNames.AutocompleteMenuOption, 'active') ); userEvent.hover(bananaOption); expect(appleOption).not.toHaveClass( classNameModifier(ComponentClassNames.AutocompleteMenuOption, 'active') ); expect(bananaOption).toHaveClass( classNameModifier(ComponentClassNames.AutocompleteMenuOption, 'active') ); expect(cherryOption).not.toHaveClass( classNameModifier(ComponentClassNames.AutocompleteMenuOption, 'active') ); userEvent.keyboard('{ArrowDown}'); expect(textInput).toHaveFocus(); expect(appleOption).not.toHaveClass( classNameModifier(ComponentClassNames.AutocompleteMenuOption, 'active') ); expect(bananaOption).not.toHaveClass( classNameModifier(ComponentClassNames.AutocompleteMenuOption, 'active') ); expect(cherryOption).toHaveClass( classNameModifier(ComponentClassNames.AutocompleteMenuOption, 'active') ); userEvent.keyboard('{ArrowUp}'); userEvent.keyboard('{ArrowUp}'); expect(textInput).toHaveFocus(); expect(appleOption).toHaveClass( classNameModifier(ComponentClassNames.AutocompleteMenuOption, 'active') ); expect(bananaOption).not.toHaveClass( classNameModifier(ComponentClassNames.AutocompleteMenuOption, 'active') ); expect(cherryOption).not.toHaveClass( classNameModifier(ComponentClassNames.AutocompleteMenuOption, 'active') ); }); it('should be case insensitive filtering', async () => { render(); const textInput = await screen.findByRole('combobox'); userEvent.type(textInput, 'ap'); let optionElements = await screen.findAllByRole('option'); expect(optionElements).toHaveLength(1); expect(optionElements[0]).toHaveTextContent('apple'); userEvent.clear(textInput); userEvent.type(textInput, 'AP'); optionElements = await screen.findAllByRole('option'); expect(optionElements).toHaveLength(1); expect(optionElements[0]).toHaveTextContent('apple'); }); it('should be able to apply custom filtering', async () => { const optionFilter = jest.fn(); render( ); const textInput = await screen.findByRole('combobox'); userEvent.type(textInput, 'test'); expect(optionFilter).toHaveBeenCalled(); }); it('should be able to customize option', async () => { const renderOption = jest.fn(); render( ); const textInput = await screen.findByRole('combobox'); userEvent.click(textInput); expect(renderOption).toHaveBeenCalled(); }); it('should be able to customize loading', async () => { const LoadingIndicator = 'This is a custom loading'; render( ); const textInput = await screen.findByRole('combobox'); userEvent.click(textInput); const loading = screen.getByText(LoadingIndicator); expect(loading).toBeInTheDocument(); expect(loading).toHaveClass(ComponentClassNames.AutocompleteMenuLoading); }); it('should be able to customize empty menu', async () => { const Empty = 'This is a custom empty'; render(); const textInput = await screen.findByRole('combobox'); userEvent.click(textInput); const empty = screen.getByText(Empty); expect(empty).toBeInTheDocument(); expect(empty).toHaveClass(ComponentClassNames.AutocompleteMenuEmpty); }); it('should be able to customize header and footer', async () => { const Header = 'This is a custom header'; const Footer = 'This is a custom footer'; render( ); const textInput = await screen.findByRole('combobox'); userEvent.click(textInput); const header = screen.getByText(Header); expect(header).toBeInTheDocument(); expect(header).toHaveClass(ComponentClassNames.AutocompleteMenuHeader); const footer = screen.getByText(Footer); expect(footer).toBeInTheDocument(); expect(footer).toHaveClass(ComponentClassNames.AutocompleteMenuFooter); }); it('should set accessible props correctly', async () => { render(); const textInput = await screen.findByRole('combobox'); expect(textInput).toHaveAttribute('aria-autocomplete', 'list'); expect(textInput).toHaveAttribute('aria-haspopup', 'listbox'); expect(textInput).toHaveAttribute('aria-expanded', 'false'); userEvent.click(textInput); const menu = screen.getByRole('listbox').parentElement; expect(textInput).toHaveAttribute('aria-expanded', 'true'); expect(textInput).toHaveAttribute('aria-controls', menu?.id); expect(textInput).toHaveAttribute('aria-owns', menu?.id); expect(textInput).not.toHaveAttribute('aria-activedescendant'); const appleOption = screen.getByText('apple') .parentElement as HTMLLIElement; const bananaOption = screen.getByText('banana') .parentElement as HTMLLIElement; const cherryOption = screen.getByText('cherry') .parentElement as HTMLLIElement; expect(appleOption).toHaveAttribute('role', 'option'); expect(appleOption).toHaveAttribute('aria-selected', 'false'); expect(bananaOption).toHaveAttribute('aria-selected', 'false'); expect(cherryOption).toHaveAttribute('aria-selected', 'false'); userEvent.hover(appleOption); expect(appleOption).toHaveAttribute('aria-selected', 'true'); expect(bananaOption).toHaveAttribute('aria-selected', 'false'); expect(cherryOption).toHaveAttribute('aria-selected', 'false'); expect(textInput).toHaveAttribute('aria-activedescendant', appleOption.id); userEvent.hover(bananaOption); expect(appleOption).toHaveAttribute('aria-selected', 'false'); expect(bananaOption).toHaveAttribute('aria-selected', 'true'); expect(cherryOption).toHaveAttribute('aria-selected', 'false'); expect(textInput).toHaveAttribute('aria-activedescendant', bananaOption.id); userEvent.hover(cherryOption); expect(appleOption).toHaveAttribute('aria-selected', 'false'); expect(bananaOption).toHaveAttribute('aria-selected', 'false'); expect(cherryOption).toHaveAttribute('aria-selected', 'true'); expect(textInput).toHaveAttribute('aria-activedescendant', cherryOption.id); userEvent.keyboard('{ArrowDown}'); expect(appleOption).toHaveAttribute('aria-selected', 'true'); expect(bananaOption).toHaveAttribute('aria-selected', 'false'); expect(cherryOption).toHaveAttribute('aria-selected', 'false'); expect(textInput).toHaveAttribute('aria-activedescendant', appleOption.id); userEvent.keyboard('{ArrowUp}'); expect(appleOption).toHaveAttribute('aria-selected', 'false'); expect(bananaOption).toHaveAttribute('aria-selected', 'false'); expect(cherryOption).toHaveAttribute('aria-selected', 'true'); expect(textInput).toHaveAttribute('aria-activedescendant', cherryOption.id); }); });