Files
portifolio-evandrodevbr/app/components/Hero.test.tsx
T
Evandro Fonseca Junior 9e2d65a926 fix(home): oferta leva ao formulário, ofertas logo depois da vitrine
O botão das ofertas rola até o contato com o tipo marcado e não quebra mais as âncoras; ofertas com o que você recebe e primeiro passo, logo depois da vitrine; bloco de IA fora da home; cartão largo da SEV no hero do celular; separador da faixa de clientes só entre itens; favicon de 13 para 1,8 KB; pré-carregamento só no hover e nunca de /#... fora da home; X do menu e cabeçalho opaco; WhatsApp com "o case"; /ia com 3 passos e modelo de relatório ilustrativo; Sobre alinhado ao currículo; CSP Report-Only com os domínios do Ads.
2026-10-14 19:14:47 -03:00

223 lines
8.1 KiB
TypeScript

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 <a> 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
>) => (
<a data-prefetch={String(prefetch)} {...props}>
{children}
</a>
),
}));
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("opens WhatsApp straight from the main button, with the subject of the home, while 'Ver projetos entregues' stays secondary", () => {
render(<Hero />);
const contactLink = screen.getByRole("link", {
name: /falar sobre meu projeto/i,
});
expect(contactLink).toHaveAccessibleName("Falar sobre meu projeto");
expect(contactLink.getAttribute("href")).toMatch(
/^https:\/\/wa\.me\/\d+\?/,
);
expect(contactLink).toHaveAttribute("target", "_blank");
expect(contactLink).toHaveAttribute("rel", "noopener noreferrer");
const text = new URL(
contactLink.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.",
);
// o botão primário é o único com o fundo limão; o secundário é contorno
expect(contactLink.className).toContain("bg-[var(--slash)]");
const projects = screen.getByRole("link", { name: /ver projetos/i });
expect(projects).toHaveAttribute("href", "/cases");
expect(projects.className).not.toContain("bg-[var(--slash)]");
expect(projects).not.toHaveAttribute("target");
});
it("keeps the headline as one readable sentence despite the per-word animation", () => {
render(<Hero />);
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent(
"Sites e sistemas que trazem pedidos pelo WhatsApp e tiram sua operação da planilha.",
);
});
it("highlights only the short stretch 'pedidos pelo WhatsApp' in lime", () => {
render(<Hero />);
const highlighted = Array.from(
screen.getByRole("heading", { level: 1 }).querySelectorAll(".hero-word"),
)
.filter((word) => word.className.includes("text-[var(--slash)]"))
.map((word) => word.textContent);
expect(highlighted).toEqual(["pedidos", "pelo", "WhatsApp"]);
});
it("keeps the single LinkedIn profile and the e-mail, with no GitHub link", () => {
render(<Hero />);
expect(screen.getByRole("link", { name: "LinkedIn" })).toHaveAttribute(
"href",
"https://www.linkedin.com/in/evandrodevbr/",
);
expect(screen.getByRole("link", { name: "Email" })).toHaveAttribute(
"href",
"mailto:[email protected]",
);
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(<Hero />);
expect(
screen.getByText(
"Do catálogo com pedido no WhatsApp ao painel da sua equipe. Você fala com quem desenvolve.",
),
).toBeInTheDocument();
expect(
screen.getByRole("link", { name: /falar sobre meu projeto/i }),
).toHaveAttribute("id", "hero-contact-cta");
});
it("shows only the years of experience, with no repository numbers", () => {
render(<Hero stats={{ years: 5 }} />);
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(<Hero clients={clients} />);
expect(screen.getByText("Já entregue para:")).toBeInTheDocument();
for (const { id, name } of clients) {
expect(screen.getByRole("link", { name })).toHaveAttribute(
"href",
`/cases/${id}`,
);
}
});
it("renders the work column with lazy posters that stay out of small screens", () => {
render(<Hero clients={clients} />);
const work = screen.getByTestId("hero-work");
expect(work).toHaveClass("hidden", "lg:block");
const images = work.querySelectorAll("img");
expect(images).toHaveLength(2);
for (const image of images) {
expect(image).toHaveAttribute("loading", "lazy");
}
// pelo otimizador do next/image: o endereço original vai no parâmetro `url`
const original = (image: Element) =>
new URL(
image.getAttribute("src") ?? "",
"http://localhost",
).searchParams.get("url");
expect(original(images[0])).toBe("/videos/sev-modaintima/desktop.webp");
expect(images[0]).toHaveAccessibleName(
"Site da SEV Moda Íntima aberto no computador",
);
expect(original(images[1])).toBe("/videos/as-servicos/mobile.webp");
});
it("shows a wide card to the SEV case below the buttons on small screens", () => {
render(<Hero clients={clients} />);
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", () => {
render(<Hero />);
const label = screen.getByText(
/Evandro Fonseca Junior · Engenharia de software/,
);
expect(label.className).toContain("font-display");
expect(label.className).not.toContain("font-mono");
});
it("uses no mono font anywhere in the first fold (label, years, scroll cue)", () => {
const { container } = render(
<Hero clients={clients} stats={{ years: 5 }} />,
);
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(<Hero clients={clients} />);
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(<Hero clients={clients} />);
const items = Array.from(
container.querySelectorAll<HTMLElement>(".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));
});
});
});