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.
189 lines
6.9 KiB
TypeScript
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");
|
|
}
|
|
});
|
|
});
|