feat(home): ofertas que vendem o título, WhatsApp no hero e menu do celular acessível
Três ofertas (site que traz pedidos pelo WhatsApp, sistema para tirar a operação da planilha, revisão de sistema existente) com miniatura e formulário com o tipo escolhido; botão principal do hero abre o WhatsApp; moldura da SEV no hero do celular; menu fecha ao navegar, Esc, foco preso incluindo a barra de cookies e espaço para ela; CinematicHome como componente de servidor com ilha cliente; /ia com H1 de oferta e cartão do que você recebe; botão flutuante em aside com transform; AVIF antes de WebP; copy sem repetir contato direto.
This commit is contained in:
1 parent
de6af74a0c
commit
f8545cd442
44 files changed
+2286
-571
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import { act, fireEvent, render, screen } from "@testing-library/react";
|
||||
import { act, fireEvent, render, screen, within } from "@testing-library/react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Header } from "./Header";
|
||||
|
||||
@@ -33,7 +33,8 @@ function setScrollY(value: number) {
|
||||
}
|
||||
|
||||
const header = () => screen.getByRole("banner");
|
||||
const isDark = () => header().className.includes("backdrop-blur-md");
|
||||
/** Rolada: borda visível e fundo a 95% (sem backdrop-blur: o filtro refazia a pintura a cada quadro da rolagem). */
|
||||
const isDark = () => header().className.includes("border-white/12");
|
||||
|
||||
beforeEach(() => {
|
||||
jest.mocked(usePathname).mockReturnValue("/");
|
||||
@@ -53,7 +54,7 @@ describe("Header: rolagem", () => {
|
||||
expect(header()).toHaveClass("border-transparent");
|
||||
});
|
||||
|
||||
it("path: scrolling down and back to the top -> blurred bar, then the plain one again", () => {
|
||||
it("path: scrolling down and back to the top -> bordered bar, then the plain one again, never with a backdrop blur", () => {
|
||||
render(<Header />);
|
||||
|
||||
setScrollY(120);
|
||||
@@ -62,6 +63,7 @@ describe("Header: rolagem", () => {
|
||||
});
|
||||
expect(isDark()).toBe(true);
|
||||
expect(header()).not.toHaveClass("border-transparent");
|
||||
expect(header().className).not.toContain("backdrop-blur");
|
||||
|
||||
// outro evento de rolagem sem mudar de lado: nada muda
|
||||
setScrollY(300);
|
||||
@@ -77,7 +79,7 @@ describe("Header: rolagem", () => {
|
||||
expect(isDark()).toBe(false);
|
||||
});
|
||||
|
||||
it("path: page already scrolled when the header mounts -> starts with the blurred bar", () => {
|
||||
it("path: page already scrolled when the header mounts -> starts with the bordered bar", () => {
|
||||
setScrollY(500);
|
||||
render(<Header />);
|
||||
|
||||
@@ -99,9 +101,12 @@ describe("Header: teclado no menu do celular", () => {
|
||||
render(<Header />);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Abrir menu" }));
|
||||
const menu = screen.getByRole("navigation", { name: "Navegação mobile" });
|
||||
// Tab entre dois controles do meio da lista: segue o caminho normal (o foco só é preso nas pontas)
|
||||
const middle = within(menu).getByRole("link", { name: "Serviços" });
|
||||
act(() => middle.focus());
|
||||
|
||||
// fireEvent devolve false só quando o evento foi cancelado (preventDefault)
|
||||
expect(fireEvent.keyDown(menu, { key: "Tab" })).toBe(true);
|
||||
expect(fireEvent.keyDown(middle, { key: "Tab" })).toBe(true);
|
||||
expect(fireEvent.keyDown(menu, { key: "Enter" })).toBe(true);
|
||||
|
||||
expect(screen.getByRole("button", { name: "Fechar menu" })).toHaveAttribute(
|
||||
@@ -142,6 +147,125 @@ describe("Header: teclado no menu do celular", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Header: menu aberto, caminhos de borda do foco, do clique e da janela", () => {
|
||||
/** O cabeçalho com o conteúdo da página que o menu aberto torna inerte (id do layout). */
|
||||
function Page() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<div id="main-content" tabIndex={-1}>
|
||||
<button type="button">Botão do conteúdo</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
const openMenu = () =>
|
||||
fireEvent.click(screen.getByRole("button", { name: "Abrir menu" }));
|
||||
const content = () => document.getElementById("main-content") as HTMLElement;
|
||||
|
||||
afterEach(() => {
|
||||
document.documentElement.style.removeProperty("overflow");
|
||||
});
|
||||
|
||||
it("path: Tab with no control left to focus in the header -> the key is not blocked and the focus does not move", () => {
|
||||
render(<Page />);
|
||||
openMenu();
|
||||
const banner = screen.getByRole("banner");
|
||||
for (const button of banner.querySelectorAll("button"))
|
||||
button.setAttribute("disabled", "");
|
||||
for (const link of banner.querySelectorAll("a"))
|
||||
link.removeAttribute("href");
|
||||
act(() => document.body.focus());
|
||||
|
||||
expect(fireEvent.keyDown(document.body, { key: "Tab" })).toBe(true);
|
||||
expect(
|
||||
fireEvent.keyDown(document.body, { key: "Tab", shiftKey: true }),
|
||||
).toBe(true);
|
||||
|
||||
expect(document.body).toHaveFocus();
|
||||
});
|
||||
|
||||
it("path: click whose target is not an element (the document itself) -> the page stays locked", () => {
|
||||
render(<Page />);
|
||||
openMenu();
|
||||
|
||||
fireEvent.click(document);
|
||||
|
||||
expect(content()).toHaveAttribute("inert");
|
||||
expect(document.documentElement.style.overflow).toBe("hidden");
|
||||
});
|
||||
|
||||
it("path: click on something inside the header that is not a link -> the page stays locked", () => {
|
||||
render(<Page />);
|
||||
openMenu();
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole("navigation", { name: "Navegação mobile" }),
|
||||
);
|
||||
|
||||
expect(content()).toHaveAttribute("inert");
|
||||
expect(document.documentElement.style.overflow).toBe("hidden");
|
||||
});
|
||||
|
||||
it("path: click on a link inside the header -> the page is released at once", () => {
|
||||
render(<Page />);
|
||||
openMenu();
|
||||
|
||||
fireEvent.click(
|
||||
within(
|
||||
screen.getByRole("navigation", { name: "Navegação mobile" }),
|
||||
).getByRole("link", { name: "Cases" }),
|
||||
);
|
||||
|
||||
expect(content()).not.toHaveAttribute("inert");
|
||||
expect(document.documentElement.style.overflow).toBe("");
|
||||
});
|
||||
|
||||
it("path: no matchMedia at all in the browser -> the menu still opens and closes", () => {
|
||||
const original = window.matchMedia;
|
||||
window.matchMedia = undefined as unknown as typeof window.matchMedia;
|
||||
try {
|
||||
render(<Page />);
|
||||
openMenu();
|
||||
expect(
|
||||
screen.getByRole("navigation", { name: "Navegação mobile" }),
|
||||
).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Fechar menu" }));
|
||||
|
||||
expect(
|
||||
screen.queryByRole("navigation", { name: "Navegação mobile" }),
|
||||
).toBeNull();
|
||||
expect(content()).not.toHaveAttribute("inert");
|
||||
} finally {
|
||||
window.matchMedia = original;
|
||||
}
|
||||
});
|
||||
|
||||
it("path: matchMedia returns an object with no change listener (older browsers) -> the menu still opens and closes", () => {
|
||||
const original = window.matchMedia;
|
||||
window.matchMedia = jest.fn().mockReturnValue({
|
||||
matches: false,
|
||||
}) as unknown as typeof window.matchMedia;
|
||||
try {
|
||||
render(<Page />);
|
||||
openMenu();
|
||||
expect(
|
||||
screen.getByRole("navigation", { name: "Navegação mobile" }),
|
||||
).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Fechar menu" }));
|
||||
|
||||
expect(
|
||||
screen.queryByRole("navigation", { name: "Navegação mobile" }),
|
||||
).toBeNull();
|
||||
expect(content()).not.toHaveAttribute("inert");
|
||||
} finally {
|
||||
window.matchMedia = original;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Header: rota ausente", () => {
|
||||
it("path: usePathname gives null (outside the router) -> the header button goes to the home form", () => {
|
||||
jest.mocked(usePathname).mockReturnValue(null as unknown as string);
|
||||
|
||||
Reference in new issue
Block a user