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 >) => ( {children} ), })); 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(Começar); 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(Começar); 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(Começar); 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(Começar); 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(Começar); 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(Começar); fireEvent.click(screen.getByRole("link", { name: "Começar" })); expect(replaceState).toHaveBeenCalledWith(routerState, "", "/#contact"); expect(window.history.state).toEqual(routerState); }); });