import { act, fireEvent, render, screen, within } from "@testing-library/react"; import { usePathname } from "next/navigation"; import { useEffect } from "react"; import type { WhatsAppContexts } from "@/lib/whatsapp"; import { useHomeFragmentNavigation } from "./cinematic/useHomeFragmentNavigation"; import { Header } from "./Header"; jest.mock("next/link", () => ({ __esModule: true, default: ({ children, prefetch, ...props }: { children?: React.ReactNode; prefetch?: boolean | null } & Record< string, unknown >) => ( {children} ), })); const contexts: WhatsAppContexts = { // o whatsappSubject do serviço (lib/site-content.ts): uma frase em minúsculas, que cabe em "Quero conversar sobre {assunto}." services: { "criacao-de-sites": "um site que traz pedidos pelo WhatsApp" }, cases: { "sev-modaintima": "SEV Moda Íntima" }, }; beforeEach(() => { jest.mocked(usePathname).mockReturnValue("/"); }); function whatsAppHref(link: HTMLElement) { return new URL(link.getAttribute("href") ?? "").searchParams.get("text"); } function HomeWithHeader() { const onLayoutReady = useHomeFragmentNavigation(); useEffect(() => { onLayoutReady(); }, [onLayoutReady]); return ( <>

Sobre

Contato

); } describe("Header", () => { it("links to commercial destinations without promoting the résumé", () => { render(
); const navigation = screen.getByRole("navigation", { name: "Navegação principal", }); expect( within(navigation) .getAllByRole("link") .map((link) => link.getAttribute("href")), ).toEqual(["/cases", "/servicos", "/#process", "/#about", "/ia"]); expect( within(navigation) .getAllByRole("link") .map((link) => link.textContent), ).toEqual([ "Cases", "Serviços", "Como trabalho", "Sobre", "Revisão de app com IA", ]); // "Projetos" saiu: duplicava "Cases" expect( within(navigation).queryByRole("link", { name: "Projetos" }), ).not.toBeInTheDocument(); expect( within(navigation).queryByRole("link", { name: "Experiência" }), ).not.toBeInTheDocument(); const contactLink = screen .getAllByRole("link") .find((link) => link.getAttribute("href") === "/#contact"); expect(contactLink).toHaveAccessibleName("Falar sobre meu projeto"); expect( screen.queryByRole("link", { name: /currículo|download cv/i }), ).not.toBeInTheDocument(); }); it("announces and controls the mobile menu", () => { render(
); const menuButton = screen.getByRole("button", { name: "Abrir menu" }); expect(menuButton).not.toHaveAttribute("aria-controls"); expect(menuButton).toHaveAttribute("aria-expanded", "false"); expect( screen.queryByRole("navigation", { name: "Navegação mobile" }), ).not.toBeInTheDocument(); fireEvent.click(menuButton); expect(screen.getByRole("button", { name: "Fechar menu" })).toHaveAttribute( "aria-expanded", "true", ); const mobileNavigation = screen.getByRole("navigation", { name: "Navegação mobile", }); expect( document.getElementById(menuButton.getAttribute("aria-controls") ?? ""), ).toBe(mobileNavigation); expect( within(mobileNavigation) .getAllByRole("link") .map((link) => link.getAttribute("href")), ).toEqual([ "/cases", "/servicos", "/#process", "/#about", "/ia", "/#contact", ]); expect( within(mobileNavigation).queryByRole("link", { name: /currículo|download cv/i, }), ).not.toBeInTheDocument(); const mobileLink = within(mobileNavigation).getByRole("link", { name: "Sobre", }); act(() => mobileLink.focus()); fireEvent.keyDown(mobileLink, { key: "Escape" }); const openButton = screen.getByRole("button", { name: "Abrir menu" }); expect(openButton).toHaveAttribute("aria-expanded", "false"); expect(document.activeElement).toBe(openButton); }); it.each([ ["/#about", "Sobre"], ["/#contact", "Contato"], ])( "closes the mobile menu and preserves destination focus for %s", (href, destination) => { window.history.replaceState(null, "", "/"); render(); fireEvent.click(screen.getByRole("button", { name: "Abrir menu" })); const mobileNavigation = screen.getByRole("navigation", { name: "Navegação mobile", }); const mobileLink = within(mobileNavigation) .getAllByRole("link") .find((link) => link.getAttribute("href") === href); if (!mobileLink) throw new Error(`Missing navigation destination: ${href}`); act(() => mobileLink.focus()); fireEvent.click(mobileLink); expect( screen.getByRole("button", { name: "Abrir menu" }), ).toHaveAttribute("aria-expanded", "false"); expect( screen.queryByRole("navigation", { name: "Navegação mobile" }), ).not.toBeInTheDocument(); expect(screen.getByRole("region", { name: destination })).toHaveFocus(); }, ); it("makes the mobile menu a full-screen opaque panel with large links and the contact button at the bottom", () => { render(
); fireEvent.click(screen.getByRole("button", { name: "Abrir menu" })); const mobileNavigation = screen.getByRole("navigation", { name: "Navegação mobile", }); const panel = mobileNavigation.parentElement?.parentElement as HTMLElement; expect(panel).toHaveClass("bg-[var(--ink)]"); expect(panel.className).toMatch(/h-\[calc\(100dvh-/); const links = within(mobileNavigation).getAllByRole("link"); for (const link of links.slice(0, 5)) { // sob o ponteiro e no foco, a barra que cresce sob o texto (globals.css) expect(link).toHaveClass("text-[22px]", "min-h-14", "cv-bar"); } const contact = links[5]; expect(contact).toHaveAccessibleName("Falar sobre meu projeto"); expect(contact.parentElement).toHaveClass("mt-auto"); // o botão verde ganha o bloco cor de papel, não só uma troca de cor expect(contact).toHaveClass("cv-fill"); expect(contact.className).not.toMatch(/hover:bg-/); }); it("uses next/link for every internal link and only prefetches on hover or focus", () => { render(
); const navigation = screen.getByRole("navigation", { name: "Navegação principal", }); const internal = [ screen.getByRole("link", { name: "Evandro.dev.br — início" }), ...within(navigation).getAllByRole("link"), screen.getByRole("link", { name: "Falar sobre meu projeto" }), ]; for (const link of internal) { expect(link).toHaveAttribute("data-next-link"); expect(link.getAttribute("href")?.startsWith("/")).toBe(true); } // cabeçalho e logotipo: nada pré-carregado na abertura da página const headerLinks = internal.slice(0, 1 + 5); for (const link of headerLinks) { expect(link).toHaveAttribute("data-prefetch", "false"); } fireEvent.mouseEnter(headerLinks[1]); expect(headerLinks[1]).toHaveAttribute("data-prefetch", "null"); expect(headerLinks[2]).toHaveAttribute("data-prefetch", "false"); fireEvent.focus(headerLinks[3]); expect(headerLinks[3]).toHaveAttribute("data-prefetch", "null"); }); it("sends the header button to the home form on the home and on pages without a subject", () => { for (const pathname of ["/", "/privacidade"]) { jest.mocked(usePathname).mockReturnValue(pathname); const { unmount } = render(
); const contact = screen.getAllByRole("link", { name: "Falar sobre meu projeto", }); expect(contact).toHaveLength(1); expect(contact[0]).toHaveAttribute("href", "/#contact"); unmount(); } }); it.each([ ["/ia", "Quero conversar sobre a revisão de um aplicativo feito com IA."], [ "/cases", "Quero conversar sobre um projeto parecido com os cases do portfólio.", ], ["/servicos", "Quero conversar sobre sites, sistemas e aplicativos."], [ "/curriculo", "Quero conversar sobre um projeto, depois de ver o seu currículo.", ], ])( "opens WhatsApp with the page subject in the header button on %s", (pathname, sentence) => { jest.mocked(usePathname).mockReturnValue(pathname); render(
); const contact = screen.getByRole("link", { name: "Chamar no WhatsApp", }); expect(contact.getAttribute("href")).toMatch(/^https:\/\/wa\.me\/\d+\?/); // "Falar sobre meu projeto" é só do formulário: o botão que abre o WhatsApp tem outro nome expect( screen.queryByRole("link", { name: "Falar sobre meu projeto" }), ).not.toBeInTheDocument(); expect(contact).toHaveAttribute("target", "_blank"); expect(whatsAppHref(contact)).toContain(sentence); }, ); it("opens WhatsApp with the service in the header button on a service page", () => { jest.mocked(usePathname).mockReturnValue("/servicos/criacao-de-sites"); render(
); const contact = screen.getByRole("link", { name: "Chamar no WhatsApp", }); expect(contact.getAttribute("href")).toMatch(/^https:\/\/wa\.me\/\d+\?/); expect(contact).toHaveAttribute("target", "_blank"); expect(whatsAppHref(contact)).toContain( "Quero conversar sobre um site que traz pedidos pelo WhatsApp.", ); expect(whatsAppHref(contact)).not.toContain("sobre Sites"); }); it("opens WhatsApp with the case in the header button on a case page, also from the mobile menu", () => { jest.mocked(usePathname).mockReturnValue("/cases/sev-modaintima"); render(
); fireEvent.click(screen.getByRole("button", { name: "Abrir menu" })); const buttons = screen.getAllByRole("link", { name: "Chamar no WhatsApp", }); expect(buttons).toHaveLength(2); for (const button of buttons) { expect(whatsAppHref(button)).toContain( "Quero conversar sobre um projeto parecido com o case SEV Moda Íntima.", ); } // abrir o WhatsApp pelo menu fecha o menu fireEvent.click(buttons[1]); expect( screen.queryByRole("navigation", { name: "Navegação mobile" }), ).not.toBeInTheDocument(); }); it("falls back to a generic message on a service page the contexts do not know", () => { jest.mocked(usePathname).mockReturnValue("/servicos/inexistente"); render(
); expect( whatsAppHref(screen.getByRole("link", { name: "Chamar no WhatsApp" })), ).toContain("Quero conversar sobre um projeto."); }); });