import { render, screen } from "@testing-library/react"; import { describe, it, expect } from "vitest"; import { TagField } from "./TagField"; describe("TagField - 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("list")).toBeInTheDocument(); }); it("filters out tags with showWhen=false", () => { render( ); expect(screen.getByText("Visible")).toBeInTheDocument(); expect(screen.queryByText("Hidden")).not.toBeInTheDocument(); }); it("filters out tags with empty text", () => { render(); expect(screen.getAllByRole("listitem")).toHaveLength(1); }); it("does not render the tag list container when there are no visible tags", () => { render(); expect(screen.queryByRole("list")).not.toBeInTheDocument(); }); }); describe("TagField - rendering", () => { it("renders all tag text", () => { render( ); expect(screen.getByText("Active")).toBeInTheDocument(); expect(screen.getByText("Approved")).toBeInTheDocument(); expect(screen.getByText("Rejected")).toBeInTheDocument(); }); it("renders the correct number of listitem elements", () => { render(); expect(screen.getAllByRole("listitem")).toHaveLength(2); }); it("renders label text when provided", () => { render(); expect(screen.getByText("Status")).toBeInTheDocument(); }); it("renders as an anchor when link is provided", () => { render(); const item = screen.getByText("Active").closest("a"); expect(item).toHaveAttribute("href", "/status"); }); it("renders as a span when no link is provided", () => { render(); expect(screen.queryByRole("link")).not.toBeInTheDocument(); }); it("applies tooltip as title and aria-label", () => { render(); const item = screen.getByText("Active").closest('[role="listitem"]'); expect(item).toHaveAttribute("title", "Currently active"); expect(item).toHaveAttribute("aria-label", "Currently active"); }); }); describe("TagField - size", () => { it("defaults to STANDARD size classes", () => { render(); const item = screen.getByText("Active").closest('[role="listitem"]'); expect(item).toHaveClass("text-base"); }); it("applies SMALL size classes", () => { render(); const item = screen.getByText("Active").closest('[role="listitem"]'); expect(item).toHaveClass("text-xs"); }); it("applies LARGE size classes", () => { render(); const item = screen.getByText("Active").closest('[role="listitem"]'); expect(item).toHaveClass("text-xl"); }); }); describe("TagField - icons", () => { it("renders an icon when icon is provided", () => { const { container } = render( ); expect(container.querySelector("svg")).toBeInTheDocument(); }); it("does not render an icon when icon is omitted", () => { const { container } = render(); expect(container.querySelector("svg")).not.toBeInTheDocument(); }); it("marks the icon as aria-hidden so it isn't announced twice", () => { const { container } = render( ); const svg = container.querySelector("svg"); expect(svg).toHaveAttribute("aria-hidden", "true"); }); it("places the icon before the text by default (iconPosition START)", () => { const { container } = render( ); const item = container.querySelector('[role="listitem"]'); const firstChild = item?.firstElementChild; expect(firstChild?.tagName.toLowerCase()).toBe("svg"); }); it("places the icon after the text when iconPosition is END", () => { const { container } = render( ); const item = container.querySelector('[role="listitem"]'); const lastChild = item?.lastElementChild; expect(lastChild?.tagName.toLowerCase()).toBe("svg"); }); it("icon inherits the tag's text color (no separate icon color prop)", () => { const { container } = render( ); const item = container.querySelector('[role="listitem"]'); const svg = container.querySelector("svg"); // The icon has no explicit color class/style — it inherits `currentColor` // from the text color class applied to the parent tag element. expect(item).toHaveClass("text-red-700"); expect(svg).not.toHaveAttribute("style"); }); it("scales icon size up for LARGE tags", () => { const { container: small } = render( ); const { container: large } = render( ); const smallSvg = small.querySelector("svg"); const largeSvg = large.querySelector("svg"); const smallWidth = Number(smallSvg?.getAttribute("width")); const largeWidth = Number(largeSvg?.getAttribute("width")); expect(largeWidth).toBeGreaterThan(smallWidth); }); it("warns and skips rendering when icon name is not a valid Lucide icon", () => { const { container } = render( ); expect(container.querySelector("svg")).not.toBeInTheDocument(); expect(screen.getByText("Active")).toBeInTheDocument(); }); }); describe("TagField - icon-adjacent padding", () => { it("does not apply inline padding overrides when there is no icon", () => { render(); const item = screen.getByText("Active").closest('[role="listitem"]') as HTMLElement; expect(item.style.paddingLeft).toBe(""); expect(item.style.paddingRight).toBe(""); }); it("tightens left padding when icon is at START (default)", () => { render(); const item = screen.getByText("Active").closest('[role="listitem"]') as HTMLElement; // STANDARD base px-4 (16px) tightens to 14px on the icon side only expect(item.style.paddingLeft).toBe("0.875rem"); expect(item.style.paddingRight).toBe(""); }); it("tightens right padding when icon is at END", () => { render( ); const item = screen.getByText("Active").closest('[role="listitem"]') as HTMLElement; expect(item.style.paddingRight).toBe("0.875rem"); expect(item.style.paddingLeft).toBe(""); }); it("scales the tightened padding value per tag size", () => { const { container: small } = render( ); const { container: large } = render( ); const smallItem = small.querySelector('[role="listitem"]') as HTMLElement; const largeItem = large.querySelector('[role="listitem"]') as HTMLElement; expect(smallItem.style.paddingLeft).toBe("0.375rem"); // SMALL px-2 (8px) → 6px expect(largeItem.style.paddingLeft).toBe("1.125rem"); // LARGE px-5 (20px) → 18px }); }); describe("TagField - accessibility", () => { it("renders the tag list with role='list' and items with role='listitem'", () => { render(); expect(screen.getByRole("list")).toBeInTheDocument(); expect(screen.getAllByRole("listitem")).toHaveLength(2); }); it("applies accessibilityText as aria-label on the root", () => { render( ); expect(screen.getByLabelText("Status tags")).toBeInTheDocument(); }); it("tag text remains visible to screen readers even with an icon present", () => { render(); // The accessible name comes from the visible text node, not the hidden icon. expect(screen.getByText("Active")).toBeInTheDocument(); }); });