import { fireEvent, render, screen } from "@testing-library/react"; import Hero from "./Hero"; import { HERO_CAPTIONS } from "./hero-captions"; // o Link do Next, trocado por um que mostra o `prefetch` recebido: HoverPrefetchLink começa em false jest.mock("next/link", () => ({ __esModule: true, default: ({ children, prefetch, ...props }: { children?: React.ReactNode; prefetch?: boolean | null } & Record< string, unknown >) => ( {children} ), })); const clients = [ { id: "sev-modaintima", name: "SEV Moda Íntima" }, { id: "lavacao-047", name: "Lavação 047" }, { id: "sc-plus", name: "SC Plus Aquicultura" }, { id: "as-servicos", name: "AS Serviços" }, ]; describe("Hero", () => { beforeEach(() => { jest.spyOn(window, "open").mockImplementation(() => null); }); afterEach(() => { jest.restoreAllMocks(); }); it("sends the main button to the contact form, keeps 'Ver projetos entregues' secondary and opens WhatsApp from a quiet link after them", () => { render(); const contactLink = screen.getByRole("link", { name: /falar sobre meu projeto/i, }); expect(contactLink).toHaveAccessibleName("Falar sobre meu projeto"); expect(contactLink).toHaveAttribute("href", "/#contact-form"); expect(contactLink).not.toHaveAttribute("target"); // o botão primário é o único com o fundo limão (e o bloco cor de papel no ponteiro); o secundário é contorno expect(contactLink.className).toContain("bg-[var(--slash)]"); expect(contactLink).toHaveClass("cv-fill"); expect(contactLink.className).not.toMatch(/hover:bg-/); const projects = screen.getByRole("link", { name: /ver projetos/i }); expect(projects).toHaveAttribute("href", "/cases"); expect(projects.className).not.toContain("bg-[var(--slash)]"); expect(projects).toHaveClass("cv-outline-dark"); expect(projects).not.toHaveAttribute("target"); const whatsApp = screen.getByRole("link", { name: "Chamar no WhatsApp" }); expect(whatsApp.getAttribute("href")).toMatch(/^https:\/\/wa\.me\/\d+\?/); expect(whatsApp).toHaveAttribute("target", "_blank"); expect(whatsApp).toHaveAttribute("rel", "noopener noreferrer"); const text = new URL(whatsApp.getAttribute("href") ?? "").searchParams.get( "text", ); expect(text).toBe( "Olá, Evandro! Quero conversar sobre um site ou sistema para a minha empresa. Contato iniciado pelo site evandro.dev.br.", ); expect(whatsApp.className).not.toContain("bg-[var(--slash)]"); expect(whatsApp.querySelector("svg")).toHaveAttribute( "aria-hidden", "true", ); // a ordem do Tab: os dois botões e depois o link expect( contactLink.compareDocumentPosition(whatsApp) & Node.DOCUMENT_POSITION_FOLLOWING, ).toBeTruthy(); expect( projects.compareDocumentPosition(whatsApp) & Node.DOCUMENT_POSITION_FOLLOWING, ).toBeTruthy(); }); it("keeps the headline as one readable sentence despite the per-word animation", () => { render(); expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent( "Sites e sistemas sob medida para o jeito que a sua empresa trabalha.", ); }); it("highlights only 'sob medida' in lime, kept on one line under one continuous marker block", () => { render(); const heading = screen.getByRole("heading", { level: 1 }); const highlighted = Array.from( heading.querySelectorAll(".hero-word"), ).filter((word) => word.className.includes("text-[var(--slash)]")); expect(highlighted.map((word) => word.textContent)).toEqual([ "sob", "medida", ]); // um trecho só, que não quebra entre as duas palavras e leva o marcador; cada palavra mantém a sua entrada const marks = heading.querySelectorAll(".hero-mark"); expect(marks).toHaveLength(1); expect(marks[0]).toHaveClass("whitespace-nowrap"); expect(marks[0].textContent).toBe("sob medida"); for (const word of highlighted) expect(marks[0]).toContainElement(word); }); it("keeps the single LinkedIn profile and the e-mail, with no GitHub link", () => { render(); expect(screen.getByRole("link", { name: "LinkedIn" })).toHaveAttribute( "href", "https://www.linkedin.com/in/evandrodevbr/", ); expect(screen.getByRole("link", { name: "Email" })).toHaveAttribute( "href", "mailto:evandrofonsecajunior@gmail.com", ); const linkedin = screen.getByRole("link", { name: "LinkedIn" }); expect(linkedin).toHaveAttribute("target", "_blank"); expect(linkedin).toHaveAttribute("rel", "noopener noreferrer"); expect(screen.queryByRole("link", { name: "GitHub" })).toBeNull(); for (const link of screen.getAllByRole("link")) { expect(link.getAttribute("href") ?? "").not.toMatch(/github\.com/); } }); it("states who the work is for and how the contact works", () => { render(); expect( screen.getByText( "Do site institucional ao sistema que a sua equipe usa todo dia. Você fala com quem desenvolve.", ), ).toBeInTheDocument(); // o primeiro passo, logo abaixo dos botões const step = screen.getByText( "Começa com uma conversa para entender o seu caso. A proposta vem por escrito.", ); expect(step.className).toContain("text-[15px]"); expect( screen .getByRole("link", { name: "Chamar no WhatsApp" }) .compareDocumentPosition(step) & Node.DOCUMENT_POSITION_FOLLOWING, ).toBeTruthy(); // o botão flutuante do WhatsApp observa este id: some enquanto o link do hero está à vista expect( screen.getByRole("link", { name: "Chamar no WhatsApp" }), ).toHaveAttribute("id", "hero-contact-cta"); }); it("shows only the years of experience, with no repository numbers", () => { render(); expect(screen.getByText("anos de experiência")).toBeInTheDocument(); expect(screen.getByText("5")).toBeInTheDocument(); expect(screen.queryByRole("term")).toBeNull(); expect(screen.queryByText(/projetos desenvolvidos/i)).toBeNull(); expect(screen.queryByText(/públicos|privados|mais de/i)).toBeNull(); }); it("lists the delivered clients, each linking to its case", () => { render(); expect(screen.getByText("Já entregue para:")).toBeInTheDocument(); for (const { id, name } of clients) { expect(screen.getByRole("link", { name })).toHaveAttribute( "href", `/cases/${id}`, ); } }); it("shows the block piece as a decorative poster: hidden from assistive tech, with an empty alt", () => { const { container } = render(); const piece = container.querySelector(".hero-piece"); expect(piece).not.toBeNull(); expect(piece).toHaveAttribute("aria-hidden", "true"); const image = piece?.querySelector("img"); expect(image?.getAttribute("src")).toContain("hero-poster"); expect(image).toHaveAttribute("alt", ""); }); it("shows a wide card to the SEV case below the buttons on small screens", () => { render(); const small = screen.getByTestId("hero-work-mobile"); expect(small).toHaveClass("lg:hidden"); expect(small).toHaveAttribute("href", "/cases/sev-modaintima"); expect(small).toHaveAccessibleName("Site da SEV Moda Íntima Ver case"); const buttons = screen.getByRole("link", { name: /ver projetos/i }); expect( buttons.compareDocumentPosition(small) & Node.DOCUMENT_POSITION_FOLLOWING, ).toBeTruthy(); }); it("keeps the label above the headline in the display font, so the mono font is not downloaded before the LCP", () => { const { container } = render(); const label = container.querySelector(".hero-kicker") as HTMLElement; expect(label.textContent).toBe( "Evandro Fonseca Junior · Engenharia de software", ); expect(label.className).toContain("font-display"); expect(label.className).not.toContain("font-mono"); // sem caixa alta nem espaçamento largo expect(label.className).not.toMatch(/uppercase|tracking-\[0\.1/); }); it("shows only 'Engenharia de software' in the label below 1024 px, where it shares the line with the piece", () => { render(); expect(screen.getByText(/Evandro Fonseca Junior/)).toHaveClass( "max-lg:hidden", ); expect(screen.getByText(/Engenharia de software/).className).not.toContain( "hidden", ); }); it("uses no mono font anywhere in the first fold (label, years, scroll cue)", () => { const { container } = render( , ); expect(container.querySelector(".font-mono")).toBeNull(); }); it("pre-fetches 'Ver projetos entregues', the clients and the SEV card only when reached (hover, focus or touch), never by viewport", () => { render(); const links = [ screen.getByRole("link", { name: /ver projetos/i }), screen.getByRole("link", { name: "SEV Moda Íntima" }), screen.getByTestId("hero-work-mobile"), ]; for (const link of links) { expect(link).toHaveAttribute("data-prefetch", "false"); fireEvent.mouseEnter(link); expect(link).toHaveAttribute("data-prefetch", "null"); } }); it("writes the range of each dive caption (--from and --to, from hero-captions.ts) on its own item, in order", () => { const { container } = render(); const items = Array.from( container.querySelectorAll(".hero-captions li"), ); expect(items).toHaveLength(HERO_CAPTIONS.length); items.forEach((item, index) => { const { title, text, from, to } = HERO_CAPTIONS[index]; expect(item).toHaveTextContent(`${title}${text}`); expect(item.style.getPropertyValue("--from")).toBe(String(from)); expect(item.style.getPropertyValue("--to")).toBe(String(to)); }); }); });