Files
portifolio-evandrodevbr/app/components/Footer.test.tsx
T
evandro c7d09d63d4
CI / audit (push) Successful in 10m38s
CI / verify (push) Successful in 13m10s
CI / deploy (push) Successful in 1m50s
CI / e2e (push) Successful in 18m48s
feat(site): blocos 3D da marca, novas ofertas e hero mais abrangente
Hero e movimento
- cena 3D em tempo real com os blocos da marca (kit feito no Blender, 3d/blocos.py):
  marca, tela, integrações e dados; segue a rolagem, tem vida em repouso, reage ao
  cursor e, parada no topo, passa sozinha pelos estados com legenda
- três níveis de aparelho (full, lite, static); sem WebGL fica o pôster
- título novo: "Sites e sistemas sob medida para o jeito que a sua empresa trabalha."
- faixa de clientes com o ramo, linha do primeiro passo, hero alinhado às seções
- rótulos digitados, entrada dos blocos, cortina de blocos entre páginas e capa
  compartilhada entre a lista de cases e o case

Ofertas
- atendimento no WhatsApp com IA e nota fiscal para a reforma tributária
  (páginas de serviço, cartões da home, formulário e aviso com data de validade)
- página /ia atualizada, com fontes
- peças 3D em loop nos cartões e topos de página (3d/pecas.py)

Design
- hover por papel (bloco nos botões, barra nos links), rótulos sem caixa alta
- um destino por rótulo: "Falar sobre meu projeto" leva ao formulário,
  "Chamar no WhatsApp" abre o WhatsApp
- logo montado em blocos, 404 redesenhado, grades sem célula vazia
- ícones do PWA e manifesto novos

Specs em specs/, contexto de design em DESIGN.md e PRODUCT.md.
2026-11-10 03:48:25 -03:00

189 lines
6.9 KiB
TypeScript

import { fireEvent, render, screen, within } from "@testing-library/react";
import { OPEN_COOKIE_PREFERENCES_EVENT } from "@/lib/analytics";
import { serviceHubLinks } from "@/lib/site-content";
import Footer from "./Footer";
// 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>
),
}));
describe("Footer contact and navigation", () => {
it("offers a direct mailto link without repeating the contact call to action", () => {
render(<Footer />);
// o convite principal fica na secao Contato, logo acima do rodape
expect(
screen.queryByRole("link", { name: "Falar sobre meu projeto" }),
).not.toBeInTheDocument();
const emailLink = screen.getByRole("link", {
name: "[email protected]",
});
expect(emailLink).toHaveAttribute(
"href",
"mailto:[email protected]",
);
expect(emailLink).not.toHaveAttribute("target", "_blank");
});
it("offers WhatsApp in the contact column, in a new tab, named like every control that opens it", () => {
render(<Footer />);
const contact = screen.getByRole("region", { name: "Contato" });
const whatsApp = within(contact).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");
});
it("lists the eight services from the services hub as same-tab links", () => {
render(<Footer serviceLinks={serviceHubLinks} />);
expect(serviceHubLinks).toHaveLength(8);
const services = screen.getByRole("navigation", { name: "Serviços" });
const links = within(services).getAllByRole("link");
expect(links.map((link) => link.getAttribute("href"))).toEqual(
serviceHubLinks.map((service) => service.href),
);
expect(links.map((link) => link.textContent)).toEqual(
serviceHubLinks.map((service) => service.label),
);
for (const link of links) expect(link).not.toHaveAttribute("target");
});
it("links Cases, the /ia page by its menu label, Privacidade and a discreet Currículo, with no PDF download", () => {
render(<Footer />);
const more = screen.getByRole("navigation", { name: "Mais" });
expect(within(more).getByRole("link", { name: "Cases" })).toHaveAttribute(
"href",
"/cases",
);
expect(
within(more).getByRole("link", {
name: "Revisão de app com IA",
}),
).toHaveAttribute("href", "/ia");
expect(
within(more).getByRole("link", { name: "Privacidade" }),
).toHaveAttribute("href", "/privacidade");
const resume = within(more).getByRole("link", { name: "Currículo" });
expect(resume).toHaveAttribute("href", "/curriculo");
expect(within(more).getAllByRole("link").at(-1)).toBe(resume);
expect(screen.queryByRole("link", { name: /baixar|pdf/i })).toBeNull();
for (const link of screen.getAllByRole("link")) {
expect(link).not.toHaveAttribute("download");
}
});
it("reopens the cookie preferences from a button", () => {
const reopen = jest.fn();
window.addEventListener(OPEN_COOKIE_PREFERENCES_EVENT, reopen);
render(<Footer />);
fireEvent.click(
screen.getByRole("button", { name: "Preferências de cookies" }),
);
expect(reopen).toHaveBeenCalledTimes(1);
window.removeEventListener(OPEN_COOKIE_PREFERENCES_EVENT, reopen);
});
it("states the location and rights in Portuguese", () => {
render(<Footer />);
expect(
screen.getByText("Garuva, SC · Atendimento remoto em todo o Brasil"),
).toBeInTheDocument();
expect(
screen.getByText(/Todos os direitos\s+reservados/),
).toBeInTheDocument();
expect(screen.queryByText(/remote worldwide|all rights/i)).toBeNull();
});
});
describe("Footer social links", () => {
it("keeps one LinkedIn profile in a new tab and no GitHub link", () => {
render(<Footer />);
const linkedin = screen.getByRole("link", { name: "LinkedIn" });
expect(linkedin).toHaveAttribute(
"href",
"https://www.linkedin.com/in/evandrodevbr/",
);
expect(linkedin).toHaveAttribute("target", "_blank");
expect(linkedin).toHaveAttribute("rel", "noopener noreferrer");
for (const link of screen.getAllByRole("link")) {
expect(link.getAttribute("href") ?? "").not.toMatch(/github\.com/);
}
expect(screen.queryByRole("link", { name: /github/i })).toBeNull();
});
it("keeps Instagram in the contact column, in a new tab", () => {
render(<Footer />);
const contact = screen.getByRole("region", { name: "Contato" });
const instagram = within(contact).getByRole("link", { name: "Instagram" });
expect(instagram).toHaveAttribute(
"href",
"https://www.instagram.com/evandrodevbr/",
);
expect(instagram).toHaveAttribute("target", "_blank");
expect(instagram).toHaveAttribute("rel", "noopener noreferrer");
});
it("names /ia like the menu and the page, and no old 'app feito com IA' name", () => {
render(<Footer />);
expect(
screen.getByRole("link", { name: "Revisão de app com IA" }),
).toHaveAttribute("href", "/ia");
expect(screen.queryByText(/app feito com IA/i)).toBeNull();
});
it("adds the height of the cookie bar to the bottom padding, so the logo and the copyright are not under it at the end of the page", () => {
const { container } = render(<Footer />);
const footer = container.querySelector("footer") as HTMLElement;
// com a barra fechada a variável não existe: o 0px do fallback deixa o rodapé como era
expect(footer.className).toContain(
"pb-[calc(var(--sp-10)+var(--cookie-bar-height,0px))]",
);
// no celular o botão flutuante do WhatsApp (72 px) também continua à parte
expect(footer.className).toContain(
"max-md:pb-[calc(var(--sp-10)+72px+var(--cookie-bar-height,0px))]",
);
expect(footer.className).not.toMatch(/(?:^|\s)py-sp-10(?:\s|$)/);
});
it("pre-fetches the internal links only when the visitor reaches them, never by viewport (services, cases, /ia, privacy, résumé, home)", () => {
render(<Footer serviceLinks={serviceHubLinks} />);
const internal = screen
.getAllByRole("link")
.filter((link) => (link.getAttribute("href") ?? "").startsWith("/"));
// serviços do hub + Cases, /ia, Privacidade, Currículo + o logo para /#home
expect(internal.length).toBeGreaterThanOrEqual(serviceHubLinks.length + 5);
for (const link of internal) {
expect(link).toHaveAttribute("data-prefetch", "false");
fireEvent.mouseEnter(link);
expect(link).toHaveAttribute("data-prefetch", "null");
}
});
});