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() }) }) describe('Rendering', () => { it('should be possible to render an (on) Switch using a render prop', () => { render( {({ checked }) => {checked ? 'On' : 'Off'}} ) assertSwitch({ state: SwitchState.On, textContent: 'On' }) }) it('should be possible to render an (off) Switch using a render prop', () => { render( {({ checked }) => {checked ? 'On' : 'Off'}} ) assertSwitch({ state: SwitchState.Off, textContent: 'Off' }) }) it('should be possible to render an (on) Switch using an `as` prop', () => { render() assertSwitch({ state: SwitchState.On, tag: 'span' }) }) it('should be possible to render an (off) Switch using an `as` prop', () => { render() assertSwitch({ state: SwitchState.Off, tag: 'span' }) }) it('should be possible to use the switch contents as the label', () => { render( Enable notifications ) assertSwitch({ state: SwitchState.Off, label: 'Enable notifications' }) }) describe('`tabIndex` attribute', () => { it('should have a default tabIndex of `0`', () => { render( Enable notifications ) assertSwitch({ state: SwitchState.Off, label: 'Enable notifications', attributes: { tabindex: '0' }, }) }) it('should be possible to override the `tabIndex`', () => { render( Enable notifications ) assertSwitch({ state: SwitchState.Off, label: 'Enable notifications', attributes: { tabindex: '3' }, }) }) it('should not be possible to override the `tabIndex` to `-1`', () => { render( Enable notifications ) assertSwitch({ state: SwitchState.Off, label: 'Enable notifications', attributes: { tabindex: '0' }, }) }) }) describe('`type` attribute', () => { it('should set the `type` to "button" by default', async () => { render( Trigger ) expect(getSwitch()).toHaveAttribute('type', 'button') }) it('should not set the `type` to "button" if it already contains a `type`', async () => { render( Trigger ) 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((props, ref) => ( ) 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(
{ e.preventDefault() handleSubmission(Object.fromEntries(new FormData(e.target as HTMLFormElement))) }} > ) 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(
{ e.preventDefault() handleSubmission(Object.fromEntries(new FormData(e.target as HTMLFormElement))) }} > ) 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(
{ e.preventDefault() handleSubmission(Object.fromEntries(new FormData(e.target as HTMLFormElement))) }} > ) // 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() // 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( Enable notifications ) assertSwitch({ state: SwitchState.Off, label: 'Enable notifications' }) }) it('should be possible to render a Switch.Group, Switch and Switch.Label (before the Switch)', () => { render( Label B Label A ) // 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( Label A Label B ) // 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( This is an important feature ) 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( This is an important feature ) 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( Label A This is an important feature ) 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 ( { setState(value) handleChange(value) }} /> ) } render() // 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() // 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 (
{ event.preventDefault() submits([...new FormData(event.currentTarget).entries()]) }} > ) } render() // 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 (
{ event.preventDefault() submits([...new FormData(event.currentTarget).entries()]) }} > ) } render() // 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(
) // 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 ( { setState(value) handleChange(value) }} /> ) } render() // 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 ( { setState(value) handleChange(value) }} /> The label ) } render() // 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 ( { setState(value) handleChange(value) }} /> The label ) } render() // 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 ( The label ) } render() // 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 (
Enable notifications
{ event.preventDefault() submits([...new FormData(event.currentTarget).entries()]) }} >
) } render() // 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 (
{ event.preventDefault() submits([...new FormData(event.currentTarget).entries()]) }} > Enable notifications
) } render() // 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 (
{ event.preventDefault() submits([...new FormData(event.currentTarget).entries()]) }} > Apple
) } render() // 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 (
{ event.preventDefault() submits([...new FormData(event.currentTarget).entries()]) }} > Apple
) } render() // Submit the form await click(getByText('Submit')) // Verify that the form has been submitted expect(submits).toHaveBeenLastCalledWith([ ['foo', 'bar'], // The only available field ]) }) })