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.
190 lines
7.0 KiB
TypeScript
190 lines
7.0 KiB
TypeScript
import { fireEvent, render, screen } from "@testing-library/react";
|
|
import ContactOfferLink from "./ContactOfferLink";
|
|
import { focusFragment, onFragmentClick } from "./cinematic/fragment-nav";
|
|
import { CONTACT_KIND_EVENT } from "./contact-subject";
|
|
|
|
/**
|
|
* Testes de caminhos-base de ContactOfferLink (complemento de ContactOfferLink.test.tsx).
|
|
*
|
|
* onClick (CC 6): fora da home | sem #contact | clique já tratado pelo gerenciador de âncoras | tecla modificadora ou
|
|
* botão que não é o principal | rolagem suave | rolagem em salto (movimento reduzido). Os seis caminhos já estão
|
|
* em ContactOfferLink.test.tsx; aqui entram o que faltava:
|
|
* - o gerenciador de âncoras REAL (fragment-nav.onFragmentClick) tratando o clique, em vez de um ouvinte avulso;
|
|
* - Alt como tecla modificadora;
|
|
* - showContactInAddress (CC 2): o endereço já é /#contact, então não há replaceState; e o `history.state` do roteador
|
|
* do Next sobrevive ao replaceState.
|
|
*/
|
|
|
|
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 scrollIntoView = jest.fn();
|
|
const scrollTo = jest.fn();
|
|
let form: HTMLElement;
|
|
let replaceState: jest.SpyInstance;
|
|
const originalMatchMedia = window.matchMedia;
|
|
const originalScrollTo = window.scrollTo;
|
|
|
|
beforeEach(() => {
|
|
scrollIntoView.mockReset();
|
|
scrollTo.mockReset();
|
|
HTMLElement.prototype.scrollIntoView = scrollIntoView;
|
|
window.scrollTo = scrollTo as unknown as typeof window.scrollTo;
|
|
window.matchMedia = jest.fn().mockImplementation((query: string) => ({
|
|
matches: false,
|
|
media: query,
|
|
addEventListener: jest.fn(),
|
|
removeEventListener: jest.fn(),
|
|
}));
|
|
window.history.replaceState(null, "", "/");
|
|
replaceState = jest.spyOn(window.history, "replaceState");
|
|
form = document.createElement("section");
|
|
form.id = "contact";
|
|
document.body.append(form);
|
|
});
|
|
|
|
afterEach(() => {
|
|
replaceState.mockRestore();
|
|
form.remove();
|
|
window.matchMedia = originalMatchMedia;
|
|
window.scrollTo = originalScrollTo;
|
|
window.history.replaceState(null, "", "/");
|
|
});
|
|
|
|
/** O que useCinematicScroll faz com um clique numa âncora: cancela, empurra o hash, rola e dá o foco. */
|
|
function installAnchorManager() {
|
|
return onFragmentClick((hit, event) => {
|
|
event.preventDefault();
|
|
if (window.location.hash !== hit.hash)
|
|
window.history.pushState(null, "", hit.hash);
|
|
window.scrollTo({ top: 1234, behavior: "smooth" });
|
|
focusFragment(hit.target);
|
|
});
|
|
}
|
|
|
|
describe("ContactOfferLink: gerenciador de âncoras real", () => {
|
|
it("path: the manager takes the click -> its scroll is the only one, the offer cleans the old ?tipo= and still tells the form", () => {
|
|
window.history.replaceState(null, "", "/?tipo=site");
|
|
replaceState.mockClear();
|
|
const heard: unknown[] = [];
|
|
const listener = (event: Event) =>
|
|
heard.push((event as CustomEvent).detail);
|
|
window.addEventListener(CONTACT_KIND_EVENT, listener);
|
|
const stop = installAnchorManager();
|
|
try {
|
|
render(<ContactOfferLink kind="sistema">Começar</ContactOfferLink>);
|
|
const entries = window.history.length;
|
|
|
|
expect(
|
|
fireEvent.click(screen.getByRole("link", { name: "Começar" })),
|
|
).toBe(false);
|
|
|
|
expect(scrollTo).toHaveBeenCalledTimes(1);
|
|
expect(scrollIntoView).not.toHaveBeenCalled();
|
|
expect(form).toHaveFocus();
|
|
expect(heard).toEqual(["sistema"]);
|
|
// o gerenciador abriu uma entrada de histórico (/?tipo=site#contact); a oferta só a reescreve
|
|
expect(window.history.length).toBe(entries + 1);
|
|
expect(window.location.search).toBe("");
|
|
expect(window.location.hash).toBe("#contact");
|
|
expect(replaceState).toHaveBeenCalledTimes(1);
|
|
} finally {
|
|
stop();
|
|
window.removeEventListener(CONTACT_KIND_EVENT, listener);
|
|
}
|
|
});
|
|
|
|
it("path: the manager takes the click and the address is already /#contact -> no replaceState at all", () => {
|
|
window.history.replaceState(null, "", "/#contact");
|
|
replaceState.mockClear();
|
|
const stop = installAnchorManager();
|
|
try {
|
|
render(<ContactOfferLink kind="site">Começar</ContactOfferLink>);
|
|
|
|
fireEvent.click(screen.getByRole("link", { name: "Começar" }));
|
|
|
|
expect(scrollTo).toHaveBeenCalledTimes(1);
|
|
expect(replaceState).not.toHaveBeenCalled();
|
|
expect(window.location.pathname + window.location.hash).toBe("/#contact");
|
|
} finally {
|
|
stop();
|
|
}
|
|
});
|
|
|
|
it("path: no manager mounted -> the offer scrolls and focuses by itself, and a second click scrolls again", () => {
|
|
render(<ContactOfferLink kind="site">Começar</ContactOfferLink>);
|
|
const link = screen.getByRole("link", { name: "Começar" });
|
|
|
|
expect(fireEvent.click(link)).toBe(false);
|
|
expect(scrollIntoView).toHaveBeenCalledTimes(1);
|
|
expect(form).toHaveFocus();
|
|
expect(window.location.hash).toBe("#contact");
|
|
|
|
// o endereço já é /#contact (o navegador não rolaria): a oferta rola de novo mesmo assim, sem tocar no endereço
|
|
(document.activeElement as HTMLElement).blur();
|
|
replaceState.mockClear();
|
|
expect(fireEvent.click(link)).toBe(false);
|
|
expect(scrollIntoView).toHaveBeenCalledTimes(2);
|
|
expect(form).toHaveFocus();
|
|
expect(replaceState).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("ContactOfferLink: teclas e histórico", () => {
|
|
it("path: Alt held down -> not taken over (the browser keeps its own behavior)", () => {
|
|
render(<ContactOfferLink kind="site">Começar</ContactOfferLink>);
|
|
|
|
expect(
|
|
fireEvent.click(screen.getByRole("link", { name: "Começar" }), {
|
|
altKey: true,
|
|
}),
|
|
).toBe(true);
|
|
expect(scrollIntoView).not.toHaveBeenCalled();
|
|
expect(replaceState).not.toHaveBeenCalled();
|
|
expect(form).not.toHaveFocus();
|
|
});
|
|
|
|
it("path: a modified click still tells the form which type was chosen (the form may be in the same tab)", () => {
|
|
const heard: unknown[] = [];
|
|
const listener = (event: Event) =>
|
|
heard.push((event as CustomEvent).detail);
|
|
window.addEventListener(CONTACT_KIND_EVENT, listener);
|
|
try {
|
|
render(<ContactOfferLink kind="revisao">Começar</ContactOfferLink>);
|
|
|
|
fireEvent.click(screen.getByRole("link", { name: "Começar" }), {
|
|
ctrlKey: true,
|
|
});
|
|
|
|
expect(heard).toEqual(["revisao"]);
|
|
} finally {
|
|
window.removeEventListener(CONTACT_KIND_EVENT, listener);
|
|
}
|
|
});
|
|
|
|
it("path: replaceState keeps the history.state of the router (the Next router stores its tree there)", () => {
|
|
const routerState = { __NA: true, tree: ["", {}] };
|
|
window.history.replaceState(routerState, "", "/?tipo=site#contact");
|
|
replaceState.mockClear();
|
|
render(<ContactOfferLink kind="sistema">Começar</ContactOfferLink>);
|
|
|
|
fireEvent.click(screen.getByRole("link", { name: "Começar" }));
|
|
|
|
expect(replaceState).toHaveBeenCalledWith(routerState, "", "/#contact");
|
|
expect(window.history.state).toEqual(routerState);
|
|
});
|
|
});
|