import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { describe, it, expect, vi } from "vitest"; import { RadioButtonField } from "./RadioButtonField"; const choices = { choiceLabels: ["Apple", "Banana", "Cherry"], choiceValues: ["apple", "banana", "cherry"], }; describe("RadioButtonField - visibility", () => { it("returns null when showWhen is false", () => { const { container } = render( ); expect(container.innerHTML).toBe(""); }); it("renders when showWhen is true (default)", () => { render(); expect(screen.getByRole("radiogroup")).toBeInTheDocument(); }); }); describe("RadioButtonField - rendering", () => { it("renders all choice labels", () => { render(); expect(screen.getByLabelText("Apple")).toBeInTheDocument(); expect(screen.getByLabelText("Banana")).toBeInTheDocument(); expect(screen.getByLabelText("Cherry")).toBeInTheDocument(); }); it("renders the correct number of radio inputs", () => { render(); expect(screen.getAllByRole("radio")).toHaveLength(3); }); it("renders label text when provided", () => { render(); expect(screen.getByText("Favorite Fruit")).toBeInTheDocument(); }); it("renders instructions when provided", () => { render(); expect(screen.getByText("Pick one")).toBeInTheDocument(); }); }); describe("RadioButtonField - selection state", () => { it("pre-selected value renders as checked", () => { render(); expect(screen.getByLabelText("Banana")).toBeChecked(); expect(screen.getByLabelText("Apple")).not.toBeChecked(); expect(screen.getByLabelText("Cherry")).not.toBeChecked(); }); it("no radio is checked when value is undefined", () => { render(); screen.getAllByRole("radio").forEach((radio) => { expect(radio).not.toBeChecked(); }); }); it("calls onChange with the selected value when a radio is clicked", async () => { const user = userEvent.setup(); const onChange = vi.fn(); render(); await user.click(screen.getByLabelText("Cherry")); expect(onChange).toHaveBeenCalledWith("cherry"); }); it("calls saveInto when onChange is not provided", async () => { const user = userEvent.setup(); const saveInto = vi.fn(); render(); await user.click(screen.getByLabelText("Apple")); expect(saveInto).toHaveBeenCalledWith("apple"); }); it("prefers onChange over saveInto when both are provided", async () => { const user = userEvent.setup(); const onChange = vi.fn(); const saveInto = vi.fn(); render(); await user.click(screen.getByLabelText("Banana")); expect(onChange).toHaveBeenCalledWith("banana"); expect(saveInto).not.toHaveBeenCalled(); }); }); describe("RadioButtonField - disabled state", () => { it("disables all radio inputs when disabled=true", () => { render(); screen.getAllByRole("radio").forEach((radio) => { expect(radio).toBeDisabled(); }); }); it("does not call onChange when disabled and a radio is clicked", async () => { const user = userEvent.setup(); const onChange = vi.fn(); render(); await user.click(screen.getByLabelText("Apple")); expect(onChange).not.toHaveBeenCalled(); }); }); describe("RadioButtonField - validations", () => { it("renders validation messages", () => { render( ); expect(screen.getByText("This field is required")).toBeInTheDocument(); expect(screen.getByText("Invalid selection")).toBeInTheDocument(); }); it("marks radio inputs as aria-invalid when validations are present", () => { render(); screen.getAllByRole("radio").forEach((radio) => { expect(radio).toHaveAttribute("aria-invalid", "true"); }); }); it("renders requiredMessage when required and no value selected", () => { render( ); expect(screen.getByText("Please select an option")).toBeInTheDocument(); }); it("does not render requiredMessage when a value is selected", () => { render( ); expect(screen.queryByText("Please select an option")).not.toBeInTheDocument(); }); }); describe("RadioButtonField - layout", () => { it("choiceLayout=STACKED applies flex-col", () => { render(); const group = screen.getByRole("radiogroup"); expect(group).toHaveClass("flex-col"); }); it("choiceLayout=COMPACT applies flex-wrap", () => { render(); const group = screen.getByRole("radiogroup"); expect(group).toHaveClass("flex-wrap"); }); }); describe("RadioButtonField - choiceStyle CARDS", () => { it("applies card styling to each choice item", () => { const { container } = render( ); const cardItems = container.querySelectorAll(".border.border-gray-300.rounded-sm"); expect(cardItems.length).toBe(3); }); it("clicking the card container (not the input) triggers selection", async () => { const user = userEvent.setup(); const onChange = vi.fn(); const { container } = render( ); // Click the label text (part of the card, not the input itself) await user.click(screen.getByText("Banana")); expect(onChange).toHaveBeenCalledWith("banana"); }); it("selected card gets highlighted classes", () => { const { container } = render( ); const cards = container.querySelectorAll(".border.rounded-sm"); // First card (Apple) should be highlighted expect(cards[0]).toHaveClass("border-blue-500"); expect(cards[0]).not.toHaveClass("border-gray-300"); // Others should not expect(cards[1]).not.toHaveClass("border-blue-500"); }); }); describe("RadioButtonField - choicePosition", () => { it("choicePosition=END applies flex-row-reverse to item containers", () => { const { container } = render( ); const items = container.querySelectorAll(".flex-row-reverse"); expect(items.length).toBe(3); }); it("choicePosition=START does not apply flex-row-reverse", () => { const { container } = render( ); const items = container.querySelectorAll(".flex-row-reverse"); expect(items.length).toBe(0); }); it("CARDS style defaults choicePosition to END", () => { const { container } = render( ); const items = container.querySelectorAll(".flex-row-reverse"); expect(items.length).toBe(3); }); });