import React, { useState } from 'react'; import userEvent from '@testing-library/user-event'; import { render, screen, within } from '@testing-library/react'; import { ListBox } from '.'; import { TextInput } from '../TextInput'; const options = [ { id: 'michael', name: 'Michael Murphy', birthDate: '1979-09-19', avatar: 'images/aurelien.webp', description: 'Growth team', }, { id: 'nayden', name: 'Nayden Lennart', birthDate: '1980-03-01', avatar: 'images/bertrand.webp', description: 'Design team', }, { id: 'nicolas', name: 'Nicolas Harvey', birthDate: '1980-06-05', avatar: 'images/mahedine.webp', description: 'Growth team', }, ]; type Option = { id: string; name: string; birthDate: string; avatar: string; description: string; }; describe('ListBox component', () => { it('should allow a User to select options', async () => { const Option = (props: { option: Option; id: string }) => { return

{props.option.name}

; }; const Wrapper = () => { const [optionsSelected, setOptionSelected] = useState([]); const [activeOption, setActiveOption] = useState(); return ( option.id} getIsOptionActive={(option) => option.id === activeOption} onOptionClick={(option) => setActiveOption(option.id)} onOptionChange={(_, id, checked) => { setOptionSelected((options) => { if (checked) { return options.concat(id); } return options.filter((optionId) => optionId !== id); }); }} onAllOptionsChange={(_, ids, checked) => { setOptionSelected(checked ? ids : []); }} checkedOptionIds={optionsSelected} aria-label="employees" header={

Employee

} > {(option, titleId) =>
); }; render(); expect(screen.getByRole('listbox', { name: 'employees' })).toBeVisible(); expect( screen.getByRole('option', { name: 'Michael Murphy' }), ).toBeVisible(); expect( screen.getByRole('option', { name: 'Nicolas Harvey' }), ).toBeVisible(); expect( screen.getByRole('option', { name: 'Nayden Lennart' }), ).toBeVisible(); await userEvent.click( screen.getByRole('checkbox', { name: 'Michael Murphy' }), ); expect( screen.getByRole('checkbox', { name: 'Michael Murphy' }), ).toBeChecked(); await userEvent.click( screen.getByRole('checkbox', { name: 'Nicolas Harvey' }), ); expect( screen.getByRole('checkbox', { name: 'Nicolas Harvey' }), ).toBeChecked(); await userEvent.click( screen.getByRole('checkbox', { name: 'Nicolas Harvey' }), ); expect( screen.getByRole('checkbox', { name: 'Select all' }), ).toBePartiallyChecked(); expect( screen.getByRole('checkbox', { name: 'Nicolas Harvey' }), ).not.toBeChecked(); await userEvent.click(screen.getByRole('checkbox', { name: 'Select all' })); expect( await screen.findByRole('checkbox', { name: 'Unselect all' }), ).toBeVisible(); screen.getAllByRole('checkbox').every((node) => expect(node).toBeChecked()); }); it('should allow a User to select one option', async () => { const handleOptionClick = vi.fn(); const Option = (props: { option: Option; id: string }) => { return

{props.option.name}

; }; const Wrapper = () => { const [activeOption, setActiveOption] = useState('michael'); return ( option.id} getIsOptionActive={(option) => option.id === activeOption} onOptionClick={(option) => { setActiveOption(option.id); handleOptionClick(); }} aria-label="employees" > {(option, titleId) => ); }; render(); expect(screen.getByRole('listbox', { name: 'employees' })).toBeVisible(); expect( screen.getByRole('option', { name: 'Michael Murphy' }), ).toHaveAttribute('aria-selected', 'true'); await userEvent.click( screen.getByRole('option', { name: 'Nicolas Harvey' }), ); expect(handleOptionClick).toHaveBeenCalled(); expect( screen.getByRole('option', { name: 'Nicolas Harvey' }), ).toHaveAttribute('aria-selected', 'true'); }); it('should allow to group options', () => { const Option = (props: { option: Option; id: string }) => { return

{props.option.name}

; }; render( option.id} onOptionClick={vi.fn()} getIsOptionActive={() => false} groupBy={(option) => { const date = new Date(option.birthDate); return `${date.getFullYear()}`; }} renderGroupedOptionsHeader={(value) => {value}} aria-label="employees" > {(option, titleId) => , ); expect(screen.getByRole('listbox', { name: 'employees' })).toBeVisible(); expect(screen.getByRole('group', { name: '1979' })).toBeVisible(); expect(screen.getByRole('group', { name: '1980' })).toBeVisible(); expect( within(screen.getByRole('group', { name: '1980' })).getAllByRole( 'option', ), ).toHaveLength(2); expect( within(screen.getByRole('group', { name: '1979' })).getByRole('option', { name: 'Michael Murphy', }), ).toBeVisible(); }); it('should diplay a header and a footer', async () => { const Option = (props: { option: Option; id: string }) => { return

{props.option.name}

; }; render( option.id} getIsOptionActive={() => false} onOptionClick={vi.fn()} aria-label="employees" header={

header placeholder

} footer={footer placeholder} > {(option, titleId) =>
, ); expect(screen.getByRole('listbox', { name: 'employees' })).toBeVisible(); await userEvent.click( screen.getByRole('option', { name: 'Michael Murphy' }), ); expect(screen.getByText('header placeholder')).toBeVisible(); expect(screen.getByText('footer placeholder')).toBeVisible(); }); it('should display an empty state when no option are provided', () => { const Option = (props: { option: Option; id: string }) => { return

{props.option.name}

; }; render( option.id} getIsOptionActive={() => false} onOptionClick={vi.fn()} aria-label="employees" emptyState={{ title: 'No employee found' }} > {(option, titleId) => , ); expect( screen.getByRole('listbox', { name: 'employees' }), ).toHaveAccessibleDescription('No employee found'); }); it('should support keyboard navigation', async () => { const Option = (props: { option: Option; id: string }) => { return

{props.option.name}

; }; const Wrapper = () => { const [optionsSelected, setOptionSelected] = useState([]); const [activeOption, setActiveOption] = useState(); return ( option.id} getIsOptionActive={(option) => option.id === activeOption} onOptionClick={(option) => setActiveOption(option.id)} aria-label="employees" header={

Employee

} onOptionChange={(_, id, checked) => { setOptionSelected((options) => { if (checked) { return options.concat(id); } return options.filter((optionId) => optionId !== id); }); }} onAllOptionsChange={(_, ids, checked) => { setOptionSelected(checked ? ids : []); }} checkedOptionIds={optionsSelected} > {(option, titleId) =>
); }; render(); const firstOption = screen.getByRole('option', { name: 'Michael Murphy' }); const firstCheckbox = screen.getByRole('checkbox', { name: 'Michael Murphy', }); const secondOption = screen.getByRole('option', { name: 'Nayden Lennart' }); const lastOption = screen.getByRole('option', { name: 'Nicolas Harvey' }); await userEvent.click(firstOption); expect(firstOption).toHaveAttribute('aria-selected', 'true'); expect(secondOption).toHaveAttribute('aria-selected', 'false'); await userEvent.click(firstCheckbox); // Check Michael Murphy checkbox expect(firstCheckbox).toBeChecked(); await userEvent.keyboard('{ }'); // Uncheck Michael Murphy checkbox using keyboard expect(firstCheckbox).not.toBeChecked(); await userEvent.keyboard('{ArrowDown}'); expect(firstOption).toHaveAttribute('aria-selected', 'false'); expect(secondOption).toHaveAttribute('aria-selected', 'true'); await userEvent.keyboard('{ArrowDown}'); expect(secondOption).toHaveAttribute('aria-selected', 'false'); expect(lastOption).toHaveAttribute('aria-selected', 'true'); await userEvent.keyboard('{ArrowUp}'); expect(secondOption).toHaveAttribute('aria-selected', 'true'); expect(lastOption).toHaveAttribute('aria-selected', 'false'); await userEvent.keyboard('{Home}'); expect(firstOption).toHaveAttribute('aria-selected', 'true'); expect(secondOption).toHaveAttribute('aria-selected', 'false'); await userEvent.keyboard('{End}'); expect(firstOption).toHaveAttribute('aria-selected', 'false'); expect(lastOption).toHaveAttribute('aria-selected', 'true'); }); it('should disable the top checkbox if every option is disabled', () => { const Option = (props: { option: Option; id: string }) => { return

{props.option.name}

; }; const Wrapper = () => { const [activeOption, setActiveOption] = useState(); return ( option.id} getIsOptionActive={(option) => option.id === activeOption} onOptionClick={(option) => setActiveOption(option.id)} onOptionChange={() => vi.fn()} onAllOptionsChange={() => vi.fn()} checkedOptionIds={[]} aria-label="employees" header={

Employee

} getIsOptionDisabled={() => true} > {(option, titleId) =>
); }; render(); expect(screen.getByRole('checkbox', { name: 'Select all' })).toBeDisabled(); }); it('should not update focus if focus is outside the listbox', async () => { const Option = (props: { option: Option; id: string }) => { return

{props.option.name}

; }; const Wrapper = () => { const [value, setValue] = useState('Nayden'); const regex = new RegExp(value, 'gi'); const filteredOptions = options.filter(({ name }) => regex.test(name)); return (
setValue(e.target.value)} aria-label="Search" /> option.id} getIsOptionActive={(option) => option.id === 'michael'} onOptionClick={vi.fn()} aria-label="employees" > {(option, titleId) =>
); }; render(); expect( screen.getByRole('option', { name: 'Nayden Lennart' }), ).toBeVisible(); expect( screen.queryByRole('option', { name: 'Michael Murphy' }), ).not.toBeInTheDocument(); await userEvent.clear(screen.getByRole('textbox')); expect( screen.getByRole('option', { name: 'Michael Murphy' }), ).toBeVisible(); expect( screen.getByRole('option', { name: 'Michael Murphy' }), ).toHaveAttribute('aria-selected', 'true'); expect(screen.getByRole('textbox')).toHaveFocus(); }); it('should allow a User to select options', async () => { const Option = (props: { option: Option; id: string }) => { return

{props.option.name}

; }; const onOptionClick = vi.fn(); const Wrapper = () => { const [optionsSelected, setOptionSelected] = useState([]); return ( option.id} getIsOptionCheckable={(option) => option.id !== 'michael'} onOptionClick={onOptionClick} onOptionChange={(_, id, checked) => { setOptionSelected((options) => { if (checked) { return options.concat(id); } return options.filter((optionId) => optionId !== id); }); }} onAllOptionsChange={(_, ids, checked) => { setOptionSelected(checked ? ids : []); }} checkedOptionIds={optionsSelected} aria-label="employees" header={

Employee

} > {(option, titleId) =>
); }; render(); expect( screen.getByRole('checkbox', { name: 'Michael Murphy' }), ).toBeDisabled(); await userEvent.click(screen.getByRole('checkbox', { name: 'Select all' })); expect( screen.getByRole('checkbox', { name: 'Michael Murphy' }), ).not.toBeChecked(); await userEvent.click( screen.getByRole('option', { name: 'Michael Murphy' }), ); expect(onOptionClick).toHaveBeenCalled(); }); });