import { render } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { describe, expect, it, mock } from "bun:test"; import type { ReactElement, ReactNode } from "react"; import * as React from "react"; import { RadioGroupDescription, RadioGroupErrorMessage, RadioGroupItem, RadioGroupItemControl, RadioGroupItemHiddenInput, RadioGroupLabel, RadioGroupRoot, type RadioGroupItemProps, type RadioGroupRootProps, } from "./RadioGroup"; function setUp(jsx: ReactElement) { return { user: userEvent.setup(), ...render(jsx), }; } function RadioGroup(props: RadioGroupRootProps) { return ; } function Radio(props: RadioGroupItemProps) { return ( ); } interface TestRadioGroupProps extends RadioGroupRootProps { label?: ReactNode; description?: ReactNode; errorMessage?: ReactNode; children?: ReactNode; } function TestRadioGroup({ label, description, errorMessage, children, ...rootProps }: TestRadioGroupProps) { return ( {label && {label}} {children} {description && ( {description} )} {errorMessage && ( {errorMessage} )} ); } function ControlledRadioGroup( props: React.PropsWithChildren>, ) { const { defaultValue } = props; const [value, setValue] = React.useState(defaultValue); const mockSetValue = mock((value: string) => setValue(value)); return ; } describe("useRadioGroup", () => { const FIRST_VALUE = "first"; const SECOND_VALUE = "second"; const THIRD_VALUE = "third"; const values = [FIRST_VALUE, SECOND_VALUE, THIRD_VALUE]; it("should render correctly", () => { const { getByTestId } = setUp( {values.map((value) => ( ))} , ); for (const value of values) { const control = getByTestId(value); expect(control).toBeInTheDocument(); } }); it("should change value on click", async () => { const { user, getByTestId } = setUp( {values.map((value) => ( ))} , ); const firstControl = getByTestId(FIRST_VALUE); const secondControl = getByTestId(SECOND_VALUE); await user.click(secondControl); expect(firstControl).not.toHaveAttribute("data-checked"); expect(secondControl).toHaveAttribute("data-checked"); }); it("should onValueChange be called", async () => { const handleValueChange = mock(() => {}); const { user, getByTestId } = setUp( {values.map((value) => ( ))} , ); const secondControl = getByTestId(SECOND_VALUE); await user.click(secondControl); expect(handleValueChange).toHaveBeenCalledWith(SECOND_VALUE); }); describe("disabled prop test", () => { it("should disabled when disabled prop is true", async () => { const { getByTestId } = setUp( {values.map((value) => ( ))} , ); for (const value of values) { const control = getByTestId(value); expect(control).toHaveAttribute("data-disabled"); } }); it("should not change value on click when disabled", async () => { const { user, getByTestId } = setUp( {values.map((value) => ( ))} , ); const firstControl = getByTestId(FIRST_VALUE); const secondControl = getByTestId(SECOND_VALUE); await user.click(secondControl); expect(firstControl).toHaveAttribute("data-checked"); expect(secondControl).not.toHaveAttribute("data-checked"); }); it("should not call onValueChange when disabled", async () => { const handleValueChange = mock(() => {}); const { user, getByTestId } = setUp( {values.map((value) => ( ))} , ); const secondControl = getByTestId(SECOND_VALUE); await user.click(secondControl); expect(handleValueChange).not.toHaveBeenCalled(); }); }); describe("controlled test", () => { it("should render correctly with controlled value", () => { const { getByTestId } = setUp( {values.map((value) => ( ))} , ); const secondControl = getByTestId(SECOND_VALUE); expect(secondControl).toHaveAttribute("data-checked"); }); it("should change value on click with controlled value", async () => { const { user, getByTestId } = setUp( {values.map((value) => ( ))} , ); const firstControl = getByTestId(FIRST_VALUE); const secondControl = getByTestId(SECOND_VALUE); await user.click(secondControl); expect(firstControl).not.toHaveAttribute("data-checked"); expect(secondControl).toHaveAttribute("data-checked"); }); }); describe("invalid prop test", () => { it("should have data-invalid when invalid prop is true", () => { const { getByTestId } = setUp( {values.map((value) => ( ))} , ); const root = getByTestId("radio-group-root"); expect(root).toHaveAttribute("data-invalid"); }); it("should have aria-invalid on root when invalid", () => { const { getByTestId } = setUp( {values.map((value) => ( ))} , ); const root = getByTestId("radio-group-root"); expect(root).toHaveAttribute("aria-invalid", "true"); }); }); describe("label, description, and error message", () => { it("should render label as div element", () => { const { getByTestId } = setUp( {values.map((value) => ( ))} , ); const label = getByTestId("radio-group-label"); expect(label).toBeInTheDocument(); expect(label.tagName).toBe("DIV"); }); it("should render description as span element", () => { const { getByTestId } = setUp( {values.map((value) => ( ))} , ); const description = getByTestId("radio-group-description"); expect(description).toBeInTheDocument(); expect(description.tagName).toBe("SPAN"); }); it("should render error message as div element", () => { const { getByTestId } = setUp( {values.map((value) => ( ))} , ); const errorMessage = getByTestId("radio-group-error-message"); expect(errorMessage).toBeInTheDocument(); expect(errorMessage.tagName).toBe("DIV"); }); }); describe("aria attributes", () => { it("should have role=radiogroup on root", () => { const { getByTestId } = setUp( {values.map((value) => ( ))} , ); const root = getByTestId("radio-group-root"); expect(root).toHaveAttribute("role", "radiogroup"); }); it("should connect label to root with aria-labelledby", () => { const { getByTestId } = setUp( {values.map((value) => ( ))} , ); const root = getByTestId("radio-group-root"); const label = getByTestId("radio-group-label"); expect(label).toHaveAttribute("id"); expect(root).toHaveAttribute("aria-labelledby", label.getAttribute("id")); }); it("should connect description to root with aria-describedby", () => { const { getByTestId } = setUp( {values.map((value) => ( ))} , ); const root = getByTestId("radio-group-root"); const description = getByTestId("radio-group-description"); expect(description).toHaveAttribute("id"); expect(root).toHaveAttribute("aria-describedby"); expect(root.getAttribute("aria-describedby")?.split(" ")).toContain( description.getAttribute("id"), ); }); it("should connect error message to root with aria-describedby", () => { const { getByTestId } = setUp( {values.map((value) => ( ))} , ); const root = getByTestId("radio-group-root"); const errorMessage = getByTestId("radio-group-error-message"); expect(errorMessage).toHaveAttribute("id"); expect(root).toHaveAttribute("aria-describedby"); expect(root.getAttribute("aria-describedby")?.split(" ")).toContain( errorMessage.getAttribute("id"), ); }); it("should combine description and error in aria-describedby", () => { const { getByTestId } = setUp( {values.map((value) => ( ))} , ); const root = getByTestId("radio-group-root"); const description = getByTestId("radio-group-description"); const errorMessage = getByTestId("radio-group-error-message"); const ariaDescribedBy = root.getAttribute("aria-describedby"); expect(ariaDescribedBy?.split(" ")).toContain(description.getAttribute("id")); expect(ariaDescribedBy?.split(" ")).toContain(errorMessage.getAttribute("id")); }); it("should not have aria-labelledby when label is not rendered", () => { const { getByTestId } = setUp( {values.map((value) => ( ))} , ); const root = getByTestId("radio-group-root"); expect(root).not.toHaveAttribute("aria-labelledby"); }); it("should not have aria-describedby when neither description nor error is rendered", () => { const { getByTestId } = setUp( {values.map((value) => ( ))} , ); const root = getByTestId("radio-group-root"); expect(root).not.toHaveAttribute("aria-describedby"); }); it("should set aria-live on error message", () => { const { getByTestId } = setUp( {values.map((value) => ( ))} , ); const errorMessage = getByTestId("radio-group-error-message"); expect(errorMessage).toHaveAttribute("aria-live", "polite"); }); }); describe("ID generation", () => { it("should generate unique id", () => { const { getByTestId } = setUp( {values.map((value) => ( ))} , ); const label = getByTestId("radio-group-label"); expect(label.id).toMatch(/^fieldset:.+:label$/); }); }); describe("name and form prop", () => { it("should use provided name prop for all radio inputs", () => { const { getByTestId } = setUp( {values.map((value) => ( ))} , ); for (const value of values) { const input = getByTestId(`${value}-input`); expect(input).toHaveAttribute("name", "custom-name"); } }); it("should fallback to generated id when name is not provided", () => { const { getByTestId } = setUp( {values.map((value) => ( ))} , ); const firstInput = getByTestId(`${FIRST_VALUE}-input`); const secondInput = getByTestId(`${SECOND_VALUE}-input`); // All inputs should have the same name (generated id) expect(firstInput).toHaveAttribute("name"); expect(firstInput.getAttribute("name")).toBe(secondInput.getAttribute("name")); }); it("should use provided form prop for all radio inputs", () => { const { getByTestId } = setUp( {values.map((value) => ( ))} , ); for (const value of values) { const input = getByTestId(`${value}-input`); expect(input).toHaveAttribute("form", "my-form"); } }); it("should not have form attribute when form prop is not provided", () => { const { getByTestId } = setUp( {values.map((value) => ( ))} , ); for (const value of values) { const input = getByTestId(`${value}-input`); expect(input).not.toHaveAttribute("form"); } }); }); describe("data attributes propagation", () => { it("should propagate data-disabled to label, description, and error message", () => { const { getByTestId } = setUp( {values.map((value) => ( ))} , ); const root = getByTestId("radio-group-root"); const label = getByTestId("radio-group-label"); const description = getByTestId("radio-group-description"); const errorMessage = getByTestId("radio-group-error-message"); [root, label, description, errorMessage].forEach((element) => { expect(element).toHaveAttribute("data-disabled"); }); }); it("should propagate data-invalid to label, description, and error message", () => { const { getByTestId } = setUp( {values.map((value) => ( ))} , ); const root = getByTestId("radio-group-root"); const label = getByTestId("radio-group-label"); const description = getByTestId("radio-group-description"); const errorMessage = getByTestId("radio-group-error-message"); [root, label, description, errorMessage].forEach((element) => { expect(element).toHaveAttribute("data-invalid"); }); }); }); });