import React from "react"; import { render, screen } from "@testing-library/react"; import { describe, it, expect, vi } from "vitest"; import { Breadcrumb, CustomRoute } from "./breadcrumb"; describe("Breadcrumb", () => { // ============================================================================ // CORE FUNCTIONALITY TESTS // ============================================================================ describe("Core Functionality", () => { it("renders a navigation element with breadcrumb list", () => { render(); const nav = screen.getByRole("navigation"); expect(nav).toBeInTheDocument(); const list = nav.querySelector("ol"); expect(list).toBeInTheDocument(); }); it("returns null when currentRoute is undefined", () => { const { container } = render(); expect(container.firstChild).toBeNull(); }); it("returns null when currentRoute is empty string", () => { const { container } = render(); expect(container.firstChild).toBeNull(); }); it("renders home/start route link correctly", () => { render(); const homeLink = screen.getByRole("link", { name: "Home" }); expect(homeLink).toBeInTheDocument(); expect(homeLink).toHaveAttribute("href", "/"); }); it("parses path segments correctly from currentRoute", () => { render(); expect(screen.getByText("products")).toBeInTheDocument(); expect(screen.getByText("shirts")).toBeInTheDocument(); expect(screen.getByText("blue-shirt")).toBeInTheDocument(); }); it("handles paths with leading slashes", () => { render(); expect(screen.getByText("products")).toBeInTheDocument(); }); it("handles paths with trailing slashes", () => { render(); expect(screen.getByText("products")).toBeInTheDocument(); expect(screen.getByText("shirts")).toBeInTheDocument(); }); it("handles encoded URI components correctly", () => { render(); expect(screen.getByText("products")).toBeInTheDocument(); // Text is truncated, so use aria-label to verify full decoded text const element = screen.getByText(/learning in pub/); expect(element).toHaveAttribute("aria-label", "learning in public"); }); }); // ============================================================================ // CUSTOM ROUTES TESTS // ============================================================================ describe("Custom Routes", () => { const customRoutes: CustomRoute[] = [ { path: "products", name: "All Products", url: "/products", }, { path: "shirts", name: "Shirts & Tops", url: "/products/shirts", }, ]; it("maps path segments to custom route names", () => { render( ); expect(screen.getByText("All Products")).toBeInTheDocument(); expect(screen.getByText("Shirts & Tops")).toBeInTheDocument(); }); it("uses custom URLs when provided", () => { render( ); const productsLink = screen.getByRole("link", { name: "All Products" }); expect(productsLink).toHaveAttribute("href", "/products"); const shirtsSpan = screen.getByText("Shirts & Tops"); expect(shirtsSpan).toBeInTheDocument(); }); it("falls back to path segment when no custom route found", () => { render( ); expect(screen.getByText("All Products")).toBeInTheDocument(); expect(screen.getByText("pants")).toBeInTheDocument(); // No custom route }); it("handles partial custom route mappings", () => { const partialRoutes: CustomRoute[] = [ { path: "products", name: "Products", url: "/products", }, ]; render( ); expect(screen.getByText("Products")).toBeInTheDocument(); expect(screen.getByText("shirts")).toBeInTheDocument(); expect(screen.getByText("item-123")).toBeInTheDocument(); }); }); // ============================================================================ // ACCESSIBILITY TESTS // ============================================================================ describe("Accessibility", () => { it("renders semantic nav element with proper aria-label", () => { render( ); const nav = screen.getByRole("navigation", { name: "Page navigation" }); expect(nav).toBeInTheDocument(); }); it("uses default aria-label of 'Breadcrumb' when not provided", () => { render(); const nav = screen.getByRole("navigation", { name: "Breadcrumb" }); expect(nav).toBeInTheDocument(); }); it("uses ordered list (ol) for breadcrumb list", () => { render(); const nav = screen.getByRole("navigation"); const list = nav.querySelector("ol"); expect(list).toBeInTheDocument(); }); it("marks last segment with aria-current='page'", () => { render(); const currentPage = screen.getByText("shirts"); expect(currentPage).toHaveAttribute("aria-current", "page"); }); it("hides spacers from screen readers with aria-hidden", () => { render(); const nav = screen.getByRole("navigation"); const spacers = nav.querySelectorAll('[aria-hidden="true"]'); // Should have at least 2 spacers (one for each segment) expect(spacers.length).toBeGreaterThan(0); }); it("does not render anchor tags with href='#'", () => { render(); const nav = screen.getByRole("navigation"); const invalidLinks = nav.querySelectorAll('a[href="#"]'); expect(invalidLinks.length).toBe(0); }); it("provides full text in aria-label when truncated", () => { const longName = "this-is-a-very-long-product-name"; render( ); const truncatedElement = screen.getByText(/this-is-a-/); expect(truncatedElement).toHaveAttribute("aria-label", longName); }); it("does not add aria-label when text is not truncated", () => { render( ); const element = screen.getByText("shirt"); expect(element).not.toHaveAttribute("aria-label"); }); }); // ============================================================================ // TRUNCATION TESTS // ============================================================================ describe("Text Truncation", () => { it("truncates text beyond default truncateLength (15)", () => { const longName = "verylongproductname"; render(); // Should be truncated to 15 chars + "..." expect(screen.getByText("verylongproduct...")).toBeInTheDocument(); }); it("respects custom truncateLength prop", () => { const longName = "verylongname"; render( ); expect(screen.getByText("veryl...")).toBeInTheDocument(); }); it("does not truncate text shorter than truncateLength", () => { render( ); expect(screen.getByText("shirt")).toBeInTheDocument(); }); it("truncates both intermediate and current page segments", () => { render( ); expect(screen.getByText("verylongfi...")).toBeInTheDocument(); expect(screen.getByText("verylongse...")).toBeInTheDocument(); }); }); // ============================================================================ // EDGE CASES TESTS // ============================================================================ describe("Edge Cases", () => { it("skips last segment if length <= 3 characters", () => { render(); // "abc" should be skipped (last segment with length 3) expect(screen.queryByText("abc")).not.toBeInTheDocument(); expect(screen.getByText("products")).toBeInTheDocument(); expect(screen.getByText("shirts")).toBeInTheDocument(); }); it("skips duplicate consecutive segments", () => { const { container } = render( ); const nav = container.querySelector("nav"); const productLinks = nav?.querySelectorAll('a[href="products"]'); // Should only have one "products" link (first occurrence) expect(productLinks?.length).toBeLessThanOrEqual(1); }); it("handles single segment path", () => { render(); expect(screen.getByRole("link", { name: "Home" })).toBeInTheDocument(); expect(screen.getByText("about")).toBeInTheDocument(); }); it("handles deep nesting (many segments)", () => { render( ); expect(screen.getByText("level1")).toBeInTheDocument(); expect(screen.getByText("level2")).toBeInTheDocument(); expect(screen.getByText("level3")).toBeInTheDocument(); expect(screen.getByText("level4")).toBeInTheDocument(); expect(screen.getByText("level5")).toBeInTheDocument(); }); it("handles special characters in segments", () => { render(); expect(screen.getByText("t-shirts")).toBeInTheDocument(); }); }); // ============================================================================ // INTEGRATION TESTS // ============================================================================ describe("Props Integration", () => { it("spreads linkProps to Link components", () => { const handleClick = vi.fn(); render( ); const links = screen.getAllByRole("link"); expect(links.length).toBeGreaterThan(0); // Click first link links[0].click(); expect(handleClick).toHaveBeenCalled(); }); it("uses custom startRoute and startRouteUrl", () => { render( ); const startLink = screen.getByRole("link", { name: "Dashboard" }); expect(startLink).toBeInTheDocument(); expect(startLink).toHaveAttribute("href", "/dashboard"); }); it("renders custom spacer element", () => { render( →} /> ); const spacers = screen.getAllByTestId("custom-spacer"); expect(spacers.length).toBeGreaterThan(0); expect(spacers[0]).toHaveTextContent("→"); }); it("applies custom id to nav element", () => { render( ); const nav = screen.getByRole("navigation"); expect(nav).toHaveAttribute("id", "custom-breadcrumb"); }); it("applies custom className via classes prop", () => { render( ); const nav = screen.getByRole("navigation"); expect(nav).toHaveClass("custom-breadcrumb-class"); }); it("applies custom inline styles", () => { render( ); const nav = screen.getByRole("navigation"); expect(nav).toHaveStyle({ padding: "1rem" }); }); }); // ============================================================================ // SUB-COMPONENT EXPORTS TESTS // ============================================================================ describe("Sub-component Exports", () => { it("exports Nav sub-component", () => { expect(Breadcrumb.Nav).toBeDefined(); }); it("exports List sub-component", () => { expect(Breadcrumb.List).toBeDefined(); }); it("exports Item sub-component", () => { expect(Breadcrumb.Item).toBeDefined(); }); it("allows custom composition with sub-components", () => { render( Home Current Page ); expect(screen.getByRole("navigation")).toBeInTheDocument(); expect(screen.getByText("Home")).toBeInTheDocument(); expect(screen.getByText("Current Page")).toBeInTheDocument(); }); }); // ============================================================================ // SNAPSHOT TESTS // ============================================================================ describe("Snapshot Tests", () => { it("matches snapshot for simple breadcrumb", () => { const { container } = render( ); expect(container).toMatchSnapshot(); }); it("matches snapshot with custom routes", () => { const routes: CustomRoute[] = [ { path: "products", name: "All Products", url: "/products" }, ]; const { container } = render( ); expect(container).toMatchSnapshot(); }); it("matches snapshot with truncation", () => { const { container } = render( ); expect(container).toMatchSnapshot(); }); }); });