import { assertActiveElement, assertFocusable, assertNotFocusable, assertRadioGroupLabel, getByText, getRadioGroupOptions, } from "$lib/test-utils/accessibility-assertions"; import { render } from "@testing-library/svelte"; import { RadioGroup, RadioGroupLabel, RadioGroupOption } from "."; import { suppressConsoleLogs } from "$lib/test-utils/suppress-console-logs"; import TestRenderer from "$lib/test-utils/TestRenderer.svelte"; import { click, Keys, press, shift } from "$lib/test-utils/interactions"; import Button from "$lib/internal/elements/Button.svelte"; import ManagedRadioGroup from "./_ManagedRadioGroup.svelte"; import svelte from "svelte-inline-compile"; let mockId = 0; jest.mock('../../hooks/use-id', () => { return { useId: jest.fn(() => ++mockId), } }) beforeEach(() => mockId = 0) 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([['RadioGroupOption', RadioGroupOption]])( 'should error when we are using a <%s /> without a parent ', suppressConsoleLogs((name, Component) => { expect(() => render(Component)).toThrowError( `<${name} /> is missing a parent component.` ) }) ) it( 'should be possible to render a RadioGroup without crashing', suppressConsoleLogs(async () => { render( TestRenderer, { allProps: [ [RadioGroup, { value: undefined, onChange: console.log }, [ [RadioGroupLabel, {}, "Pizza Delivery"], [RadioGroupOption, { value: "pickup" }, "Pickup"], [RadioGroupOption, { value: "home-delivery" }, "Home delivery"], [RadioGroupOption, { value: "dine-in" }, "Dine in"], ]], ] }) assertRadioGroupLabel({ textContent: 'Pizza Delivery' }) }) ) it('should be possible to render a RadioGroup without options and without crashing', () => { render(RadioGroup, { value: undefined }) }) }) describe('Rendering', () => { it('should be possible to render a RadioGroup, where the first element is tabbable (value is undefined)', async () => { render( TestRenderer, { allProps: [ [RadioGroup, { value: undefined, onChange: console.log }, [ [RadioGroupLabel, {}, "Pizza Delivery"], [RadioGroupOption, { value: "pickup" }, "Pickup"], [RadioGroupOption, { value: "home-delivery" }, "Home delivery"], [RadioGroupOption, { value: "dine-in" }, "Dine in"], ]], ] }) 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)', async () => { render( TestRenderer, { allProps: [ [RadioGroup, { value: null, onChange: console.log }, [ [RadioGroupLabel, {}, "Pizza Delivery"], [RadioGroupOption, { value: "pickup" }, "Pickup"], [RadioGroupOption, { value: "home-delivery" }, "Home delivery"], [RadioGroupOption, { value: "dine-in" }, "Dine in"], ]], ] }) 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', async () => { render( TestRenderer, { allProps: [ [RadioGroup, { value: "home-delivery", onChange: console.log }, [ [RadioGroupLabel, {}, "Pizza Delivery"], [RadioGroupOption, { value: "pickup" }, "Pickup"], [RadioGroupOption, { value: "home-delivery" }, "Home delivery"], [RadioGroupOption, { value: "dine-in" }, "Dine in"], ]], ] }) 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', async () => { render(svelte` active = e.detail}> Pizza Delivery {#if showFirst} Pickup {/if} Home delivery Dine in `) 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', async () => { let changeFn = jest.fn() render(svelte` Pizza Delivery Pickup Home delivery Dine in {JSON.stringify({ checked, disabled, active })} `) // Try to click one a few options await click(getByText('Pickup')) await click(getByText('Dine in')) // Verify that the RadioGroupOption gets the disabled state expect(document.querySelector('[data-value="slot-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 RadioGroupOption gets the disabled state expect(document.querySelector('[data-value="slot-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 RadioGroupOption', async () => { let changeFn = jest.fn() render(svelte` Pizza Delivery Pickup Home delivery Dine in {JSON.stringify({ checked, disabled, active })} `) // Try to click the disabled option await click(document.querySelector('[data-value="slot-prop"]')) // Verify that the RadioGroupOption gets the disabled state expect(document.querySelector('[data-value="slot-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 "slot-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 RadioGroupOption gets the disabled state expect(document.querySelector('[data-value="slot-prop"]')).toHaveTextContent( JSON.stringify({ checked: false, disabled: false, active: false, }) ) // Try to click one a few options await click(document.querySelector('[data-value="slot-prop"]')) // Make sure that the onChange handler got called expect(changeFn).toHaveBeenCalledTimes(1) }) }) describe('Keyboard interactions', () => { describe('`Tab` key', () => { it('should be possible to tab to the first item', async () => { render( TestRenderer, { allProps: [ [RadioGroup, { value: undefined, onChange: console.log }, [ [RadioGroupLabel, {}, "Pizza Delivery"], [RadioGroupOption, { value: "pickup" }, "Pickup"], [RadioGroupOption, { value: "home-delivery" }, "Home delivery"], [RadioGroupOption, { value: "dine-in" }, "Dine in"], ]], ] }) await press(Keys.Tab) assertActiveElement(getByText('Pickup')) }) it('should not change the selected element on focus', async () => { let changeFn = jest.fn() render( TestRenderer, { allProps: [ [RadioGroup, { value: undefined, onChange: changeFn }, [ [RadioGroupLabel, {}, "Pizza Delivery"], [RadioGroupOption, { value: "pickup" }, "Pickup"], [RadioGroupOption, { value: "home-delivery" }, "Home delivery"], [RadioGroupOption, { value: "dine-in" }, "Dine in"], ]], ] }) await press(Keys.Tab) assertActiveElement(getByText('Pickup')) expect(changeFn).toHaveBeenCalledTimes(0) }) it('should be possible to tab to the active item', async () => { render( TestRenderer, { allProps: [ [RadioGroup, { value: "home-delivery", onChange: console.log }, [ [RadioGroupLabel, {}, "Pizza Delivery"], [RadioGroupOption, { value: "pickup" }, "Pickup"], [RadioGroupOption, { value: "home-delivery" }, "Home delivery"], [RadioGroupOption, { value: "dine-in" }, "Dine in"], ]], ] }) await press(Keys.Tab) assertActiveElement(getByText('Home delivery')) }) it('should not change the selected element on focus (when selecting the active item)', async () => { let changeFn = jest.fn() render( TestRenderer, { allProps: [ [RadioGroup, { value: "home-delivery", onChange: changeFn }, [ [RadioGroupLabel, {}, "Pizza Delivery"], [RadioGroupOption, { value: "pickup" }, "Pickup"], [RadioGroupOption, { value: "home-delivery" }, "Home delivery"], [RadioGroupOption, { value: "dine-in" }, "Dine in"], ]], ] }) 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)', async () => { render( TestRenderer, { allProps: [ [Button, {}, "Before"], [RadioGroup, { value: undefined, onChange: console.log }, [ [RadioGroupLabel, {}, "Pizza Delivery"], [RadioGroupOption, { value: "pickup" }, "Pickup"], [RadioGroupOption, { value: "home-delivery" }, "Home delivery"], [RadioGroupOption, { value: "dine-in" }, "Dine in"], ]], [Button, {}, "After"], ] }) 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)', async () => { render( TestRenderer, { allProps: [ [Button, {}, "Before"], [RadioGroup, { value: "home-delivery", onChange: console.log }, [ [RadioGroupLabel, {}, "Pizza Delivery"], [RadioGroupOption, { value: "pickup" }, "Pickup"], [RadioGroupOption, { value: "home-delivery" }, "Home delivery"], [RadioGroupOption, { value: "dine-in" }, "Dine in"], ]], [Button, {}, "After"], ] }) 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', async () => { render( TestRenderer, { allProps: [ [RadioGroup, { value: undefined, onChange: console.log }, [ [RadioGroupLabel, {}, "Pizza Delivery"], [RadioGroupOption, { value: "pickup" }, "Pickup"], [RadioGroupOption, { value: "home-delivery" }, "Home delivery"], [RadioGroupOption, { value: "dine-in" }, "Dine in"], ]], [Button, {}, "After"], ] }) getByText('After')?.focus() await press(shift(Keys.Tab)) assertActiveElement(getByText('Pickup')) }) it('should not change the selected element on focus', async () => { let changeFn = jest.fn() render( TestRenderer, { allProps: [ [RadioGroup, { value: undefined, onChange: changeFn }, [ [RadioGroupLabel, {}, "Pizza Delivery"], [RadioGroupOption, { value: "pickup" }, "Pickup"], [RadioGroupOption, { value: "home-delivery" }, "Home delivery"], [RadioGroupOption, { value: "dine-in" }, "Dine in"], ]], [Button, {}, "After"], ] }) getByText('After')?.focus() await press(shift(Keys.Tab)) assertActiveElement(getByText('Pickup')) expect(changeFn).toHaveBeenCalledTimes(0) }) it('should be possible to tab to the active item', async () => { render( TestRenderer, { allProps: [ [RadioGroup, { value: "home-delivery", onChange: console.log }, [ [RadioGroupLabel, {}, "Pizza Delivery"], [RadioGroupOption, { value: "pickup" }, "Pickup"], [RadioGroupOption, { value: "home-delivery" }, "Home delivery"], [RadioGroupOption, { value: "dine-in" }, "Dine in"], ]], [Button, {}, "After"], ] }) getByText('After')?.focus() await press(shift(Keys.Tab)) assertActiveElement(getByText('Home delivery')) }) it('should not change the selected element on focus (when selecting the active item)', async () => { let changeFn = jest.fn() render( TestRenderer, { allProps: [ [RadioGroup, { value: "home-delivery", onChange: changeFn }, [ [RadioGroupLabel, {}, "Pizza Delivery"], [RadioGroupOption, { value: "pickup" }, "Pickup"], [RadioGroupOption, { value: "home-delivery" }, "Home delivery"], [RadioGroupOption, { value: "dine-in" }, "Dine in"], ]], [Button, {}, "After"] ] }) getByText('After')?.focus() 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)', async () => { render( TestRenderer, { allProps: [ [Button, {}, "Before"], [RadioGroup, { value: undefined, onChange: console.log }, [ [RadioGroupLabel, {}, "Pizza Delivery"], [RadioGroupOption, { value: "pickup" }, "Pickup"], [RadioGroupOption, { value: "home-delivery" }, "Home delivery"], [RadioGroupOption, { value: "dine-in" }, "Dine in"], ]], [Button, {}, "After"], ] }) getByText('After')?.focus() 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)', async () => { render( TestRenderer, { allProps: [ [Button, {}, "Before"], [RadioGroup, { value: "home-delivery", onChange: console.log }, [ [RadioGroupLabel, {}, "Pizza Delivery"], [RadioGroupOption, { value: "pickup" }, "Pickup"], [RadioGroupOption, { value: "home-delivery" }, "Home delivery"], [RadioGroupOption, { value: "dine-in" }, "Dine in"], ]], [Button, {}, "After"], ] }) getByText('After')?.focus() 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', async () => { let changeFn = jest.fn() render( TestRenderer, { allProps: [ [Button, {}, "Before"], [RadioGroup, { value: undefined, onChange: (e: CustomEvent) => changeFn(e.detail) }, [ [RadioGroupLabel, {}, "Pizza Delivery"], [RadioGroupOption, { value: "pickup" }, "Pickup"], [RadioGroupOption, { value: "home-delivery" }, "Home delivery"], [RadioGroupOption, { value: "dine-in" }, "Dine in"], ]], [Button, {}, "After"], ] }) // 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', async () => { let changeFn = jest.fn() render( TestRenderer, { allProps: [ [Button, {}, "Before"], [RadioGroup, { value: undefined, onChange: (e: CustomEvent) => changeFn(e.detail) }, [ [RadioGroupLabel, {}, "Pizza Delivery"], [RadioGroupOption, { value: "pickup" }, "Pickup"], [RadioGroupOption, { value: "home-delivery" }, "Home delivery"], [RadioGroupOption, { value: "dine-in" }, "Dine in"], ]], [Button, {}, "After"], ] }) // 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', async () => { let changeFn = jest.fn() render( TestRenderer, { allProps: [ [Button, {}, "Before"], [RadioGroup, { value: undefined, onChange: (e: CustomEvent) => changeFn(e.detail) }, [ [RadioGroupLabel, {}, "Pizza Delivery"], [RadioGroupOption, { value: "pickup" }, "Pickup"], [RadioGroupOption, { value: "home-delivery" }, "Home delivery"], [RadioGroupOption, { value: "dine-in" }, "Dine in"], ]], [Button, {}, "After"], ] }) // 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', async () => { let changeFn = jest.fn() render( TestRenderer, { allProps: [ [Button, {}, "Before"], [RadioGroup, { value: undefined, onChange: (e: CustomEvent) => changeFn(e.detail) }, [ [RadioGroupLabel, {}, "Pizza Delivery"], [RadioGroupOption, { value: "pickup" }, "Pickup"], [RadioGroupOption, { value: "home-delivery" }, "Home delivery"], [RadioGroupOption, { value: "dine-in" }, "Dine in"], ]], [Button, {}, "After"], ] }) // 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', async () => { let changeFn = jest.fn() render( TestRenderer, { allProps: [ [Button, {}, "Before"], [RadioGroup, { value: undefined, onChange: (e: CustomEvent) => changeFn(e.detail) }, [ [RadioGroupLabel, {}, "Pizza Delivery"], [RadioGroupOption, { value: "pickup" }, "Pickup"], [RadioGroupOption, { value: "home-delivery" }, "Home delivery"], [RadioGroupOption, { value: "dine-in" }, "Dine in"], ]], [Button, {}, "After"], ] }) // 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', async () => { let changeFn = jest.fn() render( TestRenderer, { allProps: [ [Button, {}, "Before"], [ManagedRadioGroup, { value: undefined, onChange: (e: CustomEvent) => changeFn(e.detail) }, [ [RadioGroupLabel, {}, "Pizza Delivery"], [RadioGroupOption, { value: "pickup" }, "Pickup"], [RadioGroupOption, { value: "home-delivery" }, "Home delivery"], [RadioGroupOption, { value: "dine-in" }, "Dine in"], ]], [Button, {}, "After"], ] }) // 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('Mouse interactions', () => { it('should be possible to change the current radio group value when clicking on a radio option', async () => { let changeFn = jest.fn() render( TestRenderer, { allProps: [ [Button, {}, "Before"], [RadioGroup, { value: undefined, onChange: (e: CustomEvent) => changeFn(e.detail) }, [ [RadioGroupLabel, {}, "Pizza Delivery"], [RadioGroupOption, { value: "pickup" }, "Pickup"], [RadioGroupOption, { value: "home-delivery" }, "Home delivery"], [RadioGroupOption, { value: "dine-in" }, "Dine in"], ]], [Button, {}, "After"], ] }) 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', async () => { let changeFn = jest.fn() render( TestRenderer, { allProps: [ [Button, {}, "Before"], [ManagedRadioGroup, { value: undefined, onChange: (e: CustomEvent) => changeFn(e.detail) }, [ [RadioGroupLabel, {}, "Pizza Delivery"], [RadioGroupOption, { value: "pickup" }, "Pickup"], [RadioGroupOption, { value: "home-delivery" }, "Home delivery"], [RadioGroupOption, { value: "dine-in" }, "Dine in"], ]], [Button, {}, "After"], ] }) 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) }) })