/*
* Portions of this file are based on code from react-spectrum.
* Apache License Version 2.0, Copyright 2020 Adobe.
*
* Credits to the React Spectrum team:
* https://github.com/adobe/react-spectrum/blob/810579b671791f1593108f62cdc1893de3a220e3/packages/@react-spectrum/radio/test/Radio.test.js
*/
import { installPointerEvent } from "@kobalte/tests";
import { fireEvent, render } from "@solidjs/testing-library";
import { vi } from "vitest";
import * as RadioGroup from ".";
describe("RadioGroup", () => {
installPointerEvent();
it("handles defaults", async () => {
const onChangeSpy = vi.fn();
const { getByRole, getAllByRole, getByLabelText } = render(() => (
Favorite Pet
Dogs
Cats
Dragons
));
const radioGroup = getByRole("radiogroup");
const inputs = getAllByRole("radio") as HTMLInputElement[];
expect(radioGroup).toBeInTheDocument();
expect(inputs.length).toBe(3);
const groupName = inputs[0].getAttribute("name");
expect(inputs[0]).toHaveAttribute("name", groupName);
expect(inputs[1]).toHaveAttribute("name", groupName);
expect(inputs[2]).toHaveAttribute("name", groupName);
expect(inputs[0].value).toBe("dogs");
expect(inputs[1].value).toBe("cats");
expect(inputs[2].value).toBe("dragons");
expect(inputs[0].checked).toBeFalsy();
expect(inputs[1].checked).toBeFalsy();
expect(inputs[2].checked).toBeFalsy();
const dragons = getByLabelText("Dragons");
fireEvent.click(dragons);
await Promise.resolve();
expect(onChangeSpy).toHaveBeenCalledTimes(1);
expect(onChangeSpy).toHaveBeenCalledWith("dragons");
expect(inputs[0].checked).toBeFalsy();
expect(inputs[1].checked).toBeFalsy();
expect(inputs[2].checked).toBeTruthy();
});
it("can have a default value", async () => {
const onChangeSpy = vi.fn();
const { getByRole, getAllByRole, getByLabelText } = render(() => (
Favorite Pet
Dogs
Cats
Dragons
));
const radioGroup = getByRole("radiogroup");
const inputs = getAllByRole("radio") as HTMLInputElement[];
expect(radioGroup).toBeTruthy();
expect(inputs.length).toBe(3);
expect(onChangeSpy).not.toHaveBeenCalled();
expect(inputs[0].checked).toBeFalsy();
expect(inputs[1].checked).toBeTruthy();
expect(inputs[2].checked).toBeFalsy();
const dragons = getByLabelText("Dragons");
fireEvent.click(dragons);
await Promise.resolve();
expect(onChangeSpy).toHaveBeenCalledTimes(1);
expect(onChangeSpy).toHaveBeenCalledWith("dragons");
expect(inputs[0].checked).toBeFalsy();
expect(inputs[1].checked).toBeFalsy();
expect(inputs[2].checked).toBeTruthy();
});
it("value can be controlled", async () => {
const onChangeSpy = vi.fn();
const { getAllByRole, getByLabelText } = render(() => (
Favorite Pet
Dogs
Cats
Dragons
));
const inputs = getAllByRole("radio") as HTMLInputElement[];
expect(inputs[0].checked).toBeFalsy();
expect(inputs[1].checked).toBeTruthy();
expect(inputs[2].checked).toBeFalsy();
const dragons = getByLabelText("Dragons");
fireEvent.click(dragons);
await Promise.resolve();
expect(onChangeSpy).toHaveBeenCalledTimes(1);
expect(onChangeSpy).toHaveBeenCalledWith("dragons");
expect(inputs[0].checked).toBeFalsy();
expect(inputs[1].checked).toBeTruthy();
// false because `value` is controlled.
expect(inputs[2].checked).toBeFalsy();
});
it("can select value by clicking on the item control", async () => {
const onChangeSpy = vi.fn();
const { getByRole, getAllByRole, getByTestId } = render(() => (
Favorite Pet
Dogs
Cats
Dragons
));
const radioGroup = getByRole("radiogroup");
const inputs = getAllByRole("radio") as HTMLInputElement[];
expect(radioGroup).toBeTruthy();
expect(inputs.length).toBe(3);
expect(onChangeSpy).not.toHaveBeenCalled();
expect(inputs[0].checked).toBeFalsy();
expect(inputs[1].checked).toBeFalsy();
expect(inputs[2].checked).toBeFalsy();
const dragonsControl = getByTestId("dragons-control");
fireEvent.click(dragonsControl);
await Promise.resolve();
expect(onChangeSpy).toHaveBeenCalledTimes(1);
expect(onChangeSpy).toHaveBeenCalledWith("dragons");
expect(inputs[0].checked).toBeFalsy();
expect(inputs[1].checked).toBeFalsy();
expect(inputs[2].checked).toBeTruthy();
});
it("can select value by pressing the Space key on the item control", async () => {
const onChangeSpy = vi.fn();
const { getByRole, getAllByRole, getByTestId } = render(() => (
Favorite Pet
Dogs
Cats
Dragons
));
const radioGroup = getByRole("radiogroup");
const inputs = getAllByRole("radio") as HTMLInputElement[];
expect(radioGroup).toBeTruthy();
expect(inputs.length).toBe(3);
expect(onChangeSpy).not.toHaveBeenCalled();
expect(inputs[0].checked).toBeFalsy();
expect(inputs[1].checked).toBeFalsy();
expect(inputs[2].checked).toBeFalsy();
const dragonsControl = getByTestId("dragons-control");
fireEvent.keyDown(dragonsControl, { key: " " });
fireEvent.keyUp(dragonsControl, { key: " " });
await Promise.resolve();
expect(onChangeSpy).toHaveBeenCalledTimes(1);
expect(onChangeSpy).toHaveBeenCalledWith("dragons");
expect(inputs[0].checked).toBeFalsy();
expect(inputs[1].checked).toBeFalsy();
expect(inputs[2].checked).toBeTruthy();
});
it("name can be controlled", () => {
const { getAllByRole } = render(() => (
Favorite Pet
Dogs
Cats
Dragons
));
const inputs = getAllByRole("radio") as HTMLInputElement[];
expect(inputs[0]).toHaveAttribute("name", "test-name");
expect(inputs[1]).toHaveAttribute("name", "test-name");
expect(inputs[2]).toHaveAttribute("name", "test-name");
});
it("supports visible label", () => {
const { getByRole, getByText } = render(() => (
Favorite Pet
Dogs
Cats
Dragons
));
const radioGroup = getByRole("radiogroup");
const label = getByText("Favorite Pet");
expect(radioGroup).toHaveAttribute("aria-labelledby", label.id);
expect(label).toBeInstanceOf(HTMLSpanElement);
expect(label).not.toHaveAttribute("for");
});
it("supports 'aria-labelledby'", () => {
const { getByRole } = render(() => (
Dogs
Cats
Dragons
));
const radioGroup = getByRole("radiogroup");
expect(radioGroup).toHaveAttribute("aria-labelledby", "foo");
});
it("should combine 'aria-labelledby' if visible label is also provided", () => {
const { getByRole, getByText } = render(() => (
Favorite Pet
Dogs
Cats
Dragons
));
const radioGroup = getByRole("radiogroup");
const label = getByText("Favorite Pet");
expect(radioGroup).toHaveAttribute("aria-labelledby", `foo ${label.id}`);
});
it("supports 'aria-label'", () => {
const { getByRole } = render(() => (
Dogs
Cats
Dragons
));
const radioGroup = getByRole("radiogroup");
expect(radioGroup).toHaveAttribute("aria-label", "My Favorite Pet");
});
it("should combine 'aria-labelledby' if visible label and 'aria-label' is also provided", () => {
const { getByRole, getByText } = render(() => (
Favorite Pet
Dogs
Cats
Dragons
));
const radioGroup = getByRole("radiogroup");
const label = getByText("Favorite Pet");
expect(radioGroup).toHaveAttribute(
"aria-labelledby",
`foo ${label.id} ${radioGroup.id}`,
);
});
it("supports visible description", () => {
const { getByRole, getByText } = render(() => (
Favorite Pet
Dogs
Cats
Dragons
Description
));
const radioGroup = getByRole("radiogroup");
const description = getByText("Description");
expect(description.id).toBeDefined();
expect(radioGroup.id).toBeDefined();
expect(radioGroup).toHaveAttribute("aria-describedby", description.id);
// check that generated ids are unique
expect(description.id).not.toBe(radioGroup.id);
});
it("supports visible description on single radio", () => {
const { getByRole, getByText } = render(() => (
Favorite Pet
Dogs
Description
));
const radio = getByRole("radio");
const itemDescription = getByText("Description");
expect(itemDescription.id).toBeDefined();
expect(radio.id).toBeDefined();
expect(radio).toHaveAttribute("aria-describedby", itemDescription.id);
});
it("supports 'aria-describedby'", () => {
const { getByRole } = render(() => (
Favorite Pet
Dogs
Cats
Dragons
));
const radioGroup = getByRole("radiogroup");
expect(radioGroup).toHaveAttribute("aria-describedby", "foo");
});
it("should combine 'aria-describedby' if visible description", () => {
const { getByRole, getByText } = render(() => (
Favorite Pet
Dogs
Cats
Dragons
Description
));
const radioGroup = getByRole("radiogroup");
const description = getByText("Description");
expect(radioGroup).toHaveAttribute(
"aria-describedby",
`${description.id} foo`,
);
});
it("supports visible error message when invalid", () => {
const { getByRole, getByText } = render(() => (
Favorite Pet
Dogs
Cats
Dragons
ErrorMessage
));
const radioGroup = getByRole("radiogroup");
const errorMessage = getByText("ErrorMessage");
expect(errorMessage.id).toBeDefined();
expect(radioGroup.id).toBeDefined();
expect(radioGroup).toHaveAttribute("aria-describedby", errorMessage.id);
// check that generated ids are unique
expect(errorMessage.id).not.toBe(radioGroup.id);
});
it("should not be described by error message when not invalid", () => {
const { getByRole } = render(() => (
Favorite Pet
Dogs
Cats
Dragons
ErrorMessage
));
const radioGroup = getByRole("radiogroup");
expect(radioGroup).not.toHaveAttribute("aria-describedby");
});
it("should combine 'aria-describedby' if visible error message when invalid", () => {
const { getByRole, getByText } = render(() => (
Favorite Pet
Dogs
Cats
Dragons
ErrorMessage
));
const radioGroup = getByRole("radiogroup");
const errorMessage = getByText("ErrorMessage");
expect(radioGroup).toHaveAttribute(
"aria-describedby",
`${errorMessage.id} foo`,
);
});
it("should combine 'aria-describedby' if visible description and error message when invalid", () => {
const { getByRole, getByText } = render(() => (
Favorite Pet
Dogs
Cats
Dragons
Description
ErrorMessage
));
const radioGroup = getByRole("radiogroup");
const description = getByText("Description");
const errorMessage = getByText("ErrorMessage");
expect(radioGroup).toHaveAttribute(
"aria-describedby",
`${description.id} ${errorMessage.id} foo`,
);
});
it("should not have form control 'data-*' attributes by default", async () => {
const { getByRole } = render(() => (
));
const radioGroup = getByRole("radiogroup");
expect(radioGroup).not.toHaveAttribute("data-valid");
expect(radioGroup).not.toHaveAttribute("data-invalid");
expect(radioGroup).not.toHaveAttribute("data-required");
expect(radioGroup).not.toHaveAttribute("data-disabled");
expect(radioGroup).not.toHaveAttribute("data-readonly");
});
it("should have 'data-valid' attribute when valid", async () => {
const { getByRole } = render(() => (
));
const radioGroup = getByRole("radiogroup");
expect(radioGroup).toHaveAttribute("data-valid");
});
it("should have 'data-invalid' attribute when invalid", async () => {
const { getByRole } = render(() => (
));
const radioGroup = getByRole("radiogroup");
expect(radioGroup).toHaveAttribute("data-invalid");
});
it("should have 'data-required' attribute when required", async () => {
const { getByRole } = render(() => (
));
const radioGroup = getByRole("radiogroup");
expect(radioGroup).toHaveAttribute("data-required");
});
it("should have 'data-disabled' attribute when disabled", async () => {
const { getByRole } = render(() => (
));
const radioGroup = getByRole("radiogroup");
expect(radioGroup).toHaveAttribute("data-disabled");
});
it("should have 'data-readonly' attribute when readonly", async () => {
const { getByRole } = render(() => (
));
const radioGroup = getByRole("radiogroup");
expect(radioGroup).toHaveAttribute("data-readonly");
});
it("sets 'aria-orientation' by default", () => {
const { getByRole } = render(() => (
Favorite Pet
Dogs
Cats
Dragons
));
const radioGroup = getByRole("radiogroup");
expect(radioGroup).toHaveAttribute("aria-orientation", "vertical");
});
it("sets 'aria-orientation' based on the 'orientation' prop", () => {
const { getByRole } = render(() => (
Favorite Pet
Dogs
Cats
Dragons
));
const radioGroup = getByRole("radiogroup");
expect(radioGroup).toHaveAttribute("aria-orientation", "horizontal");
});
it("sets 'aria-invalid' when 'validationState=invalid'", () => {
const { getByRole } = render(() => (
Favorite Pet
Dogs
Cats
Dragons
));
const radioGroup = getByRole("radiogroup");
expect(radioGroup).toHaveAttribute("aria-invalid", "true");
});
it("passes through 'aria-errormessage'", () => {
const { getByRole } = render(() => (
Favorite Pet
Dogs
Cats
Dragons
));
const radioGroup = getByRole("radiogroup");
expect(radioGroup).toHaveAttribute("aria-invalid", "true");
expect(radioGroup).toHaveAttribute("aria-errormessage", "test");
});
it("sets 'aria-required' when 'isRequired' is true", () => {
const { getByRole, getAllByRole } = render(() => (
Favorite Pet
Dogs
Cats
Dragons
));
const radioGroup = getByRole("radiogroup");
expect(radioGroup).toHaveAttribute("aria-required", "true");
const inputs = getAllByRole("radio");
for (const input of inputs) {
expect(input).not.toHaveAttribute("aria-required");
}
});
it("sets 'aria-disabled' and makes radios disabled when 'isDisabled' is true", async () => {
const groupOnChangeSpy = vi.fn();
const { getByRole, getAllByRole, getByLabelText } = render(() => (
Favorite Pet
Dogs
Cats
Dragons
));
const radioGroup = getByRole("radiogroup");
expect(radioGroup).toHaveAttribute("aria-disabled", "true");
const inputs = getAllByRole("radio") as HTMLInputElement[];
expect(inputs[0]).toHaveAttribute("disabled");
expect(inputs[1]).toHaveAttribute("disabled");
expect(inputs[2]).toHaveAttribute("disabled");
const dragons = getByLabelText("Dragons");
fireEvent.click(dragons);
await Promise.resolve();
expect(groupOnChangeSpy).toHaveBeenCalledTimes(0);
expect(inputs[2].checked).toBeFalsy();
});
it("can have a single disabled radio", async () => {
const groupOnChangeSpy = vi.fn();
const { getByText, getAllByRole } = render(() => (
Favorite Pet
Dogs
Cats
Dragons
));
const inputs = getAllByRole("radio") as HTMLInputElement[];
expect(inputs[0]).not.toHaveAttribute("disabled");
expect(inputs[1]).toHaveAttribute("disabled");
expect(inputs[2]).not.toHaveAttribute("disabled");
const dogsLabel = getByText("Dogs") as HTMLLabelElement;
const catsLabel = getByText("Cats") as HTMLLabelElement;
fireEvent.click(catsLabel);
await Promise.resolve();
expect(inputs[1].checked).toBeFalsy();
expect(groupOnChangeSpy).toHaveBeenCalledTimes(0);
expect(inputs[0].checked).toBeFalsy();
expect(inputs[1].checked).toBeFalsy();
expect(inputs[2].checked).toBeFalsy();
fireEvent.click(dogsLabel);
await Promise.resolve();
expect(groupOnChangeSpy).toHaveBeenCalledTimes(1);
expect(groupOnChangeSpy).toHaveBeenCalledWith("dogs");
expect(inputs[0].checked).toBeTruthy();
expect(inputs[1].checked).toBeFalsy();
expect(inputs[2].checked).toBeFalsy();
});
it("doesn't set 'aria-disabled' or make radios disabled by default", () => {
const { getByRole, getAllByRole } = render(() => (
Favorite Pet
Dogs
Cats
Dragons
));
const radioGroup = getByRole("radiogroup");
expect(radioGroup).not.toHaveAttribute("aria-disabled");
const inputs = getAllByRole("radio") as HTMLInputElement[];
expect(inputs[0]).not.toHaveAttribute("disabled");
expect(inputs[1]).not.toHaveAttribute("disabled");
expect(inputs[2]).not.toHaveAttribute("disabled");
});
it("doesn't set 'aria-disabled' or make radios disabled when 'isDisabled' is false", () => {
const { getByRole, getAllByRole } = render(() => (
Favorite Pet
Dogs
Cats
Dragons
));
const radioGroup = getByRole("radiogroup");
expect(radioGroup).not.toHaveAttribute("aria-disabled");
const inputs = getAllByRole("radio") as HTMLInputElement[];
expect(inputs[0]).not.toHaveAttribute("disabled");
expect(inputs[1]).not.toHaveAttribute("disabled");
expect(inputs[2]).not.toHaveAttribute("disabled");
});
it("sets 'aria-readonly=true' on radio group", async () => {
const groupOnChangeSpy = vi.fn();
const { getByRole, getAllByRole, getByLabelText } = render(() => (
Favorite Pet
Dogs
Cats
Dragons
));
const radioGroup = getByRole("radiogroup");
const inputs = getAllByRole("radio") as HTMLInputElement[];
expect(radioGroup).toHaveAttribute("aria-readonly", "true");
expect(inputs[2].checked).toBeFalsy();
const dragons = getByLabelText("Dragons");
fireEvent.click(dragons);
await Promise.resolve();
expect(groupOnChangeSpy).toHaveBeenCalledTimes(0);
expect(inputs[2].checked).toBeFalsy();
});
it("should not update state for readonly radio group", async () => {
const groupOnChangeSpy = vi.fn();
const { getAllByRole, getByLabelText } = render(() => (
Favorite Pet
Dogs
Cats
Dragons
));
const inputs = getAllByRole("radio") as HTMLInputElement[];
const dragons = getByLabelText("Dragons");
fireEvent.click(dragons);
await Promise.resolve();
expect(groupOnChangeSpy).toHaveBeenCalledTimes(0);
expect(inputs[2].checked).toBeFalsy();
});
describe("Radio", () => {
it("should generate default ids", () => {
const { getByTestId } = render(() => (
Cats
));
const radio = getByTestId("radio");
const input = getByTestId("input");
const control = getByTestId("control");
const label = getByTestId("label");
expect(radio.id).toBeDefined();
expect(input.id).toBe(`${radio.id}-input`);
expect(control.id).toBe(`${radio.id}-control`);
expect(label.id).toBe(`${radio.id}-label`);
});
it("should generate ids based on radio id", () => {
const { getByTestId } = render(() => (
Cats
));
const radio = getByTestId("radio");
const input = getByTestId("input");
const control = getByTestId("control");
const label = getByTestId("label");
expect(radio.id).toBe("foo");
expect(input.id).toBe("foo-input");
expect(control.id).toBe("foo-control");
expect(label.id).toBe("foo-label");
});
it("supports custom ids", () => {
const { getByTestId } = render(() => (
Cats
));
const radio = getByTestId("radio");
const input = getByTestId("input");
const control = getByTestId("control");
const label = getByTestId("label");
expect(radio.id).toBe("custom-radio-id");
expect(input.id).toBe("custom-input-id");
expect(control.id).toBe("custom-control-id");
expect(label.id).toBe("custom-label-id");
});
it("supports 'aria-label'", () => {
const { getByRole } = render(() => (
Cats
));
const radio = getByRole("radio");
expect(radio).toHaveAttribute("aria-label", "Label");
});
it("supports 'aria-labelledby'", () => {
const { getByRole, getByTestId } = render(() => (
Cats
));
const radioLabel = getByTestId("radio-label");
const radio = getByRole("radio");
expect(radio).toHaveAttribute("aria-labelledby", `foo ${radioLabel.id}`);
});
it("should combine 'aria-label' and 'aria-labelledby'", () => {
const { getByRole, getByTestId } = render(() => (
Cats
));
const radioLabel = getByTestId("radio-label");
const radio = getByRole("radio");
expect(radio).toHaveAttribute(
"aria-labelledby",
`foo ${radioLabel.id} ${radio.id}`,
);
});
it("supports 'aria-describedby'", () => {
const { getByRole } = render(() => (
Cats
));
const radio = getByRole("radio");
expect(radio).toHaveAttribute("aria-describedby", "foo");
});
it("should combine 'aria-describedby' from both radio and radio group", () => {
const { getByRole, getByTestId } = render(() => (
Cats
Description
));
const radio = getByRole("radio");
const description = getByTestId("description");
expect(radio).toHaveAttribute(
"aria-describedby",
`foo ${description.id}`,
);
});
describe("indicator", () => {
it("should not display indicator by default", async () => {
const { queryByTestId } = render(() => (
));
expect(queryByTestId("indicator")).toBeNull();
});
it("should display indicator when 'selected'", async () => {
const { getByRole, queryByTestId, getByTestId } = render(() => (
));
const input = getByRole("radio") as HTMLInputElement;
expect(input.checked).toBeFalsy();
expect(queryByTestId("indicator")).toBeNull();
fireEvent.click(input);
await Promise.resolve();
expect(input.checked).toBeTruthy();
expect(getByTestId("indicator")).toBeInTheDocument();
});
it("should display indicator when 'forceMount'", async () => {
const { getByTestId } = render(() => (
));
expect(getByTestId("indicator")).toBeInTheDocument();
});
});
describe("data-attributes", () => {
it("should have 'data-valid' attribute on radio elements when radio group is valid", async () => {
const { getAllByTestId } = render(() => (
Cats
));
const elements = getAllByTestId(/^radio/);
for (const el of elements) {
expect(el).toHaveAttribute("data-valid");
}
});
it("should have 'data-invalid' attribute on radios when radio group is invalid", async () => {
const { getAllByTestId } = render(() => (
Cats
));
const elements = getAllByTestId(/^radio/);
for (const el of elements) {
expect(el).toHaveAttribute("data-invalid");
}
});
it("should have 'data-required' attribute on radios when radio group is required", async () => {
const { getAllByTestId } = render(() => (
Cats
));
const elements = getAllByTestId(/^radio/);
for (const el of elements) {
expect(el).toHaveAttribute("data-required");
}
});
it("should have 'data-readonly' attribute on radios when radio group is readonly", async () => {
const { getAllByTestId } = render(() => (
Cats
));
const elements = getAllByTestId(/^radio/);
for (const el of elements) {
expect(el).toHaveAttribute("data-readonly");
}
});
it("should have 'data-disabled' attribute on radios when radio group is disabled", async () => {
const { getAllByTestId } = render(() => (
Cats
));
const elements = getAllByTestId(/^radio/);
for (const el of elements) {
expect(el).toHaveAttribute("data-disabled");
}
});
it("should have 'data-disabled' attribute on single disabled radio", async () => {
const { getAllByTestId } = render(() => (
Cats
));
const elements = getAllByTestId(/^radio/);
for (const el of elements) {
expect(el).toHaveAttribute("data-disabled");
}
});
it("should have 'data-checked' attribute on checked radio", async () => {
const { getAllByTestId } = render(() => (
Cats
));
const elements = getAllByTestId(/^radio/);
for (const el of elements) {
expect(el).toHaveAttribute("data-checked");
}
});
});
});
});