radix-ui / primitives

Radix Primitives is an open-source UI component library for building high-quality, accessible design systems and web apps. Maintained by @workos.
https://radix-ui.com/primitives
MIT License
15.93k stars 833 forks source link

Radio Group: userEvent.keyboard([ArrowDown]) doesn't check the next radio item in unit tests #3076

Open bakerj417 opened 3 months ago

bakerj417 commented 3 months ago

Bug report

It looks like when testing accessibility in a jest unit test using the Radix Primitive Radio Group by firing userEvents from the @testing-library/user-event package. The test results vary from real live interactions with the radio group. As you can see from the test example below when using RadioGroup and using the arrow keys the test fail toBeChecked. However, the same test with an html radio input passes that same check.

Current Behavior

This test will fail on the last expect because the second option isn't checked marked as checked nor is onValueChange being called:

 test("arrow keys selects option and moves focus", async () => {
        render(
                <RadioGroupPrimitive.Root
                    className="RadioGroupRoot"
                    defaultValue="default"
                    aria-label="View density"
                >
                    <div style={{ display: "flex", alignItems: "center" }}>
                        <RadioGroupPrimitive.Item
                            className="RadioGroupItem"
                            value="default"
                            id="r1"
                        >
                            <RadioGroupPrimitive.Indicator className="RadioGroupIndicator" />
                        </RadioGroupPrimitive.Item>
                        <label className="Label" htmlFor="r1">
                            Default
                        </label>
                    </div>
                    <div style={{ display: "flex", alignItems: "center" }}>
                        <RadioGroupPrimitive.Item
                            className="RadioGroupItem"
                            value="comfortable"
                            id="r2"
                        >
                            <RadioGroupPrimitive.Indicator className="RadioGroupIndicator" />
                        </RadioGroupPrimitive.Item>
                        <label className="Label" htmlFor="r2">
                            Comfortable
                        </label>
                    </div>
                    <div style={{ display: "flex", alignItems: "center" }}>
                        <RadioGroupPrimitive.Item
                            className="RadioGroupItem"
                            value="compact"
                            id="r3"
                        >
                            <RadioGroupPrimitive.Indicator className="RadioGroupIndicator" />
                        </RadioGroupPrimitive.Item>
                        <label className="Label" htmlFor="r3">
                            Compact
                        </label>
                    </div>
                </RadioGroupPrimitive.Root>
        );
        await userEvent.tab();
        // tabbing focus to the first radio item
        const radioItem = screen.getByLabelText("Default");
        expect(radioItem).toHaveFocus();
        // second options is aria-checked false and not focused
        const radioItem2 = screen.getByLabelText("Comfortable");
        expect(radioItem2).not.toHaveFocus();
        expect(radioItem2).not.toBeChecked();
        // hitting down arrow selects the second option and moves focus
        await userEvent.keyboard("[ArrowDown]");
        expect(radioItem2).toHaveFocus();
        expect(radioItem2).toBeChecked();
    });

test output:

Received element is not checked:
      <button aria-checked="false" class="RadioGroupItem" data-radix-collection-item="" data-state="unchecked" id="r2" role="radio" tabindex="0" type="button" value="comfortable" />

Expected behavior

I would expect this test of accessibility to pass when using radix primitive radio group. I ran the same test with a normal html input radio and tests pass so it seems to point to an issue with Radix Primitive Radio Group and not jest or userEvent:

test("arrow keys selects options and moves focus", async () => {
        render(
            <div>
                <input
                    type="radio"
                    id="html"
                    data-testid="radio1"
                    name="fav_language"
                    value="HTML"
                />
                <label htmlFor="html">HTML</label>
                <br />
                <input
                    type="radio"
                    id="css"
                    data-testid="radio2"
                    name="fav_language"
                    value="CSS"
                />
                <label htmlFor="css">CSS</label>
                <br />
                <input
                    type="radio"
                    id="javascript"
                    data-testid="radio3"
                    name="fav_language"
                    value="JavaScript"
                />
                <label htmlFor="javascript">JavaScript</label>
            </div>
        );
        await userEvent.tab();
        const radioItem = screen.getByTestId("radio1");
        expect(radioItem).toHaveFocus();
        const radioItem2 = screen.getByTestId("radio2");
        expect(radioItem2).not.toHaveFocus();
        expect(radioItem2).not.toBeChecked();
        await userEvent.keyboard("[ArrowDown]");
        expect(radioItem2).toHaveFocus();
        expect(radioItem2).toBeChecked();
});

Reproducible example

CodeSandbox Devbox Example

This example should open and stat a Run Tests tab in the terminal where you can see the failing test using Radix as well as the passing test using normal radio inputs. If that tab doesn't open you can go to the terminal tab on the left side and click Run tests under the tasks section.

Your environment

Dev Box given by CodeSandbox