/** * Dublê central e completo de `next/navigation` para o Jest (jest.config.ts: moduleNameMapper). Todo teste que importa * o módulo, direta ou indiretamente (HoverPrefetchLink, Header, páginas com `notFound`), recebe este arquivo: nenhum * teste precisa montar um `jest.mock("next/navigation", ...)` parcial, e nenhum componente vê o módulo sem um export. * * Estado: `usePathname`, `useSearchParams`, `useParams` e `useSelectedLayoutSegment(s)` leem `navigationState`. * Troque-o com `setNavigation({ pathname: "/cases" })` (ou `jest.mocked(usePathname).mockReturnValue("/cases")`) e * volte ao padrão ("/", sem parâmetros) com `resetNavigation()` num `beforeEach`. * Erros: `notFound` e `redirect` lançam como os do Next (com `digest`), e `notFound` lança com a mensagem * "NEXT_NOT_FOUND", que os testes das páginas conferem. */ type Params = Record; interface NavigationState { pathname: string | null; searchParams: URLSearchParams; params: Params; segments: string[]; } const initialState = (): NavigationState => ({ pathname: "/", searchParams: new URLSearchParams(), params: {}, segments: [], }); export const navigationState: NavigationState = initialState(); /** Cada `jest.fn` com a implementação padrão, para o `resetNavigation` devolvê-la depois de um `mockReset`. */ const defaults: Array<[jest.Mock, (...args: never[]) => unknown]> = []; function mockFn( implementation: (...args: Args) => Result, ) { const fn = jest.fn(implementation); defaults.push([fn as jest.Mock, implementation as never]); return fn; } export function setNavigation( next: Partial> & { searchParams?: string | URLSearchParams; }, ) { const { searchParams, ...rest } = next; Object.assign(navigationState, rest); if (searchParams !== undefined) navigationState.searchParams = new URLSearchParams(searchParams); } export function resetNavigation() { Object.assign(navigationState, initialState()); for (const [fn, implementation] of defaults) { fn.mockReset(); fn.mockImplementation(implementation); } } export const router = { push: mockFn(() => undefined), replace: mockFn(() => undefined), back: mockFn(() => undefined), forward: mockFn(() => undefined), refresh: mockFn(() => undefined), prefetch: mockFn(() => undefined), }; export const usePathname = mockFn(() => navigationState.pathname); export const useRouter = mockFn(() => router); export const useSearchParams = mockFn(() => navigationState.searchParams); export const useParams = mockFn(() => navigationState.params); export const useSelectedLayoutSegments = mockFn(() => navigationState.segments); export const useSelectedLayoutSegment = mockFn( () => navigationState.segments[0] ?? null, ); export const useServerInsertedHTML = mockFn(() => undefined); export const unstable_isUnrecognizedActionError = mockFn(() => false); export const unstable_rethrow = mockFn((error: unknown): void => { throw error; }); export const RedirectType = { push: "push", replace: "replace" } as const; export const ReadonlyURLSearchParams = URLSearchParams; /** Erro com `digest`, como os que o Next lança para interromper a renderização. */ function nextError(message: string, digest: string): never { throw Object.assign(new Error(message), { digest }); } export const notFound = mockFn((): never => { return nextError("NEXT_NOT_FOUND", "NEXT_HTTP_ERROR_FALLBACK;404"); }); export const forbidden = mockFn((): never => { return nextError("NEXT_FORBIDDEN", "NEXT_HTTP_ERROR_FALLBACK;403"); }); export const unauthorized = mockFn((): never => { return nextError("NEXT_UNAUTHORIZED", "NEXT_HTTP_ERROR_FALLBACK;401"); }); export const redirect = mockFn( (url: string, type: "push" | "replace" = "replace"): never => { return nextError("NEXT_REDIRECT", `NEXT_REDIRECT;${type};${url};307;`); }, ); export const permanentRedirect = mockFn( (url: string, type: "push" | "replace" = "replace"): never => { return nextError("NEXT_REDIRECT", `NEXT_REDIRECT;${type};${url};308;`); }, );