Files
headlessui/packages/@headlessui-react/src/components/radio-group/radio-group.test.tsx
T
Robin Malfait 08b419e9b7 Revert "prepare 1.6.3"
This reverts commit 3aaf20b9ac.
2022-05-25 15:25:48 +02:00

1200 lines
38 KiB
TypeScript

import React, { createElement, useState } from 'react'
import { render } from '@testing-library/react'
import { RadioGroup } from './radio-group'
import { suppressConsoleLogs } from '../../test-utils/suppress-console-logs'
import { press, focus, Keys, shift, click } from '../../test-utils/interactions'
import {
getByText,
assertRadioGroupLabel,
getRadioGroupOptions,
assertFocusable,
assertNotFocusable,
assertActiveElement,
} from '../../test-utils/accessibility-assertions'
jest.mock('../../hooks/use-id')
beforeAll(() => {
jest.spyOn(window, 'requestAnimationFrame').mockImplementation(setImmediate as any)
jest.spyOn(window, 'cancelAnimationFrame').mockImplementation(clearImmediate as any)
})
afterAll(() => jest.restoreAllMocks())
describe('Safe guards', () => {
it.each([['RadioGroup.Option', RadioGroup.Option]])(
'should error when we are using a <%s /> without a parent <RadioGroup />',
suppressConsoleLogs((name, Component) => {
expect(() => render(createElement(Component))).toThrowError(
`<${name} /> is missing a parent <RadioGroup /> component.`
)
})
)
it(
'should be possible to render a RadioGroup without crashing',
suppressConsoleLogs(async () => {
render(
<RadioGroup value={undefined} onChange={console.log}>
<RadioGroup.Label>Pizza Delivery</RadioGroup.Label>
<RadioGroup.Option value="pickup">Pickup</RadioGroup.Option>
<RadioGroup.Option value="home-delivery">Home delivery</RadioGroup.Option>
<RadioGroup.Option value="dine-in">Dine in</RadioGroup.Option>
</RadioGroup>
)
assertRadioGroupLabel({ textContent: 'Pizza Delivery' })
})
)
it('should be possible to render a RadioGroup without options and without crashing', () => {
render(<RadioGroup value={undefined} onChange={console.log} />)
})
})
describe('Rendering', () => {
it(
'should be possible to render a RadioGroup, where the first element is tabbable (value is undefined)',
suppressConsoleLogs(async () => {
render(
<RadioGroup value={undefined} onChange={console.log}>
<RadioGroup.Label>Pizza Delivery</RadioGroup.Label>
<RadioGroup.Option value="pickup">Pickup</RadioGroup.Option>
<RadioGroup.Option value="home-delivery">Home delivery</RadioGroup.Option>
<RadioGroup.Option value="dine-in">Dine in</RadioGroup.Option>
</RadioGroup>
)
expect(getRadioGroupOptions()).toHaveLength(3)
assertFocusable(getByText('Pickup'))
assertNotFocusable(getByText('Home delivery'))
assertNotFocusable(getByText('Dine in'))
})
)
it(
'should be possible to render a RadioGroup, where the first element is tabbable (value is null)',
suppressConsoleLogs(async () => {
render(
<RadioGroup value={null} onChange={console.log}>
<RadioGroup.Label>Pizza Delivery</RadioGroup.Label>
<RadioGroup.Option value="pickup">Pickup</RadioGroup.Option>
<RadioGroup.Option value="home-delivery">Home delivery</RadioGroup.Option>
<RadioGroup.Option value="dine-in">Dine in</RadioGroup.Option>
</RadioGroup>
)
expect(getRadioGroupOptions()).toHaveLength(3)
assertFocusable(getByText('Pickup'))
assertNotFocusable(getByText('Home delivery'))
assertNotFocusable(getByText('Dine in'))
})
)
it(
'should be possible to render a RadioGroup with an active value',
suppressConsoleLogs(async () => {
render(
<RadioGroup value="home-delivery" onChange={console.log}>
<RadioGroup.Label>Pizza Delivery</RadioGroup.Label>
<RadioGroup.Option value="pickup">Pickup</RadioGroup.Option>
<RadioGroup.Option value="home-delivery">Home delivery</RadioGroup.Option>
<RadioGroup.Option value="dine-in">Dine in</RadioGroup.Option>
</RadioGroup>
)
expect(getRadioGroupOptions()).toHaveLength(3)
assertNotFocusable(getByText('Pickup'))
assertFocusable(getByText('Home delivery'))
assertNotFocusable(getByText('Dine in'))
})
)
it(
'should guarantee the radio option order after a few unmounts',
suppressConsoleLogs(async () => {
function Example() {
let [showFirst, setShowFirst] = useState(false)
let [active, setActive] = useState()
return (
<>
<button onClick={() => setShowFirst((v) => !v)}>Toggle</button>
<RadioGroup value={active} onChange={setActive}>
<RadioGroup.Label>Pizza Delivery</RadioGroup.Label>
{showFirst && <RadioGroup.Option value="pickup">Pickup</RadioGroup.Option>}
<RadioGroup.Option value="home-delivery">Home delivery</RadioGroup.Option>
<RadioGroup.Option value="dine-in">Dine in</RadioGroup.Option>
</RadioGroup>
</>
)
}
render(<Example />)
await click(getByText('Toggle')) // Render the pickup again
await press(Keys.Tab) // Focus first element
assertActiveElement(getByText('Pickup'))
await press(Keys.ArrowUp) // Loop around
assertActiveElement(getByText('Dine in'))
await press(Keys.ArrowUp) // Up again
assertActiveElement(getByText('Home delivery'))
})
)
it(
'should be possible to disable a RadioGroup',
suppressConsoleLogs(async () => {
let changeFn = jest.fn()
function Example() {
let [disabled, setDisabled] = useState(true)
return (
<>
<button onClick={() => setDisabled((v) => !v)}>Toggle</button>
<RadioGroup value={undefined} onChange={changeFn} disabled={disabled}>
<RadioGroup.Label>Pizza Delivery</RadioGroup.Label>
<RadioGroup.Option value="pickup">Pickup</RadioGroup.Option>
<RadioGroup.Option value="home-delivery">Home delivery</RadioGroup.Option>
<RadioGroup.Option value="dine-in">Dine in</RadioGroup.Option>
<RadioGroup.Option value="render-prop" data-value="render-prop">
{JSON.stringify}
</RadioGroup.Option>
</RadioGroup>
</>
)
}
render(<Example />)
// Try to click one a few options
await click(getByText('Pickup'))
await click(getByText('Dine in'))
// Verify that the RadioGroup.Option gets the disabled state
expect(document.querySelector('[data-value="render-prop"]')).toHaveTextContent(
JSON.stringify({
checked: false,
disabled: true,
active: false,
})
)
// Make sure that the onChange handler never got called
expect(changeFn).toHaveBeenCalledTimes(0)
// Make sure that all the options get an `aria-disabled`
let options = getRadioGroupOptions()
expect(options).toHaveLength(4)
for (let option of options) expect(option).toHaveAttribute('aria-disabled', 'true')
// Toggle the disabled state
await click(getByText('Toggle'))
// Verify that the RadioGroup.Option gets the disabled state
expect(document.querySelector('[data-value="render-prop"]')).toHaveTextContent(
JSON.stringify({
checked: false,
disabled: false,
active: false,
})
)
// Try to click one a few options
await click(getByText('Pickup'))
// Make sure that the onChange handler got called
expect(changeFn).toHaveBeenCalledTimes(1)
})
)
it(
'should be possible to disable a RadioGroup.Option',
suppressConsoleLogs(async () => {
let changeFn = jest.fn()
function Example() {
let [disabled, setDisabled] = useState(true)
return (
<>
<button onClick={() => setDisabled((v) => !v)}>Toggle</button>
<RadioGroup value={undefined} onChange={changeFn}>
<RadioGroup.Label>Pizza Delivery</RadioGroup.Label>
<RadioGroup.Option value="pickup">Pickup</RadioGroup.Option>
<RadioGroup.Option value="home-delivery">Home delivery</RadioGroup.Option>
<RadioGroup.Option value="dine-in">Dine in</RadioGroup.Option>
<RadioGroup.Option value="render-prop" disabled={disabled} data-value="render-prop">
{JSON.stringify}
</RadioGroup.Option>
</RadioGroup>
</>
)
}
render(<Example />)
// Try to click the disabled option
await click(document.querySelector('[data-value="render-prop"]'))
// Verify that the RadioGroup.Option gets the disabled state
expect(document.querySelector('[data-value="render-prop"]')).toHaveTextContent(
JSON.stringify({
checked: false,
disabled: true,
active: false,
})
)
// Make sure that the onChange handler never got called
expect(changeFn).toHaveBeenCalledTimes(0)
// Make sure that the option with value "render-prop" gets an `aria-disabled`
let options = getRadioGroupOptions()
expect(options).toHaveLength(4)
for (let option of options) {
if (option.dataset.value) {
expect(option).toHaveAttribute('aria-disabled', 'true')
} else {
expect(option).not.toHaveAttribute('aria-disabled')
}
}
// Toggle the disabled state
await click(getByText('Toggle'))
// Verify that the RadioGroup.Option gets the disabled state
expect(document.querySelector('[data-value="render-prop"]')).toHaveTextContent(
JSON.stringify({
checked: false,
disabled: false,
active: false,
})
)
// Try to click one a few options
await click(document.querySelector('[data-value="render-prop"]'))
// Make sure that the onChange handler got called
expect(changeFn).toHaveBeenCalledTimes(1)
})
)
it(
'should guarantee the order of DOM nodes when performing actions',
suppressConsoleLogs(async () => {
function Example({ hide = false }) {
return (
<RadioGroup value={undefined} onChange={() => {}}>
<RadioGroup.Option value="a">Option 1</RadioGroup.Option>
{!hide && <RadioGroup.Option value="b">Option 2</RadioGroup.Option>}
<RadioGroup.Option value="c">Option 3</RadioGroup.Option>
</RadioGroup>
)
}
let { rerender } = render(<Example />)
// Focus the RadioGroup
await press(Keys.Tab)
rerender(<Example hide={true} />) // Remove RadioGroup.Option 2
rerender(<Example hide={false} />) // Re-add RadioGroup.Option 2
// Verify that the first radio group option is active
assertActiveElement(getByText('Option 1'))
await press(Keys.ArrowDown)
// Verify that the second radio group option is active
assertActiveElement(getByText('Option 2'))
await press(Keys.ArrowDown)
// Verify that the third radio group option is active
assertActiveElement(getByText('Option 3'))
})
)
describe('Equality', () => {
let options = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 3, name: 'Charlie' },
]
it(
'should use object equality by default',
suppressConsoleLogs(async () => {
render(
<RadioGroup value={options[1]} onChange={console.log}>
{options.map((option) => (
<RadioGroup.Option
key={option.id}
value={option}
className={(info) => JSON.stringify(info)}
>
{option.name}
</RadioGroup.Option>
))}
</RadioGroup>
)
let bob = getRadioGroupOptions()[1]
expect(bob).toHaveAttribute(
'class',
JSON.stringify({ checked: true, disabled: false, active: false })
)
})
)
it(
'should be possible to compare objects by a field',
suppressConsoleLogs(async () => {
render(
<RadioGroup value={{ id: 2, name: 'Bob' }} onChange={console.log} by="id">
{options.map((option) => (
<RadioGroup.Option
key={option.id}
value={option}
className={(info) => JSON.stringify(info)}
>
{option.name}
</RadioGroup.Option>
))}
</RadioGroup>
)
let bob = getRadioGroupOptions()[1]
expect(bob).toHaveAttribute(
'class',
JSON.stringify({ checked: true, disabled: false, active: false })
)
})
)
it(
'should be possible to compare objects by a comparator function',
suppressConsoleLogs(async () => {
render(
<RadioGroup
value={{ id: 2, name: 'Bob' }}
onChange={console.log}
by={(a, z) => a.id === z.id}
>
{options.map((option) => (
<RadioGroup.Option
key={option.id}
value={option}
className={(info) => JSON.stringify(info)}
>
{option.name}
</RadioGroup.Option>
))}
</RadioGroup>
)
let bob = getRadioGroupOptions()[1]
expect(bob).toHaveAttribute(
'class',
JSON.stringify({ checked: true, disabled: false, active: false })
)
})
)
})
})
describe('Keyboard interactions', () => {
describe('`Tab` key', () => {
it(
'should be possible to tab to the first item',
suppressConsoleLogs(async () => {
render(
<RadioGroup value={undefined} onChange={console.log}>
<RadioGroup.Label>Pizza Delivery</RadioGroup.Label>
<RadioGroup.Option value="pickup">Pickup</RadioGroup.Option>
<RadioGroup.Option value="home-delivery">Home delivery</RadioGroup.Option>
<RadioGroup.Option value="dine-in">Dine in</RadioGroup.Option>
</RadioGroup>
)
await press(Keys.Tab)
assertActiveElement(getByText('Pickup'))
})
)
it(
'should not change the selected element on focus',
suppressConsoleLogs(async () => {
let changeFn = jest.fn()
render(
<RadioGroup value={undefined} onChange={changeFn}>
<RadioGroup.Label>Pizza Delivery</RadioGroup.Label>
<RadioGroup.Option value="pickup">Pickup</RadioGroup.Option>
<RadioGroup.Option value="home-delivery">Home delivery</RadioGroup.Option>
<RadioGroup.Option value="dine-in">Dine in</RadioGroup.Option>
</RadioGroup>
)
await press(Keys.Tab)
assertActiveElement(getByText('Pickup'))
expect(changeFn).toHaveBeenCalledTimes(0)
})
)
it(
'should be possible to tab to the active item',
suppressConsoleLogs(async () => {
render(
<RadioGroup value="home-delivery" onChange={console.log}>
<RadioGroup.Label>Pizza Delivery</RadioGroup.Label>
<RadioGroup.Option value="pickup">Pickup</RadioGroup.Option>
<RadioGroup.Option value="home-delivery">Home delivery</RadioGroup.Option>
<RadioGroup.Option value="dine-in">Dine in</RadioGroup.Option>
</RadioGroup>
)
await press(Keys.Tab)
assertActiveElement(getByText('Home delivery'))
})
)
it(
'should not change the selected element on focus (when selecting the active item)',
suppressConsoleLogs(async () => {
let changeFn = jest.fn()
render(
<RadioGroup value="home-delivery" onChange={changeFn}>
<RadioGroup.Label>Pizza Delivery</RadioGroup.Label>
<RadioGroup.Option value="pickup">Pickup</RadioGroup.Option>
<RadioGroup.Option value="home-delivery">Home delivery</RadioGroup.Option>
<RadioGroup.Option value="dine-in">Dine in</RadioGroup.Option>
</RadioGroup>
)
await press(Keys.Tab)
assertActiveElement(getByText('Home delivery'))
expect(changeFn).toHaveBeenCalledTimes(0)
})
)
it(
'should be possible to tab out of the radio group (no selected value)',
suppressConsoleLogs(async () => {
render(
<>
<button>Before</button>
<RadioGroup value={undefined} onChange={console.log}>
<RadioGroup.Label>Pizza Delivery</RadioGroup.Label>
<RadioGroup.Option value="pickup">Pickup</RadioGroup.Option>
<RadioGroup.Option value="home-delivery">Home delivery</RadioGroup.Option>
<RadioGroup.Option value="dine-in">Dine in</RadioGroup.Option>
</RadioGroup>
<button>After</button>
</>
)
await press(Keys.Tab)
assertActiveElement(getByText('Before'))
await press(Keys.Tab)
assertActiveElement(getByText('Pickup'))
await press(Keys.Tab)
assertActiveElement(getByText('After'))
})
)
it(
'should be possible to tab out of the radio group (selected value)',
suppressConsoleLogs(async () => {
render(
<>
<button>Before</button>
<RadioGroup value="home-delivery" onChange={console.log}>
<RadioGroup.Label>Pizza Delivery</RadioGroup.Label>
<RadioGroup.Option value="pickup">Pickup</RadioGroup.Option>
<RadioGroup.Option value="home-delivery">Home delivery</RadioGroup.Option>
<RadioGroup.Option value="dine-in">Dine in</RadioGroup.Option>
</RadioGroup>
<button>After</button>
</>
)
await press(Keys.Tab)
assertActiveElement(getByText('Before'))
await press(Keys.Tab)
assertActiveElement(getByText('Home delivery'))
await press(Keys.Tab)
assertActiveElement(getByText('After'))
})
)
})
describe('`Shift+Tab` key', () => {
it(
'should be possible to tab to the first item',
suppressConsoleLogs(async () => {
render(
<>
<RadioGroup value={undefined} onChange={console.log}>
<RadioGroup.Label>Pizza Delivery</RadioGroup.Label>
<RadioGroup.Option value="pickup">Pickup</RadioGroup.Option>
<RadioGroup.Option value="home-delivery">Home delivery</RadioGroup.Option>
<RadioGroup.Option value="dine-in">Dine in</RadioGroup.Option>
</RadioGroup>
<button>After</button>
</>
)
await focus(getByText('After'))
await press(shift(Keys.Tab))
assertActiveElement(getByText('Pickup'))
})
)
it(
'should not change the selected element on focus',
suppressConsoleLogs(async () => {
let changeFn = jest.fn()
render(
<>
<RadioGroup value={undefined} onChange={changeFn}>
<RadioGroup.Label>Pizza Delivery</RadioGroup.Label>
<RadioGroup.Option value="pickup">Pickup</RadioGroup.Option>
<RadioGroup.Option value="home-delivery">Home delivery</RadioGroup.Option>
<RadioGroup.Option value="dine-in">Dine in</RadioGroup.Option>
</RadioGroup>
<button>After</button>
</>
)
await focus(getByText('After'))
await press(shift(Keys.Tab))
assertActiveElement(getByText('Pickup'))
expect(changeFn).toHaveBeenCalledTimes(0)
})
)
it(
'should be possible to tab to the active item',
suppressConsoleLogs(async () => {
render(
<>
<RadioGroup value="home-delivery" onChange={console.log}>
<RadioGroup.Label>Pizza Delivery</RadioGroup.Label>
<RadioGroup.Option value="pickup">Pickup</RadioGroup.Option>
<RadioGroup.Option value="home-delivery">Home delivery</RadioGroup.Option>
<RadioGroup.Option value="dine-in">Dine in</RadioGroup.Option>
</RadioGroup>
<button>After</button>
</>
)
await focus(getByText('After'))
await press(shift(Keys.Tab))
assertActiveElement(getByText('Home delivery'))
})
)
it(
'should not change the selected element on focus (when selecting the active item)',
suppressConsoleLogs(async () => {
let changeFn = jest.fn()
render(
<>
<RadioGroup value="home-delivery" onChange={changeFn}>
<RadioGroup.Label>Pizza Delivery</RadioGroup.Label>
<RadioGroup.Option value="pickup">Pickup</RadioGroup.Option>
<RadioGroup.Option value="home-delivery">Home delivery</RadioGroup.Option>
<RadioGroup.Option value="dine-in">Dine in</RadioGroup.Option>
</RadioGroup>
<button>After</button>
</>
)
await focus(getByText('After'))
await press(shift(Keys.Tab))
assertActiveElement(getByText('Home delivery'))
expect(changeFn).toHaveBeenCalledTimes(0)
})
)
it(
'should be possible to tab out of the radio group (no selected value)',
suppressConsoleLogs(async () => {
render(
<>
<button>Before</button>
<RadioGroup value={undefined} onChange={console.log}>
<RadioGroup.Label>Pizza Delivery</RadioGroup.Label>
<RadioGroup.Option value="pickup">Pickup</RadioGroup.Option>
<RadioGroup.Option value="home-delivery">Home delivery</RadioGroup.Option>
<RadioGroup.Option value="dine-in">Dine in</RadioGroup.Option>
</RadioGroup>
<button>After</button>
</>
)
await focus(getByText('After'))
await press(shift(Keys.Tab))
assertActiveElement(getByText('Pickup'))
await press(shift(Keys.Tab))
assertActiveElement(getByText('Before'))
})
)
it(
'should be possible to tab out of the radio group (selected value)',
suppressConsoleLogs(async () => {
render(
<>
<button>Before</button>
<RadioGroup value="home-delivery" onChange={console.log}>
<RadioGroup.Label>Pizza Delivery</RadioGroup.Label>
<RadioGroup.Option value="pickup">Pickup</RadioGroup.Option>
<RadioGroup.Option value="home-delivery">Home delivery</RadioGroup.Option>
<RadioGroup.Option value="dine-in">Dine in</RadioGroup.Option>
</RadioGroup>
<button>After</button>
</>
)
await focus(getByText('After'))
await press(shift(Keys.Tab))
assertActiveElement(getByText('Home delivery'))
await press(shift(Keys.Tab))
assertActiveElement(getByText('Before'))
})
)
})
describe('`ArrowLeft` key', () => {
it(
'should go to the previous item when pressing the ArrowLeft key',
suppressConsoleLogs(async () => {
let changeFn = jest.fn()
render(
<>
<button>Before</button>
<RadioGroup value={undefined} onChange={changeFn}>
<RadioGroup.Label>Pizza Delivery</RadioGroup.Label>
<RadioGroup.Option value="pickup">Pickup</RadioGroup.Option>
<RadioGroup.Option value="home-delivery">Home delivery</RadioGroup.Option>
<RadioGroup.Option value="dine-in">Dine in</RadioGroup.Option>
</RadioGroup>
<button>After</button>
</>
)
// Focus the "Before" button
await press(Keys.Tab)
// Focus the RadioGroup
await press(Keys.Tab)
assertActiveElement(getByText('Pickup'))
await press(Keys.ArrowLeft) // Loop around
assertActiveElement(getByText('Dine in'))
await press(Keys.ArrowLeft)
assertActiveElement(getByText('Home delivery'))
expect(changeFn).toHaveBeenCalledTimes(2)
expect(changeFn).toHaveBeenNthCalledWith(1, 'dine-in')
expect(changeFn).toHaveBeenNthCalledWith(2, 'home-delivery')
})
)
})
describe('`ArrowUp` key', () => {
it(
'should go to the previous item when pressing the ArrowUp key',
suppressConsoleLogs(async () => {
let changeFn = jest.fn()
render(
<>
<button>Before</button>
<RadioGroup value={undefined} onChange={changeFn}>
<RadioGroup.Label>Pizza Delivery</RadioGroup.Label>
<RadioGroup.Option value="pickup">Pickup</RadioGroup.Option>
<RadioGroup.Option value="home-delivery">Home delivery</RadioGroup.Option>
<RadioGroup.Option value="dine-in">Dine in</RadioGroup.Option>
</RadioGroup>
<button>After</button>
</>
)
// Focus the "Before" button
await press(Keys.Tab)
// Focus the RadioGroup
await press(Keys.Tab)
assertActiveElement(getByText('Pickup'))
await press(Keys.ArrowUp) // Loop around
assertActiveElement(getByText('Dine in'))
await press(Keys.ArrowUp)
assertActiveElement(getByText('Home delivery'))
expect(changeFn).toHaveBeenCalledTimes(2)
expect(changeFn).toHaveBeenNthCalledWith(1, 'dine-in')
expect(changeFn).toHaveBeenNthCalledWith(2, 'home-delivery')
})
)
})
describe('`ArrowRight` key', () => {
it(
'should go to the next item when pressing the ArrowRight key',
suppressConsoleLogs(async () => {
let changeFn = jest.fn()
render(
<>
<button>Before</button>
<RadioGroup value={undefined} onChange={changeFn}>
<RadioGroup.Label>Pizza Delivery</RadioGroup.Label>
<RadioGroup.Option value="pickup">Pickup</RadioGroup.Option>
<RadioGroup.Option value="home-delivery">Home delivery</RadioGroup.Option>
<RadioGroup.Option value="dine-in">Dine in</RadioGroup.Option>
</RadioGroup>
<button>After</button>
</>
)
// Focus the "Before" button
await press(Keys.Tab)
// Focus the RadioGroup
await press(Keys.Tab)
assertActiveElement(getByText('Pickup'))
await press(Keys.ArrowRight)
assertActiveElement(getByText('Home delivery'))
await press(Keys.ArrowRight)
assertActiveElement(getByText('Dine in'))
await press(Keys.ArrowRight) // Loop around
assertActiveElement(getByText('Pickup'))
expect(changeFn).toHaveBeenCalledTimes(3)
expect(changeFn).toHaveBeenNthCalledWith(1, 'home-delivery')
expect(changeFn).toHaveBeenNthCalledWith(2, 'dine-in')
expect(changeFn).toHaveBeenNthCalledWith(3, 'pickup')
})
)
})
describe('`ArrowDown` key', () => {
it(
'should go to the next item when pressing the ArrowDown key',
suppressConsoleLogs(async () => {
let changeFn = jest.fn()
render(
<>
<button>Before</button>
<RadioGroup value={undefined} onChange={changeFn}>
<RadioGroup.Label>Pizza Delivery</RadioGroup.Label>
<RadioGroup.Option value="pickup">Pickup</RadioGroup.Option>
<RadioGroup.Option value="home-delivery">Home delivery</RadioGroup.Option>
<RadioGroup.Option value="dine-in">Dine in</RadioGroup.Option>
</RadioGroup>
<button>After</button>
</>
)
// Focus the "Before" button
await press(Keys.Tab)
// Focus the RadioGroup
await press(Keys.Tab)
assertActiveElement(getByText('Pickup'))
await press(Keys.ArrowDown)
assertActiveElement(getByText('Home delivery'))
await press(Keys.ArrowDown)
assertActiveElement(getByText('Dine in'))
await press(Keys.ArrowDown) // Loop around
assertActiveElement(getByText('Pickup'))
expect(changeFn).toHaveBeenCalledTimes(3)
expect(changeFn).toHaveBeenNthCalledWith(1, 'home-delivery')
expect(changeFn).toHaveBeenNthCalledWith(2, 'dine-in')
expect(changeFn).toHaveBeenNthCalledWith(3, 'pickup')
})
)
})
describe('`Space` key', () => {
it(
'should select the current option when pressing space',
suppressConsoleLogs(async () => {
let changeFn = jest.fn()
render(
<>
<button>Before</button>
<RadioGroup value={undefined} onChange={changeFn}>
<RadioGroup.Label>Pizza Delivery</RadioGroup.Label>
<RadioGroup.Option value="pickup">Pickup</RadioGroup.Option>
<RadioGroup.Option value="home-delivery">Home delivery</RadioGroup.Option>
<RadioGroup.Option value="dine-in">Dine in</RadioGroup.Option>
</RadioGroup>
<button>After</button>
</>
)
// Focus the "Before" button
await press(Keys.Tab)
// Focus the RadioGroup
await press(Keys.Tab)
assertActiveElement(getByText('Pickup'))
await press(Keys.Space)
assertActiveElement(getByText('Pickup'))
expect(changeFn).toHaveBeenCalledTimes(1)
expect(changeFn).toHaveBeenNthCalledWith(1, 'pickup')
})
)
it(
'should select the current option only once when pressing space',
suppressConsoleLogs(async () => {
let changeFn = jest.fn()
function Example() {
let [value, setValue] = useState(undefined)
return (
<>
<button>Before</button>
<RadioGroup
value={value}
onChange={(v) => {
setValue(v)
changeFn(v)
}}
>
<RadioGroup.Label>Pizza Delivery</RadioGroup.Label>
<RadioGroup.Option value="pickup">Pickup</RadioGroup.Option>
<RadioGroup.Option value="home-delivery">Home delivery</RadioGroup.Option>
<RadioGroup.Option value="dine-in">Dine in</RadioGroup.Option>
</RadioGroup>
<button>After</button>
</>
)
}
render(<Example />)
// Focus the "Before" button
await press(Keys.Tab)
// Focus the RadioGroup
await press(Keys.Tab)
assertActiveElement(getByText('Pickup'))
await press(Keys.Space)
await press(Keys.Space)
await press(Keys.Space)
await press(Keys.Space)
await press(Keys.Space)
assertActiveElement(getByText('Pickup'))
expect(changeFn).toHaveBeenCalledTimes(1)
expect(changeFn).toHaveBeenNthCalledWith(1, 'pickup')
})
)
})
describe('`Enter`', () => {
it(
'should submit the form on `Enter`',
suppressConsoleLogs(async () => {
let submits = jest.fn()
function Example() {
let [value, setValue] = useState('bob')
return (
<form
onSubmit={(event) => {
event.preventDefault()
submits([...new FormData(event.currentTarget).entries()])
}}
>
<RadioGroup value={value} onChange={setValue} name="option">
<RadioGroup.Option value="alice">Alice</RadioGroup.Option>
<RadioGroup.Option value="bob">Bob</RadioGroup.Option>
<RadioGroup.Option value="charlie">Charlie</RadioGroup.Option>
</RadioGroup>
<button>Submit</button>
</form>
)
}
render(<Example />)
// Focus the RadioGroup
await press(Keys.Tab)
// Press enter (which should submit the form)
await press(Keys.Enter)
// Verify the form was submitted
expect(submits).toHaveBeenCalledTimes(1)
expect(submits).toHaveBeenCalledWith([['option', 'bob']])
})
)
})
})
describe('Mouse interactions', () => {
it(
'should be possible to change the current radio group value when clicking on a radio option',
suppressConsoleLogs(async () => {
let changeFn = jest.fn()
render(
<>
<button>Before</button>
<RadioGroup value={undefined} onChange={changeFn}>
<RadioGroup.Label>Pizza Delivery</RadioGroup.Label>
<RadioGroup.Option value="pickup">Pickup</RadioGroup.Option>
<RadioGroup.Option value="home-delivery">Home delivery</RadioGroup.Option>
<RadioGroup.Option value="dine-in">Dine in</RadioGroup.Option>
</RadioGroup>
<button>After</button>
</>
)
await click(getByText('Home delivery'))
assertActiveElement(getByText('Home delivery'))
expect(changeFn).toHaveBeenNthCalledWith(1, 'home-delivery')
})
)
it(
'should be a no-op when clicking on the same item',
suppressConsoleLogs(async () => {
let changeFn = jest.fn()
function Example() {
let [value, setValue] = useState(undefined)
return (
<>
<button>Before</button>
<RadioGroup
value={value}
onChange={(v) => {
setValue(v)
changeFn(v)
}}
>
<RadioGroup.Label>Pizza Delivery</RadioGroup.Label>
<RadioGroup.Option value="pickup">Pickup</RadioGroup.Option>
<RadioGroup.Option value="home-delivery">Home delivery</RadioGroup.Option>
<RadioGroup.Option value="dine-in">Dine in</RadioGroup.Option>
</RadioGroup>
<button>After</button>
</>
)
}
render(<Example />)
await click(getByText('Home delivery'))
await click(getByText('Home delivery'))
await click(getByText('Home delivery'))
await click(getByText('Home delivery'))
assertActiveElement(getByText('Home delivery'))
expect(changeFn).toHaveBeenCalledTimes(1)
})
)
})
describe('Form compatibility', () => {
it(
'should be possible to submit a form with a value',
suppressConsoleLogs(async () => {
let submits = jest.fn()
function Example() {
let [value, setValue] = useState(null)
return (
<form
onSubmit={(event) => {
event.preventDefault()
submits([...new FormData(event.currentTarget).entries()])
}}
>
<RadioGroup value={value} onChange={setValue} name="delivery">
<RadioGroup.Label>Pizza Delivery</RadioGroup.Label>
<RadioGroup.Option value="pickup">Pickup</RadioGroup.Option>
<RadioGroup.Option value="home-delivery">Home delivery</RadioGroup.Option>
<RadioGroup.Option value="dine-in">Dine in</RadioGroup.Option>
</RadioGroup>
<button>Submit</button>
</form>
)
}
render(<Example />)
// Submit the form
await click(getByText('Submit'))
// Verify that the form has been submitted
expect(submits).lastCalledWith([]) // no data
// Choose home delivery
await click(getByText('Home delivery'))
// Submit the form again
await click(getByText('Submit'))
// Verify that the form has been submitted
expect(submits).lastCalledWith([['delivery', 'home-delivery']])
// Choose pickup
await click(getByText('Pickup'))
// Submit the form again
await click(getByText('Submit'))
// Verify that the form has been submitted
expect(submits).lastCalledWith([['delivery', 'pickup']])
})
)
it(
'should be possible to submit a form with a complex value object',
suppressConsoleLogs(async () => {
let submits = jest.fn()
let options = [
{
id: 1,
value: 'pickup',
label: 'Pickup',
extra: { info: 'Some extra info' },
},
{
id: 2,
value: 'home-delivery',
label: 'Home delivery',
extra: { info: 'Some extra info' },
},
{
id: 3,
value: 'dine-in',
label: 'Dine in',
extra: { info: 'Some extra info' },
},
]
function Example() {
let [value, setValue] = useState(options[0])
return (
<form
onSubmit={(event) => {
event.preventDefault()
submits([...new FormData(event.currentTarget).entries()])
}}
>
<RadioGroup value={value} onChange={setValue} name="delivery">
<RadioGroup.Label>Pizza Delivery</RadioGroup.Label>
{options.map((option) => (
<RadioGroup.Option key={option.id} value={option}>
{option.label}
</RadioGroup.Option>
))}
</RadioGroup>
<button>Submit</button>
</form>
)
}
render(<Example />)
// Submit the form
await click(getByText('Submit'))
// Verify that the form has been submitted
expect(submits).lastCalledWith([
['delivery[id]', '1'],
['delivery[value]', 'pickup'],
['delivery[label]', 'Pickup'],
['delivery[extra][info]', 'Some extra info'],
])
// Choose home delivery
await click(getByText('Home delivery'))
// Submit the form again
await click(getByText('Submit'))
// Verify that the form has been submitted
expect(submits).lastCalledWith([
['delivery[id]', '2'],
['delivery[value]', 'home-delivery'],
['delivery[label]', 'Home delivery'],
['delivery[extra][info]', 'Some extra info'],
])
// Choose pickup
await click(getByText('Pickup'))
// Submit the form again
await click(getByText('Submit'))
// Verify that the form has been submitted
expect(submits).lastCalledWith([
['delivery[id]', '1'],
['delivery[value]', 'pickup'],
['delivery[label]', 'Pickup'],
['delivery[extra][info]', 'Some extra info'],
])
})
)
})