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) => ( {alt} ), })); 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(); }); }); });