/* * 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/checkbox/test/Checkbox.test.js */ import { installPointerEvent } from "@kobalte/tests"; import { fireEvent, render } from "@solidjs/testing-library"; import { vi } from "vitest"; import * as Checkbox from "."; describe("Checkbox", () => { installPointerEvent(); const onChangeSpy = vi.fn(); afterEach(() => { onChangeSpy.mockClear(); }); it("should generate default ids", () => { const { getByTestId } = render(() => ( Label )); const checkboxRoot = getByTestId("checkbox"); const input = getByTestId("input"); const control = getByTestId("control"); const indicator = getByTestId("indicator"); const label = getByTestId("label"); expect(checkboxRoot.id).toBeDefined(); expect(input.id).toBe(`${checkboxRoot.id}-input`); expect(control.id).toBe(`${checkboxRoot.id}-control`); expect(indicator.id).toBe(`${checkboxRoot.id}-indicator`); expect(label.id).toBe(`${checkboxRoot.id}-label`); }); it("should generate ids based on checkbox id", () => { const { getByTestId } = render(() => ( Label )); const checkboxRoot = getByTestId("checkbox"); const input = getByTestId("input"); const control = getByTestId("control"); const indicator = getByTestId("indicator"); const label = getByTestId("label"); expect(checkboxRoot.id).toBe("foo"); expect(input.id).toBe("foo-input"); expect(control.id).toBe("foo-control"); expect(indicator.id).toBe("foo-indicator"); expect(label.id).toBe("foo-label"); }); it("supports custom ids", () => { const { getByTestId } = render(() => ( Label )); const checkboxRoot = getByTestId("checkbox"); const input = getByTestId("input"); const control = getByTestId("control"); const indicator = getByTestId("indicator"); const label = getByTestId("label"); expect(checkboxRoot.id).toBe("custom-checkbox-id"); expect(input.id).toBe("custom-input-id"); expect(control.id).toBe("custom-control-id"); expect(indicator.id).toBe("custom-indicator-id"); expect(label.id).toBe("custom-label-id"); }); it("should set input type to checkbox", async () => { const { getByRole } = render(() => ( )); const input = getByRole("checkbox"); expect(input).toHaveAttribute("type", "checkbox"); }); it("should have default value of 'on'", async () => { const { getByRole } = render(() => ( )); const input = getByRole("checkbox") as HTMLInputElement; expect(input.value).toBe("on"); }); it("supports custom value", async () => { const { getByRole } = render(() => ( )); const input = getByRole("checkbox") as HTMLInputElement; expect(input.value).toBe("custom"); }); it("ensure default unchecked can be checked", async () => { const { getByRole } = render(() => ( )); const input = getByRole("checkbox") as HTMLInputElement; expect(input.checked).toBeFalsy(); expect(onChangeSpy).not.toHaveBeenCalled(); fireEvent.click(input); await Promise.resolve(); expect(input.checked).toBeTruthy(); expect(onChangeSpy.mock.calls[0][0]).toBe(true); fireEvent.click(input); await Promise.resolve(); expect(onChangeSpy.mock.calls[1][0]).toBe(false); }); it("can be default checked", async () => { const { getByRole } = render(() => ( )); const input = getByRole("checkbox") as HTMLInputElement; expect(input.checked).toBeTruthy(); fireEvent.click(input); await Promise.resolve(); expect(input.checked).toBeFalsy(); expect(onChangeSpy.mock.calls[0][0]).toBe(false); }); it("can be controlled checked", async () => { const { getByRole } = render(() => ( )); const input = getByRole("checkbox") as HTMLInputElement; expect(input.checked).toBeTruthy(); fireEvent.click(input); await Promise.resolve(); expect(input.checked).toBeTruthy(); expect(onChangeSpy.mock.calls[0][0]).toBe(false); }); it("can be controlled unchecked", async () => { const { getByRole } = render(() => ( )); const input = getByRole("checkbox") as HTMLInputElement; expect(input.checked).toBeFalsy(); fireEvent.click(input); await Promise.resolve(); expect(input.checked).toBeFalsy(); expect(onChangeSpy.mock.calls[0][0]).toBe(true); }); it("can be indeterminate", async () => { const { getByRole } = render(() => ( )); const input = getByRole("checkbox") as HTMLInputElement; expect(input.indeterminate).toBeTruthy(); expect(input.checked).toBeFalsy(); fireEvent.click(input); await Promise.resolve(); expect(input.indeterminate).toBeTruthy(); expect(input.checked).toBeTruthy(); expect(onChangeSpy).toHaveBeenCalled(); expect(onChangeSpy.mock.calls[0][0]).toBe(true); fireEvent.click(input); await Promise.resolve(); expect(input.indeterminate).toBeTruthy(); expect(input.checked).toBeFalsy(); expect(onChangeSpy.mock.calls[1][0]).toBe(false); }); it("can be checked by clicking on the control", async () => { const { getByRole, getByTestId } = render(() => ( )); const input = getByRole("checkbox") as HTMLInputElement; const control = getByTestId("control"); expect(input.checked).toBeFalsy(); fireEvent.click(control); await Promise.resolve(); expect(input.checked).toBeTruthy(); expect(onChangeSpy.mock.calls[0][0]).toBe(true); }); it("can be checked by pressing the Space key on the control", async () => { const { getByRole, getByTestId } = render(() => ( )); const input = getByRole("checkbox") as HTMLInputElement; const control = getByTestId("control"); expect(input.checked).toBeFalsy(); fireEvent.keyDown(control, { key: " " }); fireEvent.keyUp(control, { key: " " }); await Promise.resolve(); expect(input.checked).toBeTruthy(); expect(onChangeSpy.mock.calls[0][0]).toBe(true); }); it("can be disabled", async () => { const { getByRole, getByTestId } = render(() => ( Label )); const label = getByTestId("label"); const input = getByRole("checkbox") as HTMLInputElement; expect(input.disabled).toBeTruthy(); expect(input.checked).toBeFalsy(); // I don't know why but `fireEvent` on the input fire the click even if the input is disabled. fireEvent.click(label); await Promise.resolve(); expect(input.checked).toBeFalsy(); expect(onChangeSpy).not.toHaveBeenCalled(); }); it("can be invalid", async () => { const { getByRole } = render(() => ( )); const input = getByRole("checkbox") as HTMLInputElement; expect(input).toHaveAttribute("aria-invalid", "true"); }); it("passes through 'aria-errormessage'", async () => { const { getByRole } = render(() => ( )); const input = getByRole("checkbox") as HTMLInputElement; expect(input).toHaveAttribute("aria-invalid", "true"); expect(input).toHaveAttribute("aria-errormessage", "test"); }); it("supports visible label", async () => { const { getByRole, getByText } = render(() => ( Label )); const input = getByRole("checkbox") as HTMLInputElement; const label = getByText("Label"); expect(input).toHaveAttribute("aria-labelledby", label.id); expect(label).toBeInstanceOf(HTMLLabelElement); expect(label).toHaveAttribute("for", input.id); }); it("supports 'aria-labelledby'", async () => { const { getByRole } = render(() => ( )); const input = getByRole("checkbox") as HTMLInputElement; expect(input).toHaveAttribute("aria-labelledby", "foo"); }); it("should combine 'aria-labelledby' if visible label is also provided", async () => { const { getByRole, getByText } = render(() => ( Label )); const input = getByRole("checkbox") as HTMLInputElement; const label = getByText("Label"); expect(input).toHaveAttribute("aria-labelledby", `foo ${label.id}`); }); it("supports 'aria-label'", async () => { const { getByRole } = render(() => ( )); const input = getByRole("checkbox") as HTMLInputElement; expect(input).toHaveAttribute("aria-label", "My Label"); }); it("should combine 'aria-labelledby' if visible label and 'aria-label' is also provided", async () => { const { getByRole, getByText } = render(() => ( Label )); const input = getByRole("checkbox") as HTMLInputElement; const label = getByText("Label"); expect(input).toHaveAttribute( "aria-labelledby", `foo ${label.id} ${input.id}`, ); }); it("supports visible description", async () => { const { getByRole, getByText } = render(() => ( Description )); const input = getByRole("checkbox") as HTMLInputElement; const description = getByText("Description"); expect(description.id).toBeDefined(); expect(input.id).toBeDefined(); expect(input).toHaveAttribute("aria-describedby", description.id); // check that generated ids are unique expect(description.id).not.toBe(input.id); }); it("supports 'aria-describedby'", async () => { const { getByRole } = render(() => ( )); const input = getByRole("checkbox") as HTMLInputElement; expect(input).toHaveAttribute("aria-describedby", "foo"); }); it("should combine 'aria-describedby' if visible description", async () => { const { getByRole, getByText } = render(() => ( Description )); const input = getByRole("checkbox") as HTMLInputElement; const description = getByText("Description"); expect(input).toHaveAttribute("aria-describedby", `${description.id} foo`); }); it("supports visible error message when invalid", async () => { const { getByRole, getByText } = render(() => ( ErrorMessage )); const input = getByRole("checkbox") as HTMLInputElement; const errorMessage = getByText("ErrorMessage"); expect(errorMessage.id).toBeDefined(); expect(input.id).toBeDefined(); expect(input).toHaveAttribute("aria-describedby", errorMessage.id); // check that generated ids are unique expect(errorMessage.id).not.toBe(input.id); }); it("should not be described by error message when not invalid", async () => { const { getByRole } = render(() => ( ErrorMessage )); const input = getByRole("checkbox") as HTMLInputElement; expect(input).not.toHaveAttribute("aria-describedby"); }); it("should combine 'aria-describedby' if visible error message when invalid", () => { const { getByRole, getByText } = render(() => ( ErrorMessage )); const input = getByRole("checkbox") as HTMLInputElement; const errorMessage = getByText("ErrorMessage"); expect(input).toHaveAttribute("aria-describedby", `${errorMessage.id} foo`); }); it("should combine 'aria-describedby' if visible description and error message when invalid", () => { const { getByRole, getByText } = render(() => ( Description ErrorMessage )); const input = getByRole("checkbox") as HTMLInputElement; const description = getByText("Description"); const errorMessage = getByText("ErrorMessage"); expect(input).toHaveAttribute( "aria-describedby", `${description.id} ${errorMessage.id} foo`, ); }); it("can be readonly", async () => { const { getByRole } = render(() => ( )); const input = getByRole("checkbox") as HTMLInputElement; expect(input.checked).toBeTruthy(); expect(input).toHaveAttribute("aria-readonly", "true"); fireEvent.click(input); await Promise.resolve(); expect(input.checked).toBeTruthy(); expect(onChangeSpy).not.toHaveBeenCalled(); }); it("supports uncontrolled readonly", async () => { const { getByRole } = render(() => ( )); const input = getByRole("checkbox") as HTMLInputElement; expect(input.checked).toBeFalsy(); fireEvent.click(input); await Promise.resolve(); expect(input.checked).toBeFalsy(); expect(onChangeSpy).not.toHaveBeenCalled(); }); describe("indicator", () => { it("should not display indicator by default", async () => { const { queryByTestId } = render(() => ( )); expect(queryByTestId("indicator")).toBeNull(); }); it("should display indicator when 'checked'", async () => { const { getByRole, queryByTestId, getByTestId } = render(() => ( )); const input = getByRole("checkbox") as HTMLInputElement; expect(input.checked).toBeFalsy(); expect(queryByTestId("indicator")).toBeNull(); fireEvent.click(input); await Promise.resolve(); expect(input.checked).toBeTruthy(); expect(getByTestId("indicator")).toBeInTheDocument(); fireEvent.click(input); await Promise.resolve(); expect(input.checked).toBeFalsy(); // expect(queryByTestId("indicator")).toBeNull(); // TODO: fix vitest delays }); it("should display indicator when 'indeterminate'", async () => { const { getByTestId } = render(() => ( )); 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 when checkbox is valid", async () => { const { getAllByTestId } = render(() => ( Label )); const elements = getAllByTestId(/^checkbox/); for (const el of elements) { expect(el).toHaveAttribute("data-valid"); } }); it("should have 'data-invalid' attribute when checkbox is invalid", async () => { const { getAllByTestId } = render(() => ( Label )); const elements = getAllByTestId(/^checkbox/); for (const el of elements) { expect(el).toHaveAttribute("data-invalid"); } }); it("should have 'data-checked' attribute when checkbox is checked", async () => { const { getAllByTestId } = render(() => ( Label )); const elements = getAllByTestId(/^checkbox/); for (const el of elements) { expect(el).toHaveAttribute("data-checked"); } }); it("should have 'data-indeterminate' attribute when checkbox is indeterminate", async () => { const { getAllByTestId } = render(() => ( Label )); const elements = getAllByTestId(/^checkbox/); for (const el of elements) { expect(el).toHaveAttribute("data-indeterminate"); } }); it("should have 'data-required' attribute when checkbox is required", async () => { const { getAllByTestId } = render(() => ( Label )); const elements = getAllByTestId(/^checkbox/); for (const el of elements) { expect(el).toHaveAttribute("data-required"); } }); it("should have 'data-disabled' attribute when checkbox is disabled", async () => { const { getAllByTestId } = render(() => ( Label )); const elements = getAllByTestId(/^checkbox/); for (const el of elements) { expect(el).toHaveAttribute("data-disabled"); } }); it("should have 'data-readonly' attribute when checkbox is read only", async () => { const { getAllByTestId } = render(() => ( Label )); const elements = getAllByTestId(/^checkbox/); for (const el of elements) { expect(el).toHaveAttribute("data-readonly"); } }); }); });