Next.js: كيف يسخر useRouter؟

تم إنشاؤها على ١ يونيو ٢٠١٩  ·  21تعليقات  ·  مصدر: vercel/next.js

سؤال حول Next.js

أريد أن أتأكد من أن المكون الخاص بي يتم عرضه بشكل صحيح باستخدام خطاف useRouter (في الواقع أحاول فهم كيفية عمل التوجيه الديناميكي الجديد) ، لذلك لدي رمز:

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;

وما أحاول هو:

// 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));
});

لكني تلقيت خطأ TypeError: Cannot read property 'query' of null يشير إلى سطر const router = useRouter(); .

ملاحظة: أعلم أن التوجيه الديناميكي متاح في إصدارات الكناري فقط في الوقت الحالي وقد يتغير ، لكني واجهت مشكلة مع جهاز التوجيه ، وليس مع ميزة ويب (هل أنا؟).

التعليق الأكثر فائدة

انتهى بي الأمر بالسخرية من الأمر على هذا النحو ، فأنا بحاجة فقط إلى تصدير useRouter لذلك نجح هذا بشكل كافٍ لتحقيق أغراضي:

jest.mock("next/router", () => ({
    useRouter() {
        return {
            route: "/",
            pathname: "",
            query: "",
            asPath: "",
        };
    },
}));

ال 21 كومينتر

مرحبًا ، لا تزال هذه الميزة تجريبية ولكن useRouter يستخدم React.useContext لاستهلاك السياق من next-server/dist/lib/router-context . للسخرية منه ، ستحتاج إلى لفه في موفر السياق من هناك على غرار هذا السطر

ijjk مرحبا ، شكرا لك!
لا أعرف ما إذا كنت أفعل ذلك بشكل صحيح ، لكن الاختبار يجتاز 😂

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));
});

إذا كانت هناك طريقة أكثر تجريدية لسخرية معلمات الاستعلام ، لذلك سأكون قادرًا على تمرير المسار الفعلي ( /users/nikita على سبيل المثال) وتمرير المسار إلى الملف؟ ماذا تعتقد؟

قد يكون من الأفضل السخرية من جهاز التوجيه مباشرةً بدلاً من استدعاء createRouter نظرًا لأن واجهة برمجة التطبيقات هذه داخلية ويمكن تغييرها في أي وقت. هذا مثال:

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 هذا منطقي. شكرا جزيلا!

هل هناك أي طريقة للسخرية من useRouter باستخدام Enzyme + Jest؟ لقد كنت أبحث على الإنترنت قليلاً والنتائج ذات الصلة الوحيدة التي ظهرت هي هذه المشكلة.

تمكنت من السخرية منه بهذه الطريقة.

import * as nextRouter from 'next/router';

nextRouter.useRouter = jest.fn();
nextRouter.useRouter.mockImplementation(() => ({ route: '/' }));

يعمل لدي أيضًا jest.spyOn -

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()
  })
})

انتهى بي الأمر بالسخرية من الأمر على هذا النحو ، فأنا بحاجة فقط إلى تصدير useRouter لذلك نجح هذا بشكل كافٍ لتحقيق أغراضي:

jest.mock("next/router", () => ({
    useRouter() {
        return {
            route: "/",
            pathname: "",
            query: "",
            asPath: "",
        };
    },
}));

إذا كان أي شخص هنا يتطلع إلى السخرية من useRouter ببساطة لتجنب التدخل من أمر حتمي prefetch ، فعندئذ ستعمل هذه الوهمية البسيطة الميتة

jest.mock("next/router", () => ({
  useRouter() {
    return {
      prefetch: () => null
    };
  }
}));

مثال على حالة الاستخدام هو مكون نموذج يتضمن شيئًا مثل:

  const router = useRouter();
  useEffect(() => {
    router.prefetch("/success");
    if (confirmSuccess) {
      doStuff();
      router.push( {pathname: "/success" } )
    }
  }, [data]);

ijjk هل تغير هذا السلوك في الإصدار الأخير؟ اضطررت إلى الاستيراد من next/dist/next-server/lib/router-context . لن يتعرف على السياق إذا قمت بتثبيت next-server بشكل منفصل.

لدي المشكلة بالضبط نفس.
نحن أقل من 9. التالي لا يوجد أي من الحلول التي تستخدم RouterContext.Provider تعمل بالفعل.
الطريقة الوحيدة لنجاحي في الاختبار هي استخدام حل useRouter يكون دائمًا غير معرف.
هذا ليس مثاليًا لأنني لا أستطيع تعيين معلمات مختلفة للاختبار الخاص بي.
أي أفكار حول هذا؟

تعديل
لقد جعلتها تعمل مع محاكاة عالمية لاستيراد next/router و spyOn على المحاكاة ، مما يسمح لي بالاتصال بـ mockImplementation(() => ({// whatever you want}) في كل اختبار.
يبدو شيئًا مثل:

jest.mock("next/router", () => ({
  useRouter() {
    return {
      route: "",
      pathname: "",
      query: "",
      asPath: "",
    };
  },
}));

const useRouter = jest.spyOn(require("next/router"), "useRouter");

ثم في الاختبارات:

useRouter.mockImplementation(() => ({
      route: "/yourRoute",
      pathname: "/yourRoute",
      query: "",
      asPath: "",
    }));

هذا ليس مثاليًا ولكنه يعمل بالنسبة لي على الأقل

FWIW هذا ما استقرت عليه:

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

كنت بحاجة إلى شيء نجح في كل من Storybook و Jest. يبدو أن هذا يفي بالغرض ، لقد قمت للتو بتعيين <Routermock> مكان ما أعلى شجرة المكون. إنه ليس مثاليًا لأنني لا أحب تجاوز Router.router باستمرار.

أعتقد أن الحل الرسمي الساخر سيكون رائعًا :)

نجحت طريقة mockImplementationOnce() ... إذا احتاج المكون الخاص بك إلى العرض أكثر من مرة أثناء الاختبار ، فستجد أنه لا يستخدم جهاز التوجيه الوهمي الخاص بك في العرض الثاني وسوف يفشل اختبارك. من الأفضل دائمًا استخدام mockImplementation() بدلاً من ذلك ، إلا إذا كان لديك سبب محدد لاستخدام mockImplementationOnce() .

اضطررت إلى مراجعة تطبيقي الأولي لأنني كنت بحاجة إلى حالة فريدة useRouter على أساس اختبار تلو الآخر. أخذت صفحة من المثال المقدم من @ nterol24s وقم بتحديثها لتكون بمثابة وظيفة مساعدة يمكنني الاتصال بها في اختباراتي:

// 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,
    }));
}

يمكنني الآن القيام بأشياء مثل:

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();
    });
});

لاحظ تعليق mbrowne - mockNextUseRouter و mockNextUseRouterOnce إذا احتجت إلى ذلك.

أيضًا كبير: +1: لحل رسمي يسخر من timneutkens

بالنسبة لأي شخص يريد مثيل Router الاستهزاء به عالميًا ، يمكنك وضع مجلد __mocks__ أي مكان واستهداف الحزمة next/router كما يلي:

__mocks__/next/router/index.js (يجب أن يتبع نمط بنية المجلد هذا!)

يستهدف هذا المثال أدناه Router.push و 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");

الآن ، في أي مكان يوجد فيه import Router from "next/router"; ، سيكون المثال السخرية. ستتمكن أيضًا من إضافة وظائف mockImplementation عليها نظرًا لأنه سيتم السخرية منها عالميًا.
إذا كنت تريد إعادة تعيين هذا المثيل لكل اختبار ، فأضف خاصية jest.json الخاص بك.

كمرجع ، إليك بنية Router إذا كنت تريد استهداف تصدير معين:

{
  __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 
  }
}

بالإضافة إلى ذلك ، إذا كان عليك أن تستخدم مكونات mount withRouter أو useRouter ولا تريد السخرية منهم ولكنك لا تزال تريد إنشاء بعض الاختبارات ضد / حول منهم ، ثم يمكنك الاستفادة من وظيفة مصنع المجمع HOC للاختبار:

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;
};

استخدام المثال:

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

لماذا تستخدم هذا؟ لأنه يسمح لك بالحصول على مكون React قابل لإعادة الاستخدام وملفوف في سياق جهاز التوجيه ؛ والأهم من ذلك ، أنه يسمح لك باستدعاء wrapper.setProps(..) على مكون الجذر!

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')
  })
})

لم يعمل أي من هذه الحلول بالنسبة لي. سير العمل "الصحيح" موصوف هنا أيضًا في مستندات Jest: https://jestjs.io/docs/en/es6-class-mocks#spying -on-methods-of-our-class

ومع ذلك ، أستطيع أن أرى الصورة وهمية ، لكنها لا تسجل المكالمات ...

هذا هو test-utils.tsx الحالي الخاص بي. أنا أحب هذا أفضل بكثير من استخدام محاكاة عالمية.

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,
};

faybayer شكرا! يعمل بشكل رائع!

حل flybayer يناسبني ، ولكن علي تحديد نوع الإرجاع في وظيفة التصيير

import { render as defaultRender, RenderResult } from '@testing-library/react'

...

export function render(
  ui: RenderUI,
  { wrapper, router, ...options }: RenderOptions = {}
): RenderResult { ... }

بالنسبة لأي شخص يريد مثيل Router الاستهزاء به عالميًا ، يمكنك وضع مجلد __mocks__ أي مكان واستهداف الحزمة next/router كما يلي:

__mocks__/next/router/index.js (يجب أن يتبع نمط بنية المجلد هذا!)

يستهدف هذا المثال أدناه Router.push و 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");

الآن ، في أي مكان يوجد فيه import Router from "next/router"; ، سيكون المثال السخرية. ستتمكن أيضًا من إضافة وظائف mockImplementation عليها نظرًا لأنه سيتم السخرية منها عالميًا.
إذا كنت تريد إعادة تعيين هذا المثيل لكل اختبار ، فأضف خاصية jest.json الخاص بك.

كمرجع ، إليك بنية Router إذا كنت تريد استهداف تصدير معين:

{
  __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 
  }
}

بالإضافة إلى ذلك ، إذا كان عليك أن تستخدم مكونات mount withRouter أو useRouter ولا تريد السخرية منهم ولكنك لا تزال تريد إنشاء بعض الاختبارات ضد / حول منهم ، ثم يمكنك الاستفادة من وظيفة مصنع المجمع HOC للاختبار:

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;
};

استخدام المثال:

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

لماذا تستخدم هذا؟ لأنه يسمح لك بالحصول على مكون React قابل لإعادة الاستخدام وملفوف في سياق جهاز التوجيه ؛ والأهم من ذلك ، أنه يسمح لك باستدعاء wrapper.setProps(..) على مكون الجذر!

مرحبًا ، أتلقى هذا الخطأ:

TypeError: لا تعد وظيفة required.requireMock دالة

استخدم هذا الحل:

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");
هل كانت هذه الصفحة مفيدة؟
0 / 5 - 0 التقييمات

القضايا ذات الصلة

knipferrc picture knipferrc  ·  3تعليقات

sospedra picture sospedra  ·  3تعليقات

wagerfield picture wagerfield  ·  3تعليقات

havefive picture havefive  ·  3تعليقات

lixiaoyan picture lixiaoyan  ·  3تعليقات