diff --git a/__mocks__/next/navigation.ts b/__mocks__/next/navigation.ts new file mode 100644 index 0000000..2363915 --- /dev/null +++ b/__mocks__/next/navigation.ts @@ -0,0 +1,109 @@ +/** + * 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;`); + }, +); diff --git a/jest.config.ts b/jest.config.ts index 85189f0..3d3ce2f 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -12,6 +12,8 @@ const config: Config = { "^@/(.*)$": "/$1", "^.+\\.(css|sass|scss)$": "identity-obj-proxy", "^server-only$": "/lib/__mocks__/server-only.ts", + // dublê completo e central: nenhum teste monta um mock parcial de next/navigation + "^next/navigation$": "/__mocks__/next/navigation.ts", }, testMatch: ["**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)"], transform: {