import { render, screen } from "@testing-library/react"; import { useEffect, useState } from "react"; import { setNavigation } from "@/__mocks__/next/navigation"; import { resetFocusOnNavigation } from "./hooks/useFocusOnNavigation"; import Template from "./template"; const mockMounts = jest.fn(); const mockProps: Array> = []; // o React estável do Jest não exporta (só o canary do Next); este dublê conta as montagens jest.mock("react", () => { const actual = jest.requireActual("react"); function ViewTransition({ children, ...props }: { children?: React.ReactNode } & Record) { mockProps.push(props); actual.useEffect(() => { mockMounts(); }, []); return actual.createElement("div", { "data-testid": "vt" }, children); } return { ...actual, ViewTransition }; }); function Counter() { const [count, setCount] = useState(0); useEffect(() => { setCount(1); }, []); return

{count}

; } beforeEach(() => { setNavigation({ pathname: "/cases" }); mockMounts.mockClear(); mockProps.length = 0; resetFocusOnNavigation(); }); /** O contêiner que o layout põe em volta da página: o destino do foco depois da troca. */ function Shell({ children }: { children: React.ReactNode }) { return (
); } describe("app/template", () => { it("wraps the page in a ViewTransition of class page, without a name, that does not animate in-place updates", () => { render( , ); expect(screen.getByTestId("vt")).toContainElement(screen.getByRole("main")); expect(mockProps[0]).toMatchObject({ default: "page", update: "none" }); // sem nome: a página que sai e a que entra não formam par, e cada uma fica onde estava na tela expect(mockProps[0]).not.toHaveProperty("name"); }); it("remounts the page (one exit, one entry) when only the second segment changes (/cases -> /cases/um-projeto)", () => { const { rerender } = render( , ); expect(mockMounts).toHaveBeenCalledTimes(1); expect(screen.getByTestId("state")).toHaveTextContent("1"); setNavigation({ pathname: "/cases/sev-modaintima" }); rerender( , ); // a chave é o caminho: a transição desmonta a página que sai e monta a que entra (e o estado dela recomeça) expect(mockMounts).toHaveBeenCalledTimes(2); }); it("does not remount when the path stays the same (only a re-render, or a #fragment change)", () => { const { rerender } = render( , ); rerender( , ); expect(mockMounts).toHaveBeenCalledTimes(1); }); it("leaves the focus alone on the first load and moves it to #main-content, without scrolling, when the path changes", () => { const scrollTo = jest.spyOn(window, "scrollTo"); const focus = jest.spyOn(HTMLElement.prototype, "focus"); const { rerender } = render( , ); expect(focus).not.toHaveBeenCalled(); expect(screen.getByTestId("main")).not.toHaveFocus(); setNavigation({ pathname: "/cases/sev-modaintima" }); rerender( , ); expect(screen.getByTestId("main")).toHaveFocus(); expect(focus).toHaveBeenCalledWith({ preventScroll: true }); expect(scrollTo).not.toHaveBeenCalled(); focus.mockRestore(); scrollTo.mockRestore(); }); it("does not take the focus on a re-render of the same path", () => { const { rerender } = render(

Página

, ); rerender(

Página de novo

, ); expect(screen.getByTestId("main")).not.toHaveFocus(); }); it("never starts a view transition by itself, on the first load or on the focus move", () => { const start = jest.fn(); Object.defineProperty(document, "startViewTransition", { configurable: true, value: start, }); const { rerender } = render(

Página

, ); setNavigation({ pathname: "/servicos" }); rerender(

Página

, ); expect(start).not.toHaveBeenCalled(); Reflect.deleteProperty(document, "startViewTransition"); }); });