From 9ad19a5cd0d50bbef710dae29d4eabd3f15398e5 Mon Sep 17 00:00:00 2001 From: Josh Black Date: Tue, 16 Jul 2024 11:11:28 -0500 Subject: [PATCH] test(SelectPanel): update tests to account for more of API (#4705) * chore: check-in work * test: add additional tests for select panel * test(SelectPanel): add tests for open, onOpenChange --------- Co-authored-by: Josh Black --- .../src/SelectPanel/SelectPanel.test.tsx | 395 ++++++++++++++++-- 1 file changed, 356 insertions(+), 39 deletions(-) diff --git a/packages/react/src/SelectPanel/SelectPanel.test.tsx b/packages/react/src/SelectPanel/SelectPanel.test.tsx index fa9b8771dfb..a3a2d7d2576 100644 --- a/packages/react/src/SelectPanel/SelectPanel.test.tsx +++ b/packages/react/src/SelectPanel/SelectPanel.test.tsx @@ -1,59 +1,376 @@ -import {render as HTMLRender} from '@testing-library/react' -import axe from 'axe-core' +import {render, screen} from '@testing-library/react' import React from 'react' -import theme from '../theme' -import {SelectPanel} from '../SelectPanel' -import {behavesAsComponent, checkExports} from '../utils/testing' -import {BaseStyles, SSRProvider, ThemeProvider} from '..' -import type {ItemInput} from '../deprecated/ActionList/List' +import {SelectPanel, type SelectPanelProps} from '../SelectPanel' +import {userEvent} from '@testing-library/user-event' +import ThemeProvider from '../ThemeProvider' -const items = [{text: 'Foo'}, {text: 'Bar'}, {text: 'Baz'}, {text: 'Bon'}] as ItemInput[] +const items: SelectPanelProps['items'] = [ + { + text: 'item one', + }, + { + text: 'item two', + }, + { + text: 'item three', + }, +] -function SimpleSelectPanel(): JSX.Element { - const [selected, setSelected] = React.useState([]) - const [, setFilter] = React.useState('') +function BasicSelectPanel() { + const [selected, setSelected] = React.useState([]) + const [filter, setFilter] = React.useState('') const [open, setOpen] = React.useState(false) + const onSelectedChange = (selected: SelectPanelProps['items']) => { + setSelected(selected) + } + return ( - - - + + { + setFilter(value) + }} + open={open} + onOpenChange={isOpen => { + setOpen(isOpen) + }} + /> + + ) +} + +global.Element.prototype.scrollTo = jest.fn() + +describe('SelectPanel', () => { + it('should render an anchor to open the select panel using `placeholder`', () => { + render() + + expect(screen.getByText('Select items')).toBeInTheDocument() + + const trigger = screen.getByRole('button', { + name: 'Select items', + }) + expect(trigger).toHaveAttribute('aria-haspopup', 'true') + expect(trigger).toHaveAttribute('aria-expanded', 'false') + }) + + it('should open the select panel when activating the trigger', async () => { + const user = userEvent.setup() + + render() + + await user.click(screen.getByText('Select items')) + + // Verify that the button has `aria-expanded="true"` after opening + const trigger = screen.getByRole('button', { + name: 'Select items', + }) + expect(trigger).toHaveAttribute('aria-expanded', 'true') + + // Verify that the input and listbox are visible + expect(screen.getByLabelText('Filter items')).toBeVisible() + expect(screen.getByRole('listbox')).toBeVisible() + + expect(screen.getByLabelText('Filter items')).toHaveFocus() + }) + + it('should close the select panel when pressing Escape', async () => { + const user = userEvent.setup() + + render() + + await user.click(screen.getByText('Select items')) + await user.keyboard('{Escape}') + + expect(screen.getByRole('button', {name: 'Select items'})).toHaveFocus() + expect(screen.getByRole('button', {name: 'Select items'})).toHaveAttribute('aria-expanded', 'false') + }) + + it('should close the select panel when clicking outside of the select panel', async () => { + const user = userEvent.setup() + + render( + <> + + + , + ) + + await user.click(screen.getByText('Select items')) + await user.click(screen.getByText('outer button')) + + expect(screen.getByRole('button', {name: 'Select items'})).toHaveAttribute('aria-expanded', 'false') + }) + + it('should open a dialog that is labelled by `title` and described by `subtitle`', async () => { + const user = userEvent.setup() + + render() + + await user.click(screen.getByText('Select items')) + + expect( + screen.getByRole('dialog', { + name: 'test title', + description: 'test subtitle', + }), + ).toBeInTheDocument() + }) + + it('should call `onOpenChange` when opening and closing the dialog', async () => { + const onOpenChange = jest.fn() + + function SelectPanelOpenChange() { + const [selected, setSelected] = React.useState([]) + const [filter, setFilter] = React.useState('') + const [open, setOpen] = React.useState(false) + + const onSelectedChange = (selected: SelectPanelProps['items']) => { + setSelected(selected) + } + + return ( + + { + setFilter(value) + }} open={open} - onOpenChange={setOpen} + onOpenChange={(...args) => { + onOpenChange(...args) + setOpen(args[0]) + }} /> -
-
-
-
- ) -} + + ) + } -describe('SelectPanel', () => { - afterEach(() => { - jest.clearAllMocks() + const user = userEvent.setup() + + render() + + // Open by click + await user.click(screen.getByText('Select items')) + expect(onOpenChange).toHaveBeenLastCalledWith(true, 'anchor-click') + + // Close by click on anchor + await user.click(screen.getByText('Select items')) + expect(onOpenChange).toHaveBeenLastCalledWith(false, 'anchor-click') + + // Open by button activation + await user.type(screen.getByText('Select items'), '{Space}') + expect(onOpenChange).toHaveBeenLastCalledWith(true, 'anchor-click') + + // Close by Escape key + await user.keyboard('{Escape}') + expect(onOpenChange).toHaveBeenLastCalledWith(false, 'escape') + + // Close by click outside + await user.click(screen.getByText('Select items')) + await user.click(screen.getByText('Outside of select panel')) + expect(onOpenChange).toHaveBeenLastCalledWith(false, 'click-outside') }) - behavesAsComponent({ - Component: SelectPanel, - options: {skipAs: true, skipSx: true}, - toRender: () => , + describe('selection', () => { + it('should select an active option when activated', async () => { + const user = userEvent.setup() + + render() + + await user.click(screen.getByText('Select items')) + + await user.type(document.activeElement!, '{Enter}') + expect( + screen.getByRole('option', { + name: 'item one', + }), + ).toHaveAttribute('aria-selected', 'true') + + await user.type(document.activeElement!, '{Enter}') + expect( + screen.getByRole('option', { + name: 'item one', + }), + ).toHaveAttribute('aria-selected', 'false') + + await user.click(screen.getByText('item one')) + expect( + screen.getByRole('option', { + name: 'item one', + }), + ).toHaveAttribute('aria-selected', 'true') + + await user.click(screen.getByRole('option', {name: 'item one'})) + expect( + screen.getByRole('option', { + name: 'item one', + }), + ).toHaveAttribute('aria-selected', 'false') + }) + + it('should support navigating through items with ArrowUp and ArrowDown', async () => { + const user = userEvent.setup() + + render() + + await user.click(screen.getByText('Select items')) + + // First item by default should be the active element + expect(document.activeElement!).toHaveAttribute( + 'aria-activedescendant', + screen.getByRole('option', {name: 'item one'}).id, + ) + + await user.type(document.activeElement!, '{ArrowDown}') + expect(document.activeElement!).toHaveAttribute( + 'aria-activedescendant', + screen.getByRole('option', {name: 'item two'}).id, + ) + + await user.type(document.activeElement!, '{ArrowDown}') + expect(document.activeElement!).toHaveAttribute( + 'aria-activedescendant', + screen.getByRole('option', {name: 'item three'}).id, + ) + + // At end of list, should wrap to the beginning + await user.type(document.activeElement!, '{ArrowDown}') + expect(document.activeElement!).toHaveAttribute( + 'aria-activedescendant', + screen.getByRole('option', {name: 'item one'}).id, + ) + + // At beginning of list, ArrowUp should wrap to the end + await user.type(document.activeElement!, '{ArrowUp}') + expect(document.activeElement!).toHaveAttribute( + 'aria-activedescendant', + screen.getByRole('option', {name: 'item three'}).id, + ) + + await user.type(document.activeElement!, '{ArrowUp}') + expect(document.activeElement!).toHaveAttribute( + 'aria-activedescendant', + screen.getByRole('option', {name: 'item two'}).id, + ) + + await user.type(document.activeElement!, '{ArrowUp}') + expect(document.activeElement!).toHaveAttribute( + 'aria-activedescendant', + screen.getByRole('option', {name: 'item one'}).id, + ) + }) }) - checkExports('SelectPanel', { - default: undefined, - SelectPanel, + describe('filtering', () => { + function FilterableSelectPanel() { + const [selected, setSelected] = React.useState([]) + const [filter, setFilter] = React.useState('') + const [open, setOpen] = React.useState(false) + + const onSelectedChange = (selected: SelectPanelProps['items']) => { + setSelected(selected) + } + + return ( + + item.text?.includes(filter))} + placeholder="Select items" + placeholderText="Filter items" + selected={selected} + onSelectedChange={onSelectedChange} + filterValue={filter} + onFilterChange={value => { + setFilter(value) + }} + open={open} + onOpenChange={isOpen => { + setOpen(isOpen) + }} + /> + + ) + } + + it('should filter the list of items when the user types into the input', async () => { + const user = userEvent.setup() + + render() + + await user.click(screen.getByText('Select items')) + + expect(screen.getAllByRole('option')).toHaveLength(3) + + await user.type(document.activeElement!, 'two') + expect(screen.getAllByRole('option')).toHaveLength(1) + }) + + it.todo('should announce the number of results') + + it.todo('should announce when no results are available') }) - it('should have no axe violations', async () => { - const {container} = HTMLRender() - const results = await axe.run(container) - expect(results).toHaveNoViolations() + describe('with footer', () => { + function SelectPanelWithFooter() { + const [selected, setSelected] = React.useState([]) + const [filter, setFilter] = React.useState('') + const [open, setOpen] = React.useState(false) + + const onSelectedChange = (selected: SelectPanelProps['items']) => { + setSelected(selected) + } + + return ( + + test footer} + items={items} + placeholder="Select items" + placeholderText="Filter items" + selected={selected} + onSelectedChange={onSelectedChange} + filterValue={filter} + onFilterChange={value => { + setFilter(value) + }} + open={open} + onOpenChange={isOpen => { + setOpen(isOpen) + }} + /> + + ) + } + + it('should render the provided `footer` at the bottom of the dialog', async () => { + const user = userEvent.setup() + + render() + + await user.click(screen.getByText('Select items')) + expect(screen.getByText('test footer')).toBeVisible() + }) }) })