import { act, fireEvent, render, screen, within } from "@testing-library/react"; import { usePathname } from "next/navigation"; import { Header } from "./Header"; /** * Testes de caminhos-base de Header (complemento de Header.test.tsx): a barra que escurece com a rolagem, * as teclas do menu do celular que não o fecham e a rota ausente (fora do roteador). */ jest.mock("next/navigation", () => ({ usePathname: jest.fn() })); jest.mock("next/link", () => ({ __esModule: true, default: ({ children, prefetch, ...props }: { children?: React.ReactNode; prefetch?: boolean | null } & Record< string, unknown >) => ( {children} ), })); function setScrollY(value: number) { Object.defineProperty(window, "scrollY", { value, configurable: true, writable: true, }); } const header = () => screen.getByRole("banner"); /** Rolada: borda visível e fundo a 95% (sem backdrop-blur: o filtro refazia a pintura a cada quadro da rolagem). */ const isDark = () => header().className.includes("border-white/12"); beforeEach(() => { jest.mocked(usePathname).mockReturnValue("/"); setScrollY(0); }); afterEach(() => { jest.restoreAllMocks(); setScrollY(0); }); describe("Header: rolagem", () => { it("path: page at the top -> transparent border and solid background", () => { render(
); expect(isDark()).toBe(false); expect(header()).toHaveClass("border-transparent"); }); it("path: scrolling down and back to the top -> bordered bar, then the plain one again, never with a backdrop blur", () => { render(
); setScrollY(120); act(() => { window.dispatchEvent(new Event("scroll")); }); expect(isDark()).toBe(true); expect(header()).not.toHaveClass("border-transparent"); expect(header().className).not.toContain("backdrop-blur"); // outro evento de rolagem sem mudar de lado: nada muda setScrollY(300); act(() => { window.dispatchEvent(new Event("scroll")); }); expect(isDark()).toBe(true); setScrollY(0); act(() => { window.dispatchEvent(new Event("scroll")); }); expect(isDark()).toBe(false); }); it("path: page already scrolled when the header mounts -> starts with the bordered bar", () => { setScrollY(500); render(
); expect(isDark()).toBe(true); }); it("path: unmount -> the scroll listener is removed", () => { const remove = jest.spyOn(window, "removeEventListener"); const { unmount } = render(
); unmount(); expect(remove).toHaveBeenCalledWith("scroll", expect.any(Function)); }); }); describe("Header: teclado no menu do celular", () => { it("path: a key other than Escape inside the open menu -> the menu stays open and the key is not blocked", () => { render(
); fireEvent.click(screen.getByRole("button", { name: "Abrir menu" })); const menu = screen.getByRole("navigation", { name: "Navegação mobile" }); // Tab entre dois controles do meio da lista: segue o caminho normal (o foco só é preso nas pontas) const middle = within(menu).getByRole("link", { name: "Serviços" }); act(() => middle.focus()); // fireEvent devolve false só quando o evento foi cancelado (preventDefault) expect(fireEvent.keyDown(middle, { key: "Tab" })).toBe(true); expect(fireEvent.keyDown(menu, { key: "Enter" })).toBe(true); expect(screen.getByRole("button", { name: "Fechar menu" })).toHaveAttribute( "aria-expanded", "true", ); expect( screen.getByRole("navigation", { name: "Navegação mobile" }), ).toBeInTheDocument(); }); it("path: Escape on the menu button while the menu is open -> closes it and keeps the focus on the button", () => { render(
); const button = screen.getByRole("button", { name: "Abrir menu" }); fireEvent.click(button); act(() => button.focus()); expect(fireEvent.keyDown(button, { key: "Escape" })).toBe(false); const closed = screen.getByRole("button", { name: "Abrir menu" }); expect(closed).toHaveAttribute("aria-expanded", "false"); expect(closed).toHaveFocus(); expect( screen.queryByRole("navigation", { name: "Navegação mobile" }), ).toBeNull(); }); it("path: Escape while the menu is already closed -> nothing opens", () => { render(
); const button = screen.getByRole("button", { name: "Abrir menu" }); fireEvent.keyDown(button, { key: "Escape" }); expect(button).toHaveAttribute("aria-expanded", "false"); expect( screen.queryByRole("navigation", { name: "Navegação mobile" }), ).toBeNull(); }); }); describe("Header: menu aberto, caminhos de borda do foco, do clique e da janela", () => { /** O cabeçalho com o conteúdo da página que o menu aberto torna inerte (id do layout). */ function Page() { return ( <>
); } const openMenu = () => fireEvent.click(screen.getByRole("button", { name: "Abrir menu" })); const content = () => document.getElementById("main-content") as HTMLElement; afterEach(() => { document.documentElement.style.removeProperty("overflow"); }); it("path: Tab with no control left to focus in the header -> the key is not blocked and the focus does not move", () => { render(); openMenu(); const banner = screen.getByRole("banner"); for (const button of banner.querySelectorAll("button")) button.setAttribute("disabled", ""); for (const link of banner.querySelectorAll("a")) link.removeAttribute("href"); act(() => document.body.focus()); expect(fireEvent.keyDown(document.body, { key: "Tab" })).toBe(true); expect( fireEvent.keyDown(document.body, { key: "Tab", shiftKey: true }), ).toBe(true); expect(document.body).toHaveFocus(); }); it("path: click whose target is not an element (the document itself) -> the page stays locked", () => { render(); openMenu(); fireEvent.click(document); expect(content()).toHaveAttribute("inert"); expect(document.documentElement.style.overflow).toBe("hidden"); }); it("path: click on something inside the header that is not a link -> the page stays locked", () => { render(); openMenu(); fireEvent.click( screen.getByRole("navigation", { name: "Navegação mobile" }), ); expect(content()).toHaveAttribute("inert"); expect(document.documentElement.style.overflow).toBe("hidden"); }); it("path: click on a link inside the header -> the page is released at once", () => { render(); openMenu(); fireEvent.click( within( screen.getByRole("navigation", { name: "Navegação mobile" }), ).getByRole("link", { name: "Cases" }), ); expect(content()).not.toHaveAttribute("inert"); expect(document.documentElement.style.overflow).toBe(""); }); it("path: no matchMedia at all in the browser -> the menu still opens and closes", () => { const original = window.matchMedia; window.matchMedia = undefined as unknown as typeof window.matchMedia; try { render(); openMenu(); expect( screen.getByRole("navigation", { name: "Navegação mobile" }), ).toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: "Fechar menu" })); expect( screen.queryByRole("navigation", { name: "Navegação mobile" }), ).toBeNull(); expect(content()).not.toHaveAttribute("inert"); } finally { window.matchMedia = original; } }); it("path: matchMedia returns an object with no change listener (older browsers) -> the menu still opens and closes", () => { const original = window.matchMedia; window.matchMedia = jest.fn().mockReturnValue({ matches: false, }) as unknown as typeof window.matchMedia; try { render(); openMenu(); expect( screen.getByRole("navigation", { name: "Navegação mobile" }), ).toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: "Fechar menu" })); expect( screen.queryByRole("navigation", { name: "Navegação mobile" }), ).toBeNull(); expect(content()).not.toHaveAttribute("inert"); } finally { window.matchMedia = original; } }); }); describe("Header: rota ausente", () => { it("path: usePathname gives null (outside the router) -> the header button goes to the home form", () => { jest.mocked(usePathname).mockReturnValue(null as unknown as string); render(
); expect( screen.getByRole("link", { name: "Falar sobre meu projeto" }), ).toHaveAttribute("href", "/#contact"); }); });