test: dublê central de next/navigation para todos os testes
This commit is contained in:
1 parent
2cd4a98c55
commit
cf360041bd
2 files changed
+111
No files matched your search
@@ -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;`);
|
||||
},
|
||||
);
|
||||
@@ -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: {
|
||||
|
||||
Reference in new issue
Block a user