Files
headlessui/packages/@headlessui-react/src/components/switch/switch.test.tsx
T
Robin Malfait ae8c253c21 Fix typos (#3086)
* fix a bunch of typos

* fix typos in `@headlessui/vue`
2024-04-08 23:31:50 +02:00

847 lines
23 KiB
TypeScript

import { render } from '@testing-library/react'
import React, { useState } from 'react'
import {
assertActiveElement,
assertSwitch,
getByText,
getSwitch,
getSwitchLabel,
SwitchState,
} from '../../test-utils/accessibility-assertions'
import { click, focus, Keys, mouseEnter, press } from '../../test-utils/interactions'
import { Switch } from './switch'
jest.mock('../../hooks/use-id')
describe('Safe guards', () => {
it('should be possible to render a Switch without crashing', () => {
render(<Switch checked={false} onChange={console.log} />)
})
})
describe('Rendering', () => {
it('should be possible to render an (on) Switch using a render prop', () => {
render(
<Switch checked={true} onChange={console.log}>
{({ checked }) => <span>{checked ? 'On' : 'Off'}</span>}
</Switch>
)
assertSwitch({ state: SwitchState.On, textContent: 'On' })
})
it('should be possible to render an (off) Switch using a render prop', () => {
render(
<Switch checked={false} onChange={console.log}>
{({ checked }) => <span>{checked ? 'On' : 'Off'}</span>}
</Switch>
)
assertSwitch({ state: SwitchState.Off, textContent: 'Off' })
})
it('should be possible to render an (on) Switch using an `as` prop', () => {
render(<Switch as="span" checked={true} onChange={console.log} />)
assertSwitch({ state: SwitchState.On, tag: 'span' })
})
it('should be possible to render an (off) Switch using an `as` prop', () => {
render(<Switch as="span" checked={false} onChange={console.log} />)
assertSwitch({ state: SwitchState.Off, tag: 'span' })
})
it('should be possible to use the switch contents as the label', () => {
render(
<Switch checked={false} onChange={console.log}>
<span>Enable notifications</span>
</Switch>
)
assertSwitch({ state: SwitchState.Off, label: 'Enable notifications' })
})
describe('`tabIndex` attribute', () => {
it('should have a default tabIndex of `0`', () => {
render(
<Switch checked={false} onChange={console.log}>
<span>Enable notifications</span>
</Switch>
)
assertSwitch({
state: SwitchState.Off,
label: 'Enable notifications',
attributes: { tabindex: '0' },
})
})
it('should be possible to override the `tabIndex`', () => {
render(
<Switch checked={false} onChange={console.log} tabIndex={3}>
<span>Enable notifications</span>
</Switch>
)
assertSwitch({
state: SwitchState.Off,
label: 'Enable notifications',
attributes: { tabindex: '3' },
})
})
it('should not be possible to override the `tabIndex` to `-1`', () => {
render(
<Switch checked={false} onChange={console.log} tabIndex={-1}>
<span>Enable notifications</span>
</Switch>
)
assertSwitch({
state: SwitchState.Off,
label: 'Enable notifications',
attributes: { tabindex: '0' },
})
})
})
describe('`type` attribute', () => {
it('should set the `type` to "button" by default', async () => {
render(
<Switch checked={false} onChange={console.log}>
Trigger
</Switch>
)
expect(getSwitch()).toHaveAttribute('type', 'button')
})
it('should not set the `type` to "button" if it already contains a `type`', async () => {
render(
<Switch checked={false} onChange={console.log} type="submit">
Trigger
</Switch>
)
expect(getSwitch()).toHaveAttribute('type', 'submit')
})
it('should set the `type` to "button" when using the `as` prop which resolves to a "button"', async () => {
let CustomButton = React.forwardRef<HTMLButtonElement>((props, ref) => (
<button ref={ref} {...props} />
))
render(
<Switch checked={false} onChange={console.log} as={CustomButton}>
Trigger
</Switch>
)
expect(getSwitch()).toHaveAttribute('type', 'button')
})
it('should not set the type if the "as" prop is not a "button"', async () => {
render(
<Switch checked={false} onChange={console.log} as="div">
Trigger
</Switch>
)
expect(getSwitch()).not.toHaveAttribute('type')
})
it('should not set the `type` to "button" when using the `as` prop which resolves to a "div"', async () => {
let CustomButton = React.forwardRef<HTMLDivElement>((props, ref) => (
<div ref={ref} {...props} />
))
render(
<Switch checked={false} onChange={console.log} as={CustomButton}>
Trigger
</Switch>
)
expect(getSwitch()).not.toHaveAttribute('type')
})
})
describe('Uncontrolled', () => {
it('should be possible to use in an uncontrolled way', async () => {
let handleSubmission = jest.fn()
render(
<form
onSubmit={(e) => {
e.preventDefault()
handleSubmission(Object.fromEntries(new FormData(e.target as HTMLFormElement)))
}}
>
<Switch name="notifications" />
<button id="submit">submit</button>
</form>
)
await click(document.getElementById('submit'))
// No values
expect(handleSubmission).toHaveBeenLastCalledWith({})
// Toggle
await click(getSwitch())
// Submit
await click(document.getElementById('submit'))
// Notifications should be on
expect(handleSubmission).toHaveBeenLastCalledWith({ notifications: 'on' })
// Toggle
await click(getSwitch())
// Submit
await click(document.getElementById('submit'))
// Notifications should be off (or in this case, non-existent)
expect(handleSubmission).toHaveBeenLastCalledWith({})
})
it('should be possible to use in an uncontrolled way with a value', async () => {
let handleSubmission = jest.fn()
render(
<form
onSubmit={(e) => {
e.preventDefault()
handleSubmission(Object.fromEntries(new FormData(e.target as HTMLFormElement)))
}}
>
<Switch name="notifications" value="enabled" />
<button id="submit">submit</button>
</form>
)
await click(document.getElementById('submit'))
// No values
expect(handleSubmission).toHaveBeenLastCalledWith({})
// Toggle
await click(getSwitch())
// Submit
await click(document.getElementById('submit'))
// Notifications should be on
expect(handleSubmission).toHaveBeenLastCalledWith({ notifications: 'enabled' })
// Toggle
await click(getSwitch())
// Submit
await click(document.getElementById('submit'))
// Notifications should be off (or in this case, non-existent)
expect(handleSubmission).toHaveBeenLastCalledWith({})
})
it('should be possible to provide a default value', async () => {
let handleSubmission = jest.fn()
render(
<form
onSubmit={(e) => {
e.preventDefault()
handleSubmission(Object.fromEntries(new FormData(e.target as HTMLFormElement)))
}}
>
<Switch name="notifications" defaultChecked />
<button id="submit">submit</button>
</form>
)
await click(document.getElementById('submit'))
// Notifications should be on by default
expect(handleSubmission).toHaveBeenLastCalledWith({ notifications: 'on' })
// Toggle
await click(getSwitch())
// Submit
await click(document.getElementById('submit'))
// Notifications should be off (or in this case non-existent)
expect(handleSubmission).toHaveBeenLastCalledWith({})
})
it('should be possible to reset to the default value if the form is reset', async () => {
let handleSubmission = jest.fn()
render(
<form
onSubmit={(e) => {
e.preventDefault()
handleSubmission(Object.fromEntries(new FormData(e.target as HTMLFormElement)))
}}
>
<Switch name="assignee" value="bob" defaultChecked />
<button id="submit">submit</button>
<button type="reset" id="reset">
reset
</button>
</form>
)
// Bob is the defaultValue
await click(document.getElementById('submit'))
expect(handleSubmission).toHaveBeenLastCalledWith({ assignee: 'bob' })
// Toggle the switch
await click(getSwitch())
// Bob should not be active anymore
await click(document.getElementById('submit'))
expect(handleSubmission).toHaveBeenLastCalledWith({})
// Reset
await click(document.getElementById('reset'))
// Bob should be submitted again
await click(document.getElementById('submit'))
expect(handleSubmission).toHaveBeenLastCalledWith({ assignee: 'bob' })
})
it('should still call the onChange listeners when choosing new values', async () => {
let handleChange = jest.fn()
render(<Switch name="notifications" onChange={handleChange} />)
// Toggle
await click(getSwitch())
// Toggle
await click(getSwitch())
// Toggle
await click(getSwitch())
// Change handler should have been called 3 times
expect(handleChange).toHaveBeenNthCalledWith(1, true)
expect(handleChange).toHaveBeenNthCalledWith(2, false)
expect(handleChange).toHaveBeenNthCalledWith(3, true)
})
})
})
describe('Render composition', () => {
it('should be possible to render a Switch.Group, Switch and Switch.Label', () => {
render(
<Switch.Group>
<Switch checked={false} onChange={console.log} />
<Switch.Label>Enable notifications</Switch.Label>
</Switch.Group>
)
assertSwitch({ state: SwitchState.Off, label: 'Enable notifications' })
})
it('should be possible to render a Switch.Group, Switch and Switch.Label (before the Switch)', () => {
render(
<Switch.Group>
<Switch.Label>Label B</Switch.Label>
<Switch checked={false} onChange={console.log}>
Label A
</Switch>
</Switch.Group>
)
// Warning! Using aria-label or aria-labelledby will hide any descendant content from assistive
// technologies.
//
// Thus: Label A should not be part of the "label" in this case
assertSwitch({ state: SwitchState.Off, label: 'Label B' })
})
it('should be possible to render a Switch.Group, Switch and Switch.Label (after the Switch)', () => {
render(
<Switch.Group>
<Switch checked={false} onChange={console.log}>
Label A
</Switch>
<Switch.Label>Label B</Switch.Label>
</Switch.Group>
)
// Warning! Using aria-label or aria-labelledby will hide any descendant content from assistive
// technologies.
//
// Thus: Label A should not be part of the "label" in this case
assertSwitch({ state: SwitchState.Off, label: 'Label B' })
})
it('should be possible to render a Switch.Group, Switch and Switch.Description (before the Switch)', async () => {
render(
<Switch.Group>
<Switch.Description>This is an important feature</Switch.Description>
<Switch checked={false} onChange={console.log} />
</Switch.Group>
)
assertSwitch({ state: SwitchState.Off, description: 'This is an important feature' })
})
it('should be possible to render a Switch.Group, Switch and Switch.Description (after the Switch)', () => {
render(
<Switch.Group>
<Switch checked={false} onChange={console.log} />
<Switch.Description>This is an important feature</Switch.Description>
</Switch.Group>
)
assertSwitch({ state: SwitchState.Off, description: 'This is an important feature' })
})
it('should be possible to render a Switch.Group, Switch, Switch.Label and Switch.Description', () => {
render(
<Switch.Group>
<Switch.Label>Label A</Switch.Label>
<Switch checked={false} onChange={console.log} />
<Switch.Description>This is an important feature</Switch.Description>
</Switch.Group>
)
assertSwitch({
state: SwitchState.Off,
label: 'Label A',
description: 'This is an important feature',
})
})
})
describe('Keyboard interactions', () => {
describe('`Space` key', () => {
it('should be possible to toggle the Switch with Space', async () => {
let handleChange = jest.fn()
function Example() {
let [state, setState] = useState(false)
return (
<Switch
checked={state}
onChange={(value) => {
setState(value)
handleChange(value)
}}
/>
)
}
render(<Example />)
// Ensure checkbox is off
assertSwitch({ state: SwitchState.Off })
// Focus the switch
await focus(getSwitch())
// Toggle
await press(Keys.Space)
// Ensure state is on
assertSwitch({ state: SwitchState.On })
// Toggle
await press(Keys.Space)
// Ensure state is off
assertSwitch({ state: SwitchState.Off })
})
})
describe('`Enter` key', () => {
it('should not be possible to use Enter to toggle the Switch', async () => {
let handleChange = jest.fn()
render(<Switch checked={false} onChange={handleChange} />)
// Ensure checkbox is off
assertSwitch({ state: SwitchState.Off })
// Focus the switch
await focus(getSwitch())
// Try to toggle
await press(Keys.Enter)
expect(handleChange).not.toHaveBeenCalled()
})
it('should submit the form on `Enter`', async () => {
let submits = jest.fn()
function Example() {
let [value, setValue] = useState(true)
return (
<form
onSubmit={(event) => {
event.preventDefault()
submits([...new FormData(event.currentTarget).entries()])
}}
>
<Switch checked={value} onChange={setValue} name="option" />
<button>Submit</button>
</form>
)
}
render(<Example />)
// Focus the input field
await focus(getSwitch())
assertActiveElement(getSwitch())
// Press enter (which should submit the form)
await press(Keys.Enter)
// Verify the form was submitted
expect(submits).toHaveBeenCalledTimes(1)
expect(submits).toHaveBeenCalledWith([['option', 'on']])
})
it('should submit the form on `Enter` (when no submit button was found)', async () => {
let submits = jest.fn()
function Example() {
let [value, setValue] = useState(true)
return (
<form
onSubmit={(event) => {
event.preventDefault()
submits([...new FormData(event.currentTarget).entries()])
}}
>
<Switch checked={value} onChange={setValue} name="option" />
</form>
)
}
render(<Example />)
// Focus the input field
await focus(getSwitch())
assertActiveElement(getSwitch())
// Press enter (which should submit the form)
await press(Keys.Enter)
// Verify the form was submitted
expect(submits).toHaveBeenCalledTimes(1)
expect(submits).toHaveBeenCalledWith([['option', 'on']])
})
})
describe('`Tab` key', () => {
it('should be possible to tab away from the Switch', async () => {
render(
<div>
<Switch checked={false} onChange={console.log} />
<button id="btn">Other element</button>
</div>
)
// Ensure checkbox is off
assertSwitch({ state: SwitchState.Off })
// Focus the switch
await focus(getSwitch())
// Expect the switch to be active
assertActiveElement(getSwitch())
// Toggle
await press(Keys.Tab)
// Expect the button to be active
assertActiveElement(document.getElementById('btn'))
})
})
})
describe('Mouse interactions', () => {
it('should be possible to toggle the Switch with a click', async () => {
let handleChange = jest.fn()
function Example() {
let [state, setState] = useState(false)
return (
<Switch
checked={state}
onChange={(value) => {
setState(value)
handleChange(value)
}}
/>
)
}
render(<Example />)
// Ensure checkbox is off
assertSwitch({ state: SwitchState.Off })
// Toggle
await click(getSwitch())
// Ensure state is on
assertSwitch({ state: SwitchState.On })
// Toggle
await click(getSwitch())
// Ensure state is off
assertSwitch({ state: SwitchState.Off })
})
it('should be possible to toggle the Switch with a click on the Label', async () => {
let handleChange = jest.fn()
function Example() {
let [state, setState] = useState(false)
return (
<Switch.Group>
<Switch
checked={state}
onChange={(value) => {
setState(value)
handleChange(value)
}}
/>
<Switch.Label>The label</Switch.Label>
</Switch.Group>
)
}
render(<Example />)
// Ensure checkbox is off
assertSwitch({ state: SwitchState.Off })
// Toggle
await click(getSwitchLabel())
// Ensure the switch is focused
assertActiveElement(getSwitch())
// Ensure state is on
assertSwitch({ state: SwitchState.On })
// Toggle
await click(getSwitchLabel())
// Ensure the switch is focused
assertActiveElement(getSwitch())
// Ensure state is off
assertSwitch({ state: SwitchState.Off })
})
it('should not be possible to toggle the Switch with a click on the Label (passive)', async () => {
let handleChange = jest.fn()
function Example() {
let [state, setState] = useState(false)
return (
<Switch.Group>
<Switch
checked={state}
onChange={(value) => {
setState(value)
handleChange(value)
}}
/>
<Switch.Label passive>The label</Switch.Label>
</Switch.Group>
)
}
render(<Example />)
// Ensure checkbox is off
assertSwitch({ state: SwitchState.Off })
// Toggle
await click(getSwitchLabel())
// Ensure state is still off
assertSwitch({ state: SwitchState.Off })
})
xit('should be possible to hover the label and trigger a hover on the switch', async () => {
// This test doesn't work in JSDOM :(
// Keeping it here for reference when we can test this in a real browser
function Example() {
let [state] = useState(false)
return (
<Switch.Group>
<style>{`.bg{background-color:rgba(0,255,0)}.bg-on-hover:hover{background-color:rgba(255,0,0)}`}</style>
<Switch checked={state} className="bg bg-on-hover" />
<Switch.Label>The label</Switch.Label>
</Switch.Group>
)
}
render(<Example />)
// Verify the switch is not hovered
expect(window.getComputedStyle(getSwitch()!).backgroundColor).toBe('rgb(0, 255, 0)')
// Hover over the *label*
await mouseEnter(getSwitchLabel())
// Make sure the switch gets hover styles
expect(window.getComputedStyle(getSwitch()!).backgroundColor).toBe('rgb(255, 0, 0)')
})
})
describe('Form compatibility', () => {
it('should be possible to set the `form`, which is forwarded to the hidden inputs', async () => {
let submits = jest.fn()
function Example() {
let [state, setState] = useState(false)
return (
<div>
<Switch.Group>
<Switch form="my-form" checked={state} onChange={setState} name="notifications" />
<Switch.Label>Enable notifications</Switch.Label>
</Switch.Group>
<form
id="my-form"
onSubmit={(event) => {
event.preventDefault()
submits([...new FormData(event.currentTarget).entries()])
}}
>
<button>Submit</button>
</form>
</div>
)
}
render(<Example />)
// Toggle
await click(getSwitchLabel())
// Submit the form again
await click(getByText('Submit'))
// Verify that the form has been submitted
expect(submits).toHaveBeenLastCalledWith([['notifications', 'on']])
})
it('should be possible to submit a form with an boolean value', async () => {
let submits = jest.fn()
function Example() {
let [state, setState] = useState(false)
return (
<form
onSubmit={(event) => {
event.preventDefault()
submits([...new FormData(event.currentTarget).entries()])
}}
>
<Switch.Group>
<Switch checked={state} onChange={setState} name="notifications" />
<Switch.Label>Enable notifications</Switch.Label>
</Switch.Group>
<button>Submit</button>
</form>
)
}
render(<Example />)
// Submit the form
await click(getByText('Submit'))
// Verify that the form has been submitted
expect(submits).toHaveBeenLastCalledWith([]) // no data
// Toggle
await click(getSwitchLabel())
// Submit the form again
await click(getByText('Submit'))
// Verify that the form has been submitted
expect(submits).toHaveBeenLastCalledWith([['notifications', 'on']])
})
it('should be possible to submit a form with a provided string value', async () => {
let submits = jest.fn()
function Example() {
let [state, setState] = useState(false)
return (
<form
onSubmit={(event) => {
event.preventDefault()
submits([...new FormData(event.currentTarget).entries()])
}}
>
<Switch.Group>
<Switch checked={state} onChange={setState} name="fruit" value="apple" />
<Switch.Label>Apple</Switch.Label>
</Switch.Group>
<button>Submit</button>
</form>
)
}
render(<Example />)
// Submit the form
await click(getByText('Submit'))
// Verify that the form has been submitted
expect(submits).toHaveBeenLastCalledWith([]) // no data
// Toggle
await click(getSwitchLabel())
// Submit the form again
await click(getByText('Submit'))
// Verify that the form has been submitted
expect(submits).toHaveBeenLastCalledWith([['fruit', 'apple']])
})
it('should not submit the data if the Switch is disabled', async () => {
let submits = jest.fn()
function Example() {
let [state, setState] = useState(true)
return (
<form
onSubmit={(event) => {
event.preventDefault()
submits([...new FormData(event.currentTarget).entries()])
}}
>
<input type="hidden" name="foo" value="bar" />
<Switch.Group>
<Switch checked={state} onChange={setState} name="fruit" value="apple" disabled />
<Switch.Label>Apple</Switch.Label>
</Switch.Group>
<button>Submit</button>
</form>
)
}
render(<Example />)
// Submit the form
await click(getByText('Submit'))
// Verify that the form has been submitted
expect(submits).toHaveBeenLastCalledWith([
['foo', 'bar'], // The only available field
])
})
})