import { describe, expect, it, vi, afterEach } from "vitest" import { render, screen, fireEvent, cleanup } from "@testing-library/react" import userEvent from "@testing-library/user-event" import { Input } from "./Input" import { TextField } from "react-aria-components" afterEach(() => { cleanup() }) // Wrap Input in TextField for proper React Aria context const renderInput = (props: React.ComponentProps) => { return render( ) } // Render Input standalone (without TextField) for testing Input's own behavior const renderInputStandalone = (props: React.ComponentProps) => { return render() } describe("Input", () => { describe("props", () => { it("applies readOnly attribute", () => { renderInput({ label: "Email", readOnly: true }) expect(screen.getByRole("textbox")).toHaveProperty("readOnly", true) }) it("applies placeholder for floating label when provided", () => { renderInput({ label: "Email", placeholder: "Enter email" }) expect(screen.getByRole("textbox").getAttribute("placeholder")).toBe( "Enter email" ) }) it("applies empty placeholder for top label by default", () => { renderInput({ label: "Email", labelPosition: "top" }) expect(screen.getByRole("textbox").getAttribute("placeholder")).toBe("") }) it("applies custom id", () => { // Use standalone render since TextField overrides id via context renderInputStandalone({ label: "Email", id: "custom-id" }) expect(screen.getByRole("textbox").getAttribute("id")).toBe("custom-id") }) it("applies aria-describedby", () => { renderInput({ label: "Email", "aria-describedby": "error-message" }) expect(screen.getByRole("textbox").getAttribute("aria-describedby")).toBe( "error-message" ) }) }) describe("clear content button", () => { it("does not show clear button when showClearContentIcon is false", () => { renderInput({ label: "Email", value: "test", onChange: vi.fn(), showClearContentIcon: false, }) expect(screen.queryByLabelText("Clear content")).toBeNull() }) it("does not show clear button when input is empty", async () => { renderInput({ label: "Email", value: "", onChange: vi.fn(), showClearContentIcon: true, }) expect(await screen.findByRole("textbox")).toBeTruthy() expect(screen.queryByLabelText("Clear content")).toBeNull() }) it("shows clear button when input has value and showClearContentIcon is true", async () => { renderInput({ label: "Email", value: "test", onChange: vi.fn(), showClearContentIcon: true, }) expect(await screen.findByLabelText("Clear content")).toBeTruthy() }) }) describe("icons", () => { it("renders left icon when provided", async () => { renderInput({ label: "Search", // eslint-disable-next-line formatjs/no-literal-string-in-jsx leftIcon: 🔍, }) expect(await screen.findByTestId("left-icon")).toBeTruthy() }) it("renders right icon when provided", () => { renderInput({ label: "Password", // eslint-disable-next-line formatjs/no-literal-string-in-jsx rightIcon: 👁, }) expect(screen.getByTestId("right-icon")).toBeTruthy() }) it("hides right icon when clear button is shown", async () => { renderInput({ label: "Email", value: "test", onChange: vi.fn(), showClearContentIcon: true, // eslint-disable-next-line formatjs/no-literal-string-in-jsx rightIcon: 👁, }) expect(await screen.findByLabelText("Clear content")).toBeTruthy() expect(screen.queryByTestId("right-icon")).toBeNull() }) it("shows right icon when clear button condition not met", () => { renderInput({ label: "Email", value: "", onChange: vi.fn(), showClearContentIcon: true, // eslint-disable-next-line formatjs/no-literal-string-in-jsx rightIcon: 👁, }) expect(screen.getByTestId("right-icon")).toBeTruthy() }) }) describe("controlled input", () => { it("displays the controlled value", () => { renderInput({ label: "Email", value: "test@example.com", onChange: vi.fn(), }) expect(screen.getByRole("textbox")).toHaveProperty( "value", "test@example.com" ) }) it("calls onChange when typing", async () => { const onChange = vi.fn() renderInput({ label: "Email", value: "", onChange }) const input = screen.getByRole("textbox") await userEvent.type(input, "a") expect(onChange).toHaveBeenCalled() }) it("does not change value without onChange updating it", () => { const onChange = vi.fn() renderInput({ label: "Email", value: "initial", onChange }) const input = screen.getByRole("textbox") fireEvent.change(input, { target: { value: "changed" } }) // Value stays the same because it's controlled expect(input).toHaveProperty("value", "initial") }) }) describe("ref forwarding", () => { it("forwards ref to the input element", () => { const ref = { current: null as HTMLInputElement | null } render( ) expect(ref.current).toBeInstanceOf(HTMLInputElement) }) it("allows focusing via ref", () => { const ref = { current: null as HTMLInputElement | null } render( ) ref.current?.focus() expect(document.activeElement).toBe(ref.current) }) }) })