Je veux m'assurer que mon composant s'affiche correctement avec le hook useRouter (en fait, j'essaie de comprendre comment fonctionne le nouveau routage dynamique), j'ai donc le code :
import React from 'react';
import { NextPage } from 'next';
import { useRouter } from 'next/router';
const UserInfo : NextPage = () => {
const router = useRouter();
const { query } = router;
return <div>Hello {query.user}</div>;
};
export default UserInfo;
Et ce que j'essaye c'est :
// test
import { render, cleanup, waitForElement } from '@testing-library/react';
import UserInfo from './$user';
// somehow mock useRouter for $user component!!!
afterEach(cleanup);
it('Should render correctly on route: /users/nikita', async () => {
const { getByText } = render(<UserInfo />);
await waitForElement(() => getByText(/Hello nikita!/i));
});
Mais j'obtiens une erreur TypeError: Cannot read property 'query' of null
qui pointe sur la ligne const router = useRouter();
.
PS Je sais que le routage dynamique est disponible sur les versions Canary pour le moment et pourrait changer, mais j'ai un problème avec le routeur, pas avec la fonction WIP (suis-je ?).
Bonjour, cette fonctionnalité est encore expérimentale mais useRouter
utilise React.useContext
pour consommer le contexte de next-server/dist/lib/router-context
. Pour vous en moquer, vous devrez l'envelopper dans le fournisseur de contexte similaire à cette ligne
@ijjk Salut, merci !
Je ne sais pas si je le fais bien, mais le test passe
import { render, cleanup, waitForElement } from '@testing-library/react';
import { createRouter } from 'next/router';
import { RouterContext } from 'next-server/dist/lib/router-context';
const router = createRouter('', { user: 'nikita' }, '', {
initialProps: {},
pageLoader: jest.fn(),
App: jest.fn(),
Component: jest.fn(),
});
import UserInfo from './$user';
afterEach(cleanup);
it('Should render correctly on route: /users/nikita', async () => {
const { getByText } = render(
<RouterContext.Provider value={router}>
<UserInfo />
</RouterContext.Provider>,
);
await waitForElement(() => getByText(/Hello nikita!/i));
});
S'il existe un moyen plus abstrait de simuler les paramètres de requête, je pourrais donc passer la route réelle ( /users/nikita
par exemple) et passer le chemin d'accès au fichier ? Qu'est-ce que tu penses?
Il peut être préférable de se moquer directement du routeur au lieu d'appeler createRouter
car cette API est interne et peut changer à tout moment. Voici un exemple :
import React from 'react'
import { render } from '@testing-library/react'
import { RouterContext } from 'next-server/dist/lib/router-context'
describe('Basic test', () => {
it('Renders current user value', async () => {
const router = {
pathname: '/users/$user',
route: '/users/$user',
query: { user: 'nikita' },
asPath: '/users/nikita',
}
const User = require('../pages/users/$user').default
const tree = render(
<RouterContext.Provider value={router}>
<User />
</RouterContext.Provider>
)
expect(tree.getByText('User: nikita')).toBeTruthy()
})
})
@ijjk qui a du sens. Merci beaucoup!
Existe-t-il un moyen de se moquer de useRouter en utilisant Enzyme + Jest ? J'ai cherché un peu en ligne et les seuls résultats pertinents qui se présentent sont ce problème.
J'ai réussi à m'en moquer de cette façon.
import * as nextRouter from 'next/router';
nextRouter.useRouter = jest.fn();
nextRouter.useRouter.mockImplementation(() => ({ route: '/' }));
jest.spyOn
fonctionne pour moi aussi -
import React from 'react'
import { render } from '@testing-library/react'
import ResultsProductPage from 'pages/results/[product]'
const useRouter = jest.spyOn(require('next/router'), 'useRouter')
describe('ResultsProductPage', () => {
it('renders - display mode list', () => {
useRouter.mockImplementationOnce(() => ({
query: { product: 'coffee' },
}))
const { container } = render(
<ResultsProductPage items={[{ name: 'mocha' }]} />
)
expect(container).toMatchSnapshot()
})
})
J'ai fini par me moquer comme ça, je n'ai besoin que de l'export useRouter
donc cela a assez bien fonctionné pour mes besoins :
jest.mock("next/router", () => ({
useRouter() {
return {
route: "/",
pathname: "",
query: "",
asPath: "",
};
},
}));
Si quelqu'un cherche à se moquer de useRouter
simplement pour éviter les interférences d'un impératif prefetch
, alors cette simple simulation fonctionnera
jest.mock("next/router", () => ({
useRouter() {
return {
prefetch: () => null
};
}
}));
un exemple de cas d'utilisation serait un composant de formulaire qui inclut quelque chose comme :
const router = useRouter();
useEffect(() => {
router.prefetch("/success");
if (confirmSuccess) {
doStuff();
router.push( {pathname: "/success" } )
}
}, [data]);
@ijjk Ce comportement a-t-il changé dans la dernière version ? J'ai dû importer à partir de next/dist/next-server/lib/router-context
. Il ne reconnaîtrait pas le contexte si j'installais next-server
séparément.
J'ai exactement le même problème.
Nous sommes sous 9. Aucune des solutions utilisant le RouterContext.Provider
ne fonctionne réellement.
La seule façon dont mon test réussit est d'utiliser la solution useRouter
est toujours indéfini.
Ce n'est pas idéal car je ne peux pas définir différents paramètres pour mon test.
Des idées à ce sujet ?
ÉDITER
Je l'ai fait fonctionner avec un simulacre global de l'import next/router
et un spyOn
sur le simulacre, ce qui me permet d'appeler mockImplementation(() => ({// whatever you want})
à chaque test.
Cela ressemble à quelque chose comme :
jest.mock("next/router", () => ({
useRouter() {
return {
route: "",
pathname: "",
query: "",
asPath: "",
};
},
}));
const useRouter = jest.spyOn(require("next/router"), "useRouter");
Ensuite dans les tests :
useRouter.mockImplementation(() => ({
route: "/yourRoute",
pathname: "/yourRoute",
query: "",
asPath: "",
}));
Ce n'est pas l'idéal mais au moins ça marche pour moi
FWIW c'est ce que j'ai choisi:
import { RouterContext } from 'next/dist/next-server/lib/router-context'
import { action } from '@storybook/addon-actions'
import PropTypes from 'prop-types'
import { useState } from 'react'
import Router from 'next/router'
function RouterMock({ children }) {
const [pathname, setPathname] = useState('/')
const mockRouter = {
pathname,
prefetch: () => {},
push: async newPathname => {
action('Clicked link')(newPathname)
setPathname(newPathname)
}
}
Router.router = mockRouter
return (
<RouterContext.Provider value={mockRouter}>
{children}
</RouterContext.Provider>
)
}
RouterMock.propTypes = {
children: PropTypes.node.isRequired
}
export default RouterMock
J'avais besoin de quelque chose qui fonctionne à la fois dans Storybook et dans Jest. Cela semble faire l'affaire, vous venez de définir <Routermock>
quelque part dans l'arborescence des composants. Ce n'est pas idéal parce que je n'aime pas écraser Router.router
permanence.
Je pense qu'une solution de moquerie officielle serait sympa :)
La méthode de @smasontst a fonctionné pour nous, mais soyez prudent avec mockImplementationOnce()
... si votre composant doit être rendu plus d'une fois pendant votre test, vous constaterez qu'il n'utilise pas votre faux routeur lors du deuxième rendu et votre test échouera. Il est probablement préférable de toujours utiliser mockImplementation()
place, à moins que vous n'ayez une raison spécifique d'utiliser mockImplementationOnce()
.
J'ai dû réviser mon implémentation initiale car j'avais besoin d'un état unique useRouter
sur une base test par test. A pris une page de l'exemple fourni par @nterol24s et l'a mise à jour pour agir comme une fonction utilitaire que je peux appeler dans mes tests :
// Mocks useRouter
const useRouter = jest.spyOn(require("next/router"), "useRouter");
/**
* mockNextUseRouter
* Mocks the useRouter React hook from Next.js on a test-case by test-case basis
*/
export function mockNextUseRouter(props: {
route: string;
pathname: string;
query: string;
asPath: string;
}) {
useRouter.mockImplementationOnce(() => ({
route: props.route,
pathname: props.pathname,
query: props.query,
asPath: props.asPath,
}));
}
Je peux maintenant faire des choses comme :
import { mockNextUseRouter } from "@src/test_util";
describe("Pricing Page", () => {
// Mocks Next.js route
mockNextUseRouter({
route: "/pricing",
pathname: "/pricing",
query: "",
asPath: `/pricing?error=${encodeURIComponent("Uh oh - something went wrong")}`,
});
test("render with error param", () => {
const tree: ReactTestRendererJSON = Renderer.create(
<ComponentThatDependsOnUseRouter />
).toJSON();
expect(tree).toMatchSnapshot();
});
});
Notez le commentaire de @mbrowne - vous rencontrerez le même problème avec cette approche, mais vous pouvez diviser l'exemple ci-dessus en fonctions mockNextUseRouter
et mockNextUseRouterOnce
si vous en avez besoin.
Aussi un GROS :+1: pour une solution de moquerie officielle @timneutkens
Pour tous ceux qui veulent une instance Router
moquée globalement , vous pouvez placer un dossier __mocks__
n'importe où et cibler le package next/router
comme suit :
__mocks__/next/router/index.js
(doit suivre ce modèle de structure de dossier !)
Cet exemple ci-dessous cible Router.push
et Router.replace
:
jest.mock("next/router", () => ({
// spread out all "Router" exports
...require.requireActual("next/router"),
// shallow merge the "default" exports with...
default: {
// all actual "default" exports...
...require.requireActual("next/router").default,
// and overwrite push and replace to be jest functions
push: jest.fn(),
replace: jest.fn(),
},
}));
// export the mocked instance above
module.exports = require.requireMock("next/router");
Maintenant, partout où il y a un import Router from "next/router";
ce sera l'instance simulée. Vous pourrez également leur ajouter des fonctions mockImplementation
car elles seront globalement moquées.
Si vous souhaitez que cette instance soit réinitialisée pour chaque test, ajoutez une propriété clearMocks dans votre jest.json
.
Pour référence, voici la structure Router
si vous souhaitez cibler une exportation spécifique :
{
__esModule: true,
useRouter: [Function: useRouter],
makePublicRouterInstance: [Function: makePublicRouterInstance],
default: {
router: null,
readyCallbacks: [
[Function],
[Function],
[Function],
[Function],
[Function],
[Function]
],
ready: [Function: ready],
push: [Function],
replace: [Function],
reload: [Function],
back: [Function],
prefetch: [Function],
beforePopState: [Function] },
withRouter: [Function: withRouter],
createRouter: [Function: createRouter],
Router: {
[Function: Router]
events: {
on: [Function: on],
off: [Function: off],
emit: [Function: emit]
}
},
NextRouter: undefined
}
}
De plus, si vous devez mount
composants qui utilisent withRouter
ou useRouter
et que vous ne voulez pas vous
import { createElement } from "react";
import { mount } from "enzyme";
import { RouterContext } from "next/dist/next-server/lib/router-context";
// Important note: The RouterContext import will vary based upon the next version you're using;
// in some versions, it's a part of the next package, in others, it's a separate package
/**
* Factory function to create a mounted RouterContext wrapper for a React component
*
* <strong i="33">@function</strong> withRouterContext
* <strong i="34">@param</strong> {node} Component - Component to be mounted
* <strong i="35">@param</strong> {object} initialProps - Component initial props for setup.
* <strong i="36">@param</strong> {object} state - Component initial state for setup.
* <strong i="37">@param</strong> {object} router - Initial route options for RouterContext.
* <strong i="38">@param</strong> {object} options - Optional options for enzyme's mount function.
* <strong i="39">@function</strong> createElement - Creates a wrapper around passed in component (now we can use wrapper.setProps on root)
* <strong i="40">@returns</strong> {wrapper} - a mounted React component with Router context.
*/
export const withRouterContext = (
Component,
initialProps = {},
state = null,
router = {
pathname: "/",
route: "/",
query: {},
asPath: "/",
},
options = {},
) => {
const wrapper = mount(
createElement(
props => (
<RouterContext.Provider value={router}>
<Component { ...props } />
</RouterContext.Provider>
),
initialProps,
),
options,
);
if (state) wrapper.find(Component).setState(state);
return wrapper;
};
Exemple d'utilisation :
import React from "react";
import withRouterContext from "./path/to/reusable/test/utils"; // alternatively you can make this global
import ExampleComponent from "./index";
const initialProps = {
id: "0123456789",
firstName: "John",
lastName: "Smith"
};
const router = {
pathname: "/users/$user",
route: "/users/$user",
query: { user: "john" },
asPath: "/users/john",
};
const wrapper = withRouterContext(ExampleComponent, initialProps, null, router);
...etc
Pourquoi utiliser ça ? Parce qu'il vous permet d'avoir un composant React monté réutilisable enveloppé dans un contexte de routeur ; et surtout, il vous permet d'appeler wrapper.setProps(..)
sur le composant racine !
import { useRouter } from 'next/router'
jest.mock('next/router', () => ({
__esModule: true,
useRouter: jest.fn()
}))
describe('XXX', () => {
it('XXX', () => {
const mockRouter = {
push: jest.fn() // the component uses `router.push` only
}
;(useRouter as jest.Mock).mockReturnValue(mockRouter)
// ...
expect(mockRouter.push).toHaveBeenCalledWith('/hello/world')
})
})
Aucune de ces solutions n'a fonctionné pour moi. Le workflow « correct » est également décrit ici dans les documents Jest : https://jestjs.io/docs/en/es6-class-mocks#spying -on-methods-of-our-class
Cependant, je peux voir la simulation, mais elle n'enregistre pas les appels...
Voici mon test-utils.tsx
actuel. J'aime beaucoup mieux cela que d'utiliser une maquette globale.
import React from 'react';
import { render as defaultRender } from '@testing-library/react';
import { RouterContext } from 'next/dist/next-server/lib/router-context';
import { NextRouter } from 'next/router';
export * from '@testing-library/react';
// --------------------------------------------------
// Override the default test render with our own
//
// You can override the router mock like this:
//
// const { baseElement } = render(<MyComponent />, {
// router: { pathname: '/my-custom-pathname' },
// });
// --------------------------------------------------
type DefaultParams = Parameters<typeof defaultRender>;
type RenderUI = DefaultParams[0];
type RenderOptions = DefaultParams[1] & { router?: Partial<NextRouter> };
export function render(
ui: RenderUI,
{ wrapper, router, ...options }: RenderOptions = {},
) {
if (!wrapper) {
wrapper = ({ children }) => (
<RouterContext.Provider value={{ ...mockRouter, ...router }}>
{children}
</RouterContext.Provider>
);
}
return defaultRender(ui, { wrapper, ...options });
}
const mockRouter: NextRouter = {
basePath: '',
pathname: '/',
route: '/',
asPath: '/',
query: {},
push: jest.fn(),
replace: jest.fn(),
reload: jest.fn(),
back: jest.fn(),
prefetch: jest.fn(),
beforePopState: jest.fn(),
events: {
on: jest.fn(),
off: jest.fn(),
emit: jest.fn(),
},
isFallback: false,
};
@flybayer merci ! Fonctionne très bien!
La solution de @flybayer fonctionne pour moi, mais je dois spécifier le type de retour sur la fonction de rendu
import { render as defaultRender, RenderResult } from '@testing-library/react'
...
export function render(
ui: RenderUI,
{ wrapper, router, ...options }: RenderOptions = {}
): RenderResult { ... }
Pour tous ceux qui veulent une instance
Router
moquée globalement , vous pouvez placer un dossier__mocks__
n'importe où et cibler le packagenext/router
comme suit :
__mocks__/next/router/index.js
(doit suivre ce modèle de structure de dossier !)Cet exemple ci-dessous cible
Router.push
etRouter.replace
:jest.mock("next/router", () => ({ // spread out all "Router" exports ...require.requireActual("next/router"), // shallow merge the "default" exports with... default: { // all actual "default" exports... ...require.requireActual("next/router").default, // and overwrite push and replace to be jest functions push: jest.fn(), replace: jest.fn(), }, })); // export the mocked instance above module.exports = require.requireMock("next/router");
Maintenant, partout où il y a un
import Router from "next/router";
ce sera l'instance simulée. Vous pourrez également leur ajouter des fonctionsmockImplementation
car elles seront globalement moquées.
Si vous souhaitez que cette instance soit réinitialisée pour chaque test, ajoutez une propriété clearMocks dans votrejest.json
.Pour référence, voici la structure
Router
si vous souhaitez cibler une exportation spécifique :{ __esModule: true, useRouter: [Function: useRouter], makePublicRouterInstance: [Function: makePublicRouterInstance], default: { router: null, readyCallbacks: [ [Function], [Function], [Function], [Function], [Function], [Function] ], ready: [Function: ready], push: [Function], replace: [Function], reload: [Function], back: [Function], prefetch: [Function], beforePopState: [Function] }, withRouter: [Function: withRouter], createRouter: [Function: createRouter], Router: { [Function: Router] events: { on: [Function: on], off: [Function: off], emit: [Function: emit] } }, NextRouter: undefined } }
De plus, si vous devez
mount
composants qui utilisentwithRouter
ouuseRouter
et que vous ne voulez pas vousimport { createElement } from "react"; import { mount } from "enzyme"; import { RouterContext } from "next/dist/next-server/lib/router-context"; // Important note: The RouterContext import will vary based upon the next version you're using; // in some versions, it's a part of the next package, in others, it's a separate package /** * Factory function to create a mounted RouterContext wrapper for a React component * * <strong i="33">@function</strong> withRouterContext * <strong i="34">@param</strong> {node} Component - Component to be mounted * <strong i="35">@param</strong> {object} initialProps - Component initial props for setup. * <strong i="36">@param</strong> {object} state - Component initial state for setup. * <strong i="37">@param</strong> {object} router - Initial route options for RouterContext. * <strong i="38">@param</strong> {object} options - Optional options for enzyme's mount function. * <strong i="39">@function</strong> createElement - Creates a wrapper around passed in component (now we can use wrapper.setProps on root) * <strong i="40">@returns</strong> {wrapper} - a mounted React component with Router context. */ export const withRouterContext = ( Component, initialProps = {}, state = null, router = { pathname: "/", route: "/", query: {}, asPath: "/", }, options = {}, ) => { const wrapper = mount( createElement( props => ( <RouterContext.Provider value={router}> <Component { ...props } /> </RouterContext.Provider> ), initialProps, ), options, ); if (state) wrapper.find(Component).setState(state); return wrapper; };
Exemple d'utilisation :
import React from "react"; import withRouterContext from "./path/to/reusable/test/utils"; // alternatively you can make this global import ExampleComponent from "./index"; const initialProps = { id: "0123456789", firstName: "John", lastName: "Smith" }; const router = { pathname: "/users/$user", route: "/users/$user", query: { user: "john" }, asPath: "/users/john", }; const wrapper = withRouterContext(ExampleComponent, initialProps, null, router); ...etc
Pourquoi utiliser ça ? Parce qu'il vous permet d'avoir un composant React monté réutilisable enveloppé dans un contexte de routeur ; et surtout, il vous permet d'appeler
wrapper.setProps(..)
sur le composant racine !
salut, j'obtiens cette erreur:
TypeError : require.requireMock n'est pas une fonction
UTILISÉ CETTE SOLUTION :
jest.mock("next/router", () => ({
// spread out all "Router" exports
...jest.requireActual("next/router"),
// shallow merge the "default" exports with...
default: {
// all actual "default" exports...
...jest.requireActual("next/router").default,
// and overwrite push and replace to be jest functions
push: jest.fn(),
replace: jest.fn(),
},
}));
// export the mocked instance above
module.exports = jest.requireMock("next/router");
Commentaire le plus utile
J'ai fini par me moquer comme ça, je n'ai besoin que de l'export
useRouter
donc cela a assez bien fonctionné pour mes besoins :