import "@testing-library/jest-dom";
import React from "react";
import { Navigation } from "./index";
import { NavigationProps } from "./types";
import { fireEvent, render, screen } from "@testing-library/react";
jest.mock("./desktop-link-groups.tsx", () => ({
DesktopLinkGroups: ({ anchorName, link }: any) => (
{link?.title || link?.buttonLabel}
),
}));
jest.mock("./mobile-link-groups.tsx", () => ({
MobileLinkGroups: ({ link }: any) => (
{link?.title || link?.buttonLabel}
),
}));
jest.mock("@shared/components/call-button", () => ({
CallButton: ({ children, href, onClick, className }: any) => (
{children}
),
}));
jest.mock("@shared/components/input", () => ({
// eslint-disable-next-line react/display-name
Input: React.forwardRef(
(
{
name,
value,
onChange,
placeholder,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
className,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
containerClassName,
}: any,
ref: any
) => (
)
),
}));
jest.mock("@shared/components/link", () => ({
Link: ({ children, href, className, onClick, ...rest }: any) => (
{children}
),
}));
jest.mock("@shared/components/material-icon", () => ({
MaterialIcon: ({ name }: any) => (
{name}
),
}));
jest.mock("@shared/components/next-image", () => ({
// eslint-disable-next-line @typescript-eslint/no-unused-vars
NextImage: ({ src, alt, onClick, ...rest }: any) => (
),
}));
jest.mock("@shared/components/text", () => ({
Text: ({ as: Tag = "span", children, className }: any) => (
{children}
),
}));
jest.mock("@shared/contentful/blocks/button", () => ({
Button: ({
children,
buttonClassName,
onClick,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
showButtonAs,
linkClassName,
...rest
}: any) => (
),
__esModule: true,
}));
jest.mock("@shared/utils", () => ({
cx: (...args: any[]) => args.filter(Boolean).join(" "),
}));
const defaultProps: NavigationProps = {
primaryNavigationLinks: [],
utilityNavigationLinks: [],
primaryNavigationLogo: "https://logo.test/logo.png",
accountNavigationLinks: [],
supportNavigationLinks: [],
searchBarIcon: "https://icon.test/search.png",
invocaPhoneNumberLink: "tel:1234567890",
invocaPhoneNumberDisplayText: "123-456-7890",
callNowCtaText: "Call Now",
displayCartIcon: false,
cartHref: "/cart",
cartHasRetention: false,
cartIconAriaLabel: "Cart",
onSearch: jest.fn(),
};
// Props that include primaryNavigationLinks so MobileMenu renders
const mobileMenuProps: NavigationProps = {
...defaultProps,
primaryNavigationLinks: [
{ buttonLabel: "Products", href: "/products" },
] as any,
};
describe("Navigation", () => {
beforeEach(() => jest.clearAllMocks());
describe("Basic rendering", () => {
it("renders nav with utility and main nav sections", () => {
const { container } = render();
expect(container.querySelector(".menu-container")).toBeInTheDocument();
expect(container.querySelector(".utility-container")).toBeInTheDocument();
expect(
container.querySelector(".desktop-nav-section")
).toBeInTheDocument();
expect(
container.querySelector(".mobile-nav-section")
).toBeInTheDocument();
});
it("renders logo with string src", () => {
render();
const logos = screen.getAllByAltText("Kinetic business logo");
expect(logos[0]).toHaveAttribute("src", "https://logo.test/logo.png");
});
it("renders logo with object src (Asset)", () => {
render(
);
const logos = screen.getAllByAltText("Kinetic business logo");
expect(logos[0]).toHaveAttribute("src", "https://logo.test/obj.png");
});
it("does not render logo when logo url is empty", () => {
const { container } = render(
);
// logoUrl is "" so the conditional renders null
expect(
container.querySelector("[alt='Kinetic business logo']")
).not.toBeInTheDocument();
});
it("renders phone number display text", () => {
render();
expect(screen.getAllByText("123-456-7890").length).toBeGreaterThan(0);
});
});
describe("Utility navigation links", () => {
it("renders utility nav links", () => {
const links = [
{ buttonLabel: "Residential", href: "/residential" },
{ buttonLabel: "Business", href: "/business" },
];
render(
);
const buttons = screen.getAllByTestId("contentful-button");
expect(buttons.length).toBeGreaterThanOrEqual(2);
});
it("applies active class to second link by default (no utilityNavActiveIndex)", () => {
const links = [
{ buttonLabel: "Residential", href: "/residential" },
{ buttonLabel: "Business", href: "/business" },
];
const { container } = render(
);
const utilityButtons = container.querySelectorAll(
".utility-container button"
);
expect(utilityButtons[1]?.className).toContain("label4");
});
it("applies active class based on utilityNavActiveIndex", () => {
const links = [
{ buttonLabel: "Residential", href: "/residential" },
{ buttonLabel: "Business", href: "/business" },
];
const { container } = render(
);
const utilityButtons = container.querySelectorAll(
".utility-container button"
);
expect(utilityButtons[0]?.className).toContain("label4");
});
});
describe("Cart icon", () => {
it("renders cart icon when displayCartIcon is true", () => {
render();
expect(
screen.getAllByTestId("icon-shopping_cart").length
).toBeGreaterThan(0);
});
it("shows retention dot when cartHasRetention is true", () => {
const { container } = render(
);
expect(container.querySelector(".bg-icon-brand")).toBeInTheDocument();
});
it("does not render cart icon when displayCartIcon is false", () => {
render();
expect(
screen.queryByTestId("icon-shopping_cart")
).not.toBeInTheDocument();
});
it("calls onCartClick when cart is clicked", () => {
const onCartClick = jest.fn();
render(
);
const cartLinks = screen.getAllByLabelText("Cart");
fireEvent.click(cartLinks[0]);
expect(onCartClick).toHaveBeenCalled();
});
});
describe("Primary navigation links", () => {
it("renders DesktopLinkGroups for primary nav links", () => {
const links = [{ title: "Products", items: { items: [] } }];
render(
);
expect(
screen.getByTestId("desktop-link-group-main-menu-0")
).toBeInTheDocument();
});
});
describe("Account navigation links", () => {
it("renders DesktopLinkGroups for account links", () => {
const links = [{ title: "My Account", items: { items: [] } }];
render(
);
expect(
screen.getByTestId("desktop-link-group-my-account-0")
).toBeInTheDocument();
});
});
describe("Support navigation links", () => {
it("renders DesktopLinkGroups for support links", () => {
const links = [{ title: "Support", items: { items: [] } }];
render(
);
expect(
screen.getByTestId("desktop-link-group-support-menu-0")
).toBeInTheDocument();
});
});
describe("Search", () => {
it("renders desktop search input", () => {
render();
expect(
screen.getAllByPlaceholderText("Search...").length
).toBeGreaterThan(0);
});
it("calls onSearch when desktop search form is submitted", () => {
const onSearch = jest.fn();
const { container } = render(
);
const desktopForm = container.querySelector(".desktop-nav-section form");
const input = desktopForm?.querySelector("input");
fireEvent.change(input!, { target: { value: "test query" } });
fireEvent.submit(desktopForm!);
expect(onSearch).toHaveBeenCalledWith("test query");
});
it("calls onSearch when search icon is clicked", () => {
const onSearch = jest.fn();
const { container } = render(
);
const desktopForm = container.querySelector(".desktop-nav-section form");
const input = desktopForm?.querySelector("input");
fireEvent.change(input!, { target: { value: "click search" } });
const searchIcon = desktopForm?.querySelector("img");
fireEvent.click(searchIcon!);
expect(onSearch).toHaveBeenCalledWith("click search");
});
it("uses searchBarIcon url from object", () => {
render(
);
const imgs = screen.getAllByAltText("Search icon");
expect(imgs[0]).toHaveAttribute("src", "https://icon.test/obj.png");
});
});
describe("Mobile menu", () => {
it("does not render mobile menu when no nav links provided", () => {
const { container } = render();
const menuButton = container.querySelector(
".mobile-nav-section [data-testid='icon-menu']"
);
expect(menuButton).not.toBeInTheDocument();
});
it("opens mobile menu on hamburger click", () => {
const { container } = render();
const menuButton = container
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
?.closest("button");
fireEvent.click(menuButton!);
expect(
container.querySelector("#mobile-menu-overlay")
).toBeInTheDocument();
});
it("closes mobile menu on close button click", () => {
const { container } = render();
const menuButton = container
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
?.closest("button");
fireEvent.click(menuButton!);
const closeButton = container
.querySelector("#drawer-items [data-testid='icon-close']")
?.closest("button");
fireEvent.click(closeButton!);
expect(
container.querySelector("#mobile-menu-overlay")?.className
).toContain("-right-96");
});
it("renders mobile search input and submits", () => {
const onSearch = jest.fn();
const { container } = render(
);
const menuButton = container
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
?.closest("button");
fireEvent.click(menuButton!);
const mobileForm = container.querySelector("#drawer-items form");
const input = mobileForm?.querySelector("input");
fireEvent.change(input!, { target: { value: "mobile search" } });
fireEvent.submit(mobileForm!);
expect(onSearch).toHaveBeenCalledWith("mobile search");
});
it("closes menu when a link is clicked in the drawer", () => {
const links = [{ buttonLabel: "Link1", href: "/link1" }];
const { container } = render(
);
const menuButton = container
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
?.closest("button");
fireEvent.click(menuButton!);
expect(
container.querySelector("#mobile-menu-overlay")?.className
).toContain("right-0");
expect(
container.querySelector("#mobile-menu-overlay")?.className
).not.toContain("-right-96");
});
it("renders MobileLinkGroups in drawer", () => {
const links = [{ title: "Products", items: { items: [] } }];
const { container } = render(
);
const menuButton = container
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
?.closest("button");
fireEvent.click(menuButton!);
expect(screen.getAllByTestId("mobile-link-group").length).toBeGreaterThan(
0
);
});
it("closes menu when an anchor inside the drawer content is clicked", () => {
const links = [{ buttonLabel: "Internet", href: "/internet" }];
const { container } = render(
);
const menuButton = container
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
?.closest("button");
fireEvent.click(menuButton!);
expect(
container.querySelector("#mobile-menu-overlay")?.className
).toContain("right-0");
const scrollDiv = container.querySelector(
"#drawer-items .flex-grow.overflow-y-auto"
);
const anchor = document.createElement("a");
anchor.href = "/internet";
scrollDiv!.appendChild(anchor);
fireEvent.click(anchor);
expect(
container.querySelector("#mobile-menu-overlay")?.className
).toContain("-right-96");
});
it("does not close menu when non-anchor element is clicked in drawer", () => {
const links = [{ title: "Products", items: { items: [] } }];
const { container } = render(
);
const menuButton = container
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
?.closest("button");
fireEvent.click(menuButton!);
const scrollDiv = container.querySelector(
"#drawer-items .flex-grow.overflow-y-auto"
);
fireEvent.click(scrollDiv!);
expect(
container.querySelector("#mobile-menu-overlay")?.className
).toContain("right-0");
expect(
container.querySelector("#mobile-menu-overlay")?.className
).not.toContain("-right-96");
});
it("traps focus with Tab key - wraps from last to first", () => {
const links = [{ buttonLabel: "Link1", href: "/link1" }];
const { container } = render(
);
const menuButton = container
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
?.closest("button");
fireEvent.click(menuButton!);
const drawerItems = container.querySelector("#drawer-items");
const focusableEls = drawerItems!.querySelectorAll(".focus-item");
const firstEl = focusableEls[0] as HTMLElement;
const lastEl = focusableEls[focusableEls.length - 1] as HTMLElement;
lastEl.focus();
expect(document.activeElement).toBe(lastEl);
fireEvent.keyDown(window, { key: "Tab", keyCode: 9, shiftKey: false });
expect(document.activeElement).toBe(firstEl);
});
it("traps focus with Shift+Tab - wraps from first to last", () => {
const links = [{ buttonLabel: "Link1", href: "/link1" }];
const { container } = render(
);
const menuButton = container
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
?.closest("button");
fireEvent.click(menuButton!);
const drawerItems = container.querySelector("#drawer-items");
const focusableEls = drawerItems!.querySelectorAll(".focus-item");
const firstEl = focusableEls[0] as HTMLElement;
const lastEl = focusableEls[focusableEls.length - 1] as HTMLElement;
firstEl.focus();
expect(document.activeElement).toBe(firstEl);
fireEvent.keyDown(window, { key: "Tab", keyCode: 9, shiftKey: true });
expect(document.activeElement).toBe(lastEl);
});
it("does not trap focus for non-Tab keys", () => {
const links = [{ buttonLabel: "Link1", href: "/link1" }];
const { container } = render(
);
const menuButton = container
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
?.closest("button");
fireEvent.click(menuButton!);
const drawerItems = container.querySelector("#drawer-items");
const focusableEls = drawerItems!.querySelectorAll(".focus-item");
const lastEl = focusableEls[focusableEls.length - 1] as HTMLElement;
lastEl.focus();
fireEvent.keyDown(window, { key: "Enter", keyCode: 13 });
expect(document.activeElement).toBe(lastEl);
});
it("hides mobile call button when hideMobileCallButton is true", () => {
const { container } = render(
);
const mobileGapDiv = container.querySelector(
".mobile-nav-section .flex.items-center.gap-6"
);
const directCallButtons = mobileGapDiv?.querySelectorAll(
":scope > [data-testid='call-button']"
);
expect(directCallButtons?.length || 0).toBe(0);
});
it("shows mobile call button by default", () => {
const { container } = render();
const mobileSection = container.querySelector(".mobile-nav-section");
const callButton = mobileSection?.querySelector(
"[data-testid='call-button']"
);
expect(callButton).toBeInTheDocument();
});
});
describe("checkPlansJSX", () => {
it("renders checkPlansJSX when provided", () => {
render(
Plans}
/>
);
expect(screen.getByTestId("check-plans")).toBeInTheDocument();
});
it("does not render checkPlansJSX when not provided", () => {
render();
expect(screen.queryByTestId("check-plans")).not.toBeInTheDocument();
});
});
describe("Logo dimensions", () => {
it("uses custom logo dimensions", () => {
render(
);
const logos = screen.getAllByAltText("Kinetic business logo");
expect(logos.length).toBeGreaterThan(0);
});
it("renders logo from object src in mobile section", () => {
render(
);
const logos = screen.getAllByAltText("Kinetic business logo");
expect(logos[0]).toHaveAttribute("src", "https://logo.test/m.png");
});
});
describe("Null/undefined optional arrays (branch coverage)", () => {
it("renders without crashing when utilityNavigationLinks is undefined", () => {
const { container } = render(
);
expect(container.querySelector(".menu-container")).toBeInTheDocument();
});
it("renders without crashing when primaryNavigationLinks is undefined", () => {
const { container } = render(
);
expect(
container.querySelector(".desktop-nav-section")
).toBeInTheDocument();
});
it("renders without crashing when accountNavigationLinks is undefined", () => {
const { container } = render(
);
expect(container.querySelector(".menu-container")).toBeInTheDocument();
});
it("renders without crashing when supportNavigationLinks is undefined", () => {
const { container } = render(
);
expect(container.querySelector(".menu-container")).toBeInTheDocument();
});
});
describe("Mobile cart icon", () => {
it("renders cart icon in mobile section when displayCartIcon is true", () => {
const { container } = render(
);
const mobileSection = container.querySelector(".mobile-nav-section");
expect(
mobileSection?.querySelector("[data-testid='icon-shopping_cart']")
).toBeInTheDocument();
});
it("shows retention dot on mobile cart when cartHasRetention is true", () => {
const { container } = render(
);
const mobileSection = container.querySelector(".mobile-nav-section");
expect(
mobileSection?.querySelector(".bg-icon-brand")
).toBeInTheDocument();
});
it("does not show retention dot on mobile when cartHasRetention is false", () => {
const { container } = render(
);
const mobileSection = container.querySelector(".mobile-nav-section");
const dots = mobileSection?.querySelectorAll(".bg-icon-brand");
expect(dots?.length || 0).toBe(0);
});
it("uses fallback '#' when cartHref is undefined", () => {
const { container } = render(
);
const cartLinks = container.querySelectorAll("[aria-label='Cart']");
cartLinks.forEach(link => {
expect(link).toHaveAttribute("href", "#");
});
});
});
describe("Mobile drawer utility links", () => {
it("renders utility links in mobile drawer", () => {
const links = [
{ buttonLabel: "Residential", href: "/residential", anchorId: "r1" },
{ buttonLabel: "Business", href: "/business", anchorId: "b1" },
];
const { container } = render(
);
const menuButton = container
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
?.closest("button");
fireEvent.click(menuButton!);
const drawerButtons = container.querySelectorAll(
"#drawer-items .bg-bg-fill-info button"
);
expect(drawerButtons.length).toBe(2);
});
it("applies active class to utility links in drawer using utilityNavActiveIndex", () => {
const links = [
{ buttonLabel: "Residential", href: "/residential", anchorId: "r1" },
{ buttonLabel: "Business", href: "/business", anchorId: "b1" },
];
const { container } = render(
);
const menuButton = container
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
?.closest("button");
fireEvent.click(menuButton!);
const drawerButtons = container.querySelectorAll(
"#drawer-items .bg-bg-fill-info button"
);
expect(drawerButtons[0]?.className).toContain("label4");
});
it("handles undefined utilityNavigationLinks in mobile drawer", () => {
const { container } = render(
);
const menuButton = container
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
?.closest("button");
fireEvent.click(menuButton!);
const drawerButtons = container.querySelectorAll(
"#drawer-items .bg-bg-fill-info button"
);
expect(drawerButtons.length).toBe(0);
});
});
describe("Mobile search with object searchBarIcon", () => {
it("uses searchBarIcon url from object in mobile menu", () => {
const { container } = render(
);
const menuButton = container
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
?.closest("button");
fireEvent.click(menuButton!);
const mobileForm = container.querySelector("#drawer-items form");
const img = mobileForm?.querySelector("img");
expect(img).toHaveAttribute("src", "https://icon.test/mobile.png");
});
});
describe("Focus trap edge cases", () => {
it("does not wrap focus when Tab pressed and not on last element", () => {
const links = [{ buttonLabel: "Link1", href: "/link1" }];
const { container } = render(
);
const menuButton = container
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
?.closest("button");
fireEvent.click(menuButton!);
const drawerItems = container.querySelector("#drawer-items");
const focusableEls = drawerItems!.querySelectorAll(".focus-item");
const firstEl = focusableEls[0] as HTMLElement;
firstEl.focus();
fireEvent.keyDown(window, { key: "Tab", keyCode: 9, shiftKey: false });
expect(document.activeElement).toBe(firstEl);
});
it("does not wrap focus when Shift+Tab pressed and not on first element", () => {
const links = [{ buttonLabel: "Link1", href: "/link1" }];
const { container } = render(
);
const menuButton = container
.querySelector(".mobile-nav-section [data-testid='icon-menu']")
?.closest("button");
fireEvent.click(menuButton!);
const drawerItems = container.querySelector("#drawer-items");
const focusableEls = drawerItems!.querySelectorAll(".focus-item");
const lastEl = focusableEls[focusableEls.length - 1] as HTMLElement;
lastEl.focus();
fireEvent.keyDown(window, { key: "Tab", keyCode: 9, shiftKey: true });
expect(document.activeElement).toBe(lastEl);
});
});
describe("Navigation background color", () => {
it("applies bg-secondary-navy500 by default", () => {
const { container } = render();
const mainNav = container.querySelector(".main-nav-container");
expect(mainNav?.className).toContain("bg-secondary-navy500");
});
it("applies custom bg class with token format", () => {
const { container } = render(
);
const mainNav = container.querySelector(".main-nav-container");
expect(mainNav?.className).toContain("bg-secondary-green300");
});
it("passes through raw tailwind class", () => {
const { container } = render(
);
const mainNav = container.querySelector(".main-nav-container");
expect(mainNav?.className).toContain("bg-blue-500");
});
});
describe("Display options", () => {
it("hides utility navigation when displayUtilityNavigation is false", () => {
const { container } = render(
);
expect(
container.querySelector(".utility-container")
).not.toBeInTheDocument();
});
it("hides search bar when displaySearchBar is false", () => {
const { container } = render(
);
const desktopForm = container.querySelector(".desktop-nav-section form");
expect(desktopForm).not.toBeInTheDocument();
});
it("hides call now CTA when displayCallNowCta is false", () => {
const { container } = render(
);
const utilityCta = container.querySelector(
".utility-container [data-testid='call-button']"
);
expect(utilityCta).not.toBeInTheDocument();
});
it("renders main nav call CTA when showCallNowCtaInMainNav is true", () => {
render(
);
const ctaLink = screen
.getAllByTestId("link")
.find(el => el.getAttribute("aria-label") === "Call 123-456-7890");
expect(ctaLink).toBeInTheDocument();
});
it("renders main nav call CTA on mobile when showMobileSliderMenu is false", () => {
render(
);
const ctaLink = screen
.getAllByTestId("link")
.find(el => el.getAttribute("aria-label") === "Call 123-456-7890");
expect(ctaLink).toBeInTheDocument();
});
});
});