feat(home): posicionamento com prova, menu sem recarregar e WhatsApp em todo o site
H1 'Sites e sistemas que trazem pedidos pelo WhatsApp e tiram sua operação da planilha.'; HeroWork maior com a SEV; menu Cases, Serviços, Como trabalho, Sobre e IA com next/link e pré-carregamento no hover; menu do celular em tela cheia; WhatsApp flutuante e no rodapé com a mensagem da rota; rodapé em todas as páginas; FAQ da home; /ia como oferta de revisão de app feito com IA; formulário com noscript, validação do mailto e plano B do pop-up; cartões sem foco indevido; sem a fonte Monument.
This commit is contained in:
1 parent
e80f73d075
commit
10f170abf4
56 files changed
+3099
-906
No files matched your search
@@ -1,8 +1,40 @@
|
||||
import { fireEvent, render, screen, within } from "@testing-library/react";
|
||||
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/navigation", () => ({ usePathname: jest.fn() }));
|
||||
jest.mock("next/link", () => ({
|
||||
__esModule: true,
|
||||
default: ({
|
||||
children,
|
||||
prefetch,
|
||||
...props
|
||||
}: { children?: React.ReactNode; prefetch?: boolean | null } & Record<
|
||||
string,
|
||||
unknown
|
||||
>) => (
|
||||
<a data-next-link="" data-prefetch={String(prefetch)} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}));
|
||||
|
||||
const contexts: WhatsAppContexts = {
|
||||
services: { "criacao-de-sites": "Sites profissionais" },
|
||||
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(() => {
|
||||
@@ -32,12 +64,16 @@ describe("Header", () => {
|
||||
within(navigation)
|
||||
.getAllByRole("link")
|
||||
.map((link) => link.getAttribute("href")),
|
||||
).toEqual(["/#projects", "/#process", "/#about", "/cases", "/servicos"]);
|
||||
).toEqual(["/cases", "/servicos", "/#process", "/#about", "/ia"]);
|
||||
expect(
|
||||
within(navigation)
|
||||
.getAllByRole("link")
|
||||
.map((link) => link.textContent),
|
||||
).toEqual(["Projetos", "Como trabalho", "Sobre", "Cases", "Serviços"]);
|
||||
).toEqual(["Cases", "Serviços", "Como trabalho", "Sobre", "IA"]);
|
||||
// "Projetos" saiu: duplicava "Cases"
|
||||
expect(
|
||||
within(navigation).queryByRole("link", { name: "Projetos" }),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
within(navigation).queryByRole("link", { name: "Experiência" }),
|
||||
).not.toBeInTheDocument();
|
||||
@@ -77,11 +113,11 @@ describe("Header", () => {
|
||||
.getAllByRole("link")
|
||||
.map((link) => link.getAttribute("href")),
|
||||
).toEqual([
|
||||
"/#projects",
|
||||
"/#process",
|
||||
"/#about",
|
||||
"/cases",
|
||||
"/servicos",
|
||||
"/#process",
|
||||
"/#about",
|
||||
"/ia",
|
||||
"/#contact",
|
||||
]);
|
||||
expect(
|
||||
@@ -93,7 +129,7 @@ describe("Header", () => {
|
||||
const mobileLink = within(mobileNavigation).getByRole("link", {
|
||||
name: "Sobre",
|
||||
});
|
||||
mobileLink.focus();
|
||||
act(() => mobileLink.focus());
|
||||
fireEvent.keyDown(mobileLink, { key: "Escape" });
|
||||
const openButton = screen.getByRole("button", { name: "Abrir menu" });
|
||||
expect(openButton).toHaveAttribute("aria-expanded", "false");
|
||||
@@ -118,7 +154,7 @@ describe("Header", () => {
|
||||
.find((link) => link.getAttribute("href") === href);
|
||||
if (!mobileLink)
|
||||
throw new Error(`Missing navigation destination: ${href}`);
|
||||
mobileLink.focus();
|
||||
act(() => mobileLink.focus());
|
||||
fireEvent.click(mobileLink);
|
||||
|
||||
expect(
|
||||
@@ -130,4 +166,111 @@ describe("Header", () => {
|
||||
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(<Header />);
|
||||
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)) {
|
||||
expect(link).toHaveClass("text-[22px]", "min-h-14");
|
||||
}
|
||||
const contact = links[5];
|
||||
expect(contact).toHaveAccessibleName("Falar sobre meu projeto");
|
||||
expect(contact.parentElement).toHaveClass("mt-auto");
|
||||
});
|
||||
|
||||
it("uses next/link for every internal link and only prefetches on hover or focus", () => {
|
||||
render(<Header />);
|
||||
|
||||
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 other pages", () => {
|
||||
for (const pathname of ["/", "/cases", "/ia", "/servicos"]) {
|
||||
jest.mocked(usePathname).mockReturnValue(pathname);
|
||||
const { unmount } = render(<Header whatsAppContexts={contexts} />);
|
||||
|
||||
const contact = screen.getAllByRole("link", {
|
||||
name: "Falar sobre meu projeto",
|
||||
});
|
||||
expect(contact).toHaveLength(1);
|
||||
expect(contact[0]).toHaveAttribute("href", "/#contact");
|
||||
unmount();
|
||||
}
|
||||
});
|
||||
|
||||
it("opens WhatsApp with the service in the header button on a service page", () => {
|
||||
jest.mocked(usePathname).mockReturnValue("/servicos/criacao-de-sites");
|
||||
render(<Header whatsAppContexts={contexts} />);
|
||||
|
||||
const contact = screen.getByRole("link", {
|
||||
name: "Falar sobre meu projeto",
|
||||
});
|
||||
expect(contact.getAttribute("href")).toMatch(/^https:\/\/wa\.me\/\d+\?/);
|
||||
expect(contact).toHaveAttribute("target", "_blank");
|
||||
expect(whatsAppHref(contact)).toContain(
|
||||
"Quero conversar sobre Sites profissionais.",
|
||||
);
|
||||
});
|
||||
|
||||
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(<Header whatsAppContexts={contexts} />);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Abrir menu" }));
|
||||
const buttons = screen.getAllByRole("link", {
|
||||
name: "Falar sobre meu projeto",
|
||||
});
|
||||
expect(buttons).toHaveLength(2);
|
||||
for (const button of buttons) {
|
||||
expect(whatsAppHref(button)).toContain(
|
||||
"Quero conversar sobre um projeto parecido com o da 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(<Header whatsAppContexts={contexts} />);
|
||||
|
||||
expect(
|
||||
whatsAppHref(
|
||||
screen.getByRole("link", { name: "Falar sobre meu projeto" }),
|
||||
),
|
||||
).toContain("Quero conversar sobre um projeto.");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user