import { describe, it, expect, vi } from "vitest"; import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import React from "react"; import Link from "./link"; describe("Link Component", () => { describe("Rendering", () => { it("should render with href and children", () => { render(About Us); const link = screen.getByRole("link"); expect(link).toBeInTheDocument(); expect(link).toHaveTextContent("About Us"); expect(link).toHaveAttribute("href", "/about"); }); it("should render as an anchor element", () => { render(Link); const link = screen.getByRole("link"); expect(link.tagName).toBe("A"); }); it("should render with custom classes via UI component", () => { render( Test ); const link = screen.getByRole("link"); expect(link).toHaveClass("custom-link-class"); }); it("should apply custom styles via styles prop", () => { render( Styled Link ); const link = screen.getByRole("link"); const styleAttr = link.getAttribute("style") || ""; expect(styleAttr).toContain("color"); }); it("should render children correctly", () => { render( Child Content ); expect(screen.getByTestId("child-element")).toBeInTheDocument(); expect(screen.getByText("Child Content")).toBeInTheDocument(); }); }); describe("Target Attribute", () => { it("should render with target attribute", () => { render( External ); const link = screen.getByRole("link"); expect(link).toHaveAttribute("target", "_blank"); }); it("should work with different target values", () => { const { rerender } = render( Self ); let link = screen.getByRole("link"); expect(link).toHaveAttribute("target", "_self"); rerender( Parent ); link = screen.getByRole("link"); expect(link).toHaveAttribute("target", "_parent"); }); }); describe("Security (rel attribute)", () => { it("should automatically add security attributes for target=_blank", () => { render( External Link ); const link = screen.getByRole("link"); const rel = link.getAttribute("rel"); expect(rel).toContain("noopener"); expect(rel).toContain("noreferrer"); }); it("should merge custom rel with security defaults for target=_blank", () => { render( External Link ); const link = screen.getByRole("link"); const rel = link.getAttribute("rel"); // Should have security defaults expect(rel).toContain("noopener"); expect(rel).toContain("noreferrer"); // Should also have custom values expect(rel).toContain("nofollow"); expect(rel).toContain("author"); }); it("should use provided rel as-is when target is not _blank", () => { render( Internal Link ); const link = screen.getByRole("link"); expect(link).toHaveAttribute("rel", "author"); }); it("should not add security attributes for internal links", () => { render(About); const link = screen.getByRole("link"); expect(link).not.toHaveAttribute("rel"); }); it("should handle empty rel attribute gracefully", () => { render( External ); const link = screen.getByRole("link"); const rel = link.getAttribute("rel"); expect(rel).toContain("noopener"); expect(rel).toContain("noreferrer"); }); it("should not duplicate rel tokens when merging", () => { render( External ); const link = screen.getByRole("link"); const rel = link.getAttribute("rel") || ""; const tokens = rel.split(/\s+/); // Check for duplicates const uniqueTokens = new Set(tokens); expect(tokens.length).toBe(uniqueTokens.size); // Should have all three: noopener, noreferrer, nofollow expect(uniqueTokens.has("noopener")).toBe(true); expect(uniqueTokens.has("noreferrer")).toBe(true); expect(uniqueTokens.has("nofollow")).toBe(true); }); }); describe("Prefetch", () => { it("should add prefetch to rel when prefetch=true and target=_blank", () => { render( Prefetch Link ); const link = screen.getByRole("link"); const rel = link.getAttribute("rel"); expect(rel).toContain("noopener"); expect(rel).toContain("noreferrer"); expect(rel).toContain("prefetch"); }); it("should not add prefetch when prefetch=false", () => { render( No Prefetch ); const link = screen.getByRole("link"); const rel = link.getAttribute("rel"); expect(rel).not.toContain("prefetch"); }); it("should not add prefetch for internal links", () => { render( Internal ); const link = screen.getByRole("link"); const rel = link.getAttribute("rel"); // Internal links don't get prefetch (only external with target="_blank") expect(rel).toBeNull(); }); }); describe("Button Styling", () => { it("should apply data-btn attribute when btnStyle is provided", () => { render( Button Link ); const link = screen.getByRole("link"); expect(link).toHaveAttribute("data-btn", "primary"); }); it("should render button-styled link with wrapper", () => { render( Button Text ); const link = screen.getByRole("link"); const bold = link.querySelector("b"); expect(bold).toBeInTheDocument(); expect(bold).toHaveTextContent("Button Text"); }); it("should render pill-styled link with wrapper", () => { render( Pill Text ); const link = screen.getByRole("link"); const italic = link.querySelector("i"); expect(italic).toBeInTheDocument(); expect(italic).toHaveTextContent("Pill Text"); }); }); describe("Event Handlers", () => { it("should call onClick when link is clicked", async () => { const user = userEvent.setup(); const handleClick = vi.fn(); render( Click Me ); const link = screen.getByRole("link"); await user.click(link); expect(handleClick).toHaveBeenCalledTimes(1); expect(handleClick).toHaveBeenCalledWith( expect.objectContaining({ type: "click", }) ); }); it("should call onClick when activated with keyboard (Enter)", async () => { const user = userEvent.setup(); const handleClick = vi.fn(); render( Click Me ); const link = screen.getByRole("link"); // Focus the link link.focus(); expect(link).toHaveFocus(); // Press Enter to activate await user.keyboard("{Enter}"); // onClick should be called for keyboard activation expect(handleClick).toHaveBeenCalled(); }); it("should call onPointerDown when link is clicked", async () => { const user = userEvent.setup(); const handlePointerDown = vi.fn(); render( Click Me ); const link = screen.getByRole("link"); await user.click(link); expect(handlePointerDown).toHaveBeenCalledTimes(1); expect(handlePointerDown).toHaveBeenCalledWith( expect.objectContaining({ type: "pointerdown", }) ); }); it("should call both onClick and onPointerDown when provided", async () => { const user = userEvent.setup(); const handleClick = vi.fn(); const handlePointerDown = vi.fn(); render( Click Me ); const link = screen.getByRole("link"); await user.click(link); // Both handlers should be called expect(handleClick).toHaveBeenCalled(); expect(handlePointerDown).toHaveBeenCalled(); }); it("should NOT call onPointerDown on keyboard activation (Enter)", async () => { const user = userEvent.setup(); const handlePointerDown = vi.fn(); render( Click Me ); const link = screen.getByRole("link"); link.focus(); // Press Enter await user.keyboard("{Enter}"); // onPointerDown should NOT be called (only pointer events trigger it) expect(handlePointerDown).not.toHaveBeenCalled(); }); it("should not throw error when onClick is not provided", async () => { const user = userEvent.setup(); render(Click Me); const link = screen.getByRole("link"); await expect(user.click(link)).resolves.not.toThrow(); }); it("should not throw error when onPointerDown is not provided", async () => { const user = userEvent.setup(); render(Click Me); const link = screen.getByRole("link"); await expect(user.click(link)).resolves.not.toThrow(); }); }); describe("Ref Forwarding", () => { it("should forward ref to the anchor element", () => { const ref = React.createRef(); render( Test Link ); expect(ref.current).toBeInstanceOf(HTMLAnchorElement); expect(ref.current?.tagName).toBe("A"); expect(ref.current?.href).toContain("/test"); }); it("should allow programmatic focus via ref", () => { const ref = React.createRef(); render( Focusable Link ); ref.current?.focus(); expect(ref.current).toHaveFocus(); }); }); describe("Accessibility", () => { it("should render with proper role", () => { render(Accessible Link); const link = screen.getByRole("link"); expect(link).toBeInTheDocument(); expect(link).toBeVisible(); }); it("should render external links accessibly", () => { render( External Link ); const link = screen.getByRole("link"); expect(link).toBeInTheDocument(); expect(link).toHaveAttribute("target", "_blank"); // Security attributes present const rel = link.getAttribute("rel"); expect(rel).toContain("noopener"); expect(rel).toContain("noreferrer"); }); it("should render button-styled links with semantic anchor", () => { render( Call to Action ); const link = screen.getByRole("link"); expect(link.tagName).toBe("A"); expect(link).toHaveAttribute("href", "/action"); }); it("should support aria-label for icon-only links", () => { render( ); const link = screen.getByRole("link"); expect(link).toHaveAccessibleName("Open settings"); // Verify SVG is hidden from screen readers const svg = link.querySelector("svg"); expect(svg).toHaveAttribute("aria-hidden", "true"); }); it("should be keyboard accessible", async () => { const user = userEvent.setup(); render(Keyboard Link); const link = screen.getByRole("link"); // Tab to link await user.tab(); expect(link).toHaveFocus(); // Press Enter should work (default browser behavior) // We just verify focus worked expect(link).toHaveFocus(); }); it("should have accessible name from text content", () => { render(Read installation guide); const link = screen.getByRole("link"); expect(link).toHaveAccessibleName("Read installation guide"); }); it("should support aria-describedby for additional context", () => { render( <> Opens in a new window External Resource ); const link = screen.getByRole("link"); expect(link).toHaveAttribute("aria-describedby", "link-description"); }); }); describe("URL Schemes", () => { it("should support mailto: links", () => { render(Email Us); const link = screen.getByRole("link"); expect(link).toHaveAttribute("href", "mailto:test@example.com"); }); it("should support tel: links", () => { render(Call Us); const link = screen.getByRole("link"); expect(link).toHaveAttribute("href", "tel:+1234567890"); }); it("should support hash/anchor links", () => { render(Jump to Section); const link = screen.getByRole("link"); expect(link).toHaveAttribute("href", "#section-1"); }); it("should support relative paths", () => { render(Go to Parent); const link = screen.getByRole("link"); expect(link).toHaveAttribute("href", "../parent"); }); it("should support absolute paths", () => { render(Absolute Path); const link = screen.getByRole("link"); expect(link).toHaveAttribute("href", "/absolute/path"); }); }); describe("Display Name", () => { it("should have correct displayName for debugging", () => { expect(Link.displayName).toBe("Link"); }); }); describe("Props Spreading", () => { it("should spread additional HTML attributes", () => { render( Test ); const link = screen.getByTestId("custom-link"); expect(link).toHaveAttribute("id", "link-123"); }); it("should support title attribute", () => { render( Hover Me ); const link = screen.getByRole("link"); expect(link).toHaveAttribute("title", "Additional information"); }); }); describe("Edge Cases", () => { it("should handle missing href gracefully", () => { // href is optional in the type, testing edge case behavior render(No href); // Should still render, though not a valid link const element = screen.getByText("No href"); expect(element).toBeInTheDocument(); }); it("should handle whitespace-only rel values", () => { render( External ); const link = screen.getByRole("link"); const rel = link.getAttribute("rel"); // Should still include security tokens expect(rel).toContain("noopener"); expect(rel).toContain("noreferrer"); }); it("should handle multiple whitespace between rel tokens", () => { render( External ); const link = screen.getByRole("link"); const rel = link.getAttribute("rel") || ""; // Split should handle multiple spaces expect(rel).toContain("noopener"); expect(rel).toContain("noreferrer"); expect(rel).toContain("nofollow"); expect(rel).toContain("author"); }); }); describe("Performance", () => { it("should memoize rel computation to avoid unnecessary recalculations", () => { const { rerender } = render( Link ); const link1 = screen.getByRole("link"); const rel1 = link1.getAttribute("rel"); // Rerender with same props rerender( Link ); const link2 = screen.getByRole("link"); const rel2 = link2.getAttribute("rel"); // Should produce same result expect(rel1).toBe(rel2); }); it("should update rel when dependencies change", () => { const { rerender } = render( Link ); const link1 = screen.getByRole("link"); const rel1 = link1.getAttribute("rel"); expect(rel1).not.toContain("prefetch"); // Rerender with prefetch=true rerender( Link ); const link2 = screen.getByRole("link"); const rel2 = link2.getAttribute("rel"); expect(rel2).toContain("prefetch"); }); }); });