test: dublê central de next/navigation para todos os testes

This commit is contained in:
Evandro Fonseca Junior committed 2026-10-14 22:54:18 -03:00
1 parent 2cd4a98c55
commit cf360041bd
2 files changed
+111

No files matched your search

+109
View File
@@ -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<string, string | string[]>;
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<Args extends unknown[], Result>(
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<Omit<NavigationState, "searchParams">> & {
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;`);
},
);
+2
View File
@@ -12,6 +12,8 @@ const config: Config = {
"^@/(.*)$": "<rootDir>/$1",
"^.+\\.(css|sass|scss)$": "identity-obj-proxy",
"^server-only$": "<rootDir>/lib/__mocks__/server-only.ts",
// dublê completo e central: nenhum teste monta um mock parcial de next/navigation
"^next/navigation$": "<rootDir>/__mocks__/next/navigation.ts",
},
testMatch: ["**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)"],
transform: {