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();
});
});
});