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");
});
});
});
});