Mock useSearchParams react testing library
Asked Answered
W

7

19

I have this custom hook

import { useLocation, useNavigate, useSearchParams } from "react-router-dom";

const useZRoutes = () => {
  const navigate = useNavigate();
  const { search, hash } = useLocation();
  const [searchParams, setSearchParams] = useSearchParams();

  return {
    getQueryParam: <T>(key: string): T | null => {
      if (searchParams.has(key)) {
        const value = searchParams.get(key);
        return value as unknown as T;
      }

      return null;
    },
    deleteQueryParam: (key: string): void => {
      if (searchParams.has(key)) {
        searchParams.delete(key);
        setSearchParams(searchParams);
      }
    },
    extendsNavigate: (pathname: string) => navigate({ pathname, search, hash }),
  };
};

export { useZRoutes };

Now, I need to test the getQueryParam function but I can't update the URL with query param.

I tried to mock useSearchParams with Object.defineProperty and with jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), // use actual for all non-hook parts useSearchParams: () => ("pid=123"), }));

and my test isn't passed. what should I do?

Wessex answered 9/3, 2022 at 20:39 Comment(0)
T
17

Instead of attempting to mock useSearchParams, I would recommend wrapping a component that is using your custom hook with a MemoryRouter and passing in your search params as initialEntries.

import { MemoryRouter } from 'react-router-dom'
import { render } from '@testing-library/react'

describe('My component using the useZRoutes hook', () => {    
  it('should do something', () => {
    render(
      <MemoryRouter initialEntries={["?pid=123"]}>
        <TestComponentUsingHook />
      </MemoryRouter>
    )
    // expect something
  })
})
Tirewoman answered 14/4, 2022 at 16:29 Comment(2)
This works for passing initial search params, but what if you wanted to assert search params changed?Arbe
@Arbe you can create another component SearchParamsObserver that internally uses useSearchParams and calls a callback whenever they change. Just render this component in your test next to the component you are testing.Ozell
M
4

you could just mock URLSearchParams, like so,

  jest.spyOn(
    URLSearchParams.prototype,
    'get'
  ).mockImplementation(
    (key) => "foo"
  );
Martinamartindale answered 26/10, 2022 at 16:41 Comment(1)
As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.Mahayana
A
3

Mock useSearchParams

const searchParams = { "pid": "123"};
jest.mock('react-router-dom', () => ({
  ...(jest.requireActual('react-router-dom') as object),
  useSearchParams: () => [searchParams]
}));
Alcides answered 16/8, 2022 at 22:14 Comment(1)
The important part of this answer is that you need to return an ARRAY. So you can't use jest.fn() to mock this. The simples way would be () => [].Illuminate
P
1

Passing initialEntries only string we will have this error in Typescript

Type 'string' is not assignable to type 'InitialEntry[] | undefined'.ts(2322)
components.d.ts(9, 5): 
The expected type comes from property 'initialEntries' 
which is declared here on type 'IntrinsicAttributes & MemoryRouterProps'

instead of string, you can pass

["/path?pid=123"]

Popp answered 12/7, 2022 at 17:50 Comment(0)
P
1

If you are seeking for TypeScript typesafe version: here is the mock that will work for react-router V6.4+ for both cases: initial urlParams and updates via setParams function that is returned as second argument

import { useState } from 'react'
import * as reactRouterDom from 'react-router-dom'

type SetParams = ReturnType<typeof reactRouterDom.useSearchParams>['1']

// from react-router-dom github
function createSearchParams(init: reactRouterDom.URLSearchParamsInit = ''): URLSearchParams {
  return new URLSearchParams(
    typeof init === 'string' || Array.isArray(init) || init instanceof URLSearchParams
      ? init
      : Object.keys(init).reduce<reactRouterDom.ParamKeyValuePair[]>((memo, key) => {
          const value = init[key]
          return memo.concat(Array.isArray(value) ? value.map((v) => [key, v]) : [[key, value]])
        }, []),
  )
}

export const useSearchParamsMock: typeof reactRouterDom.useSearchParams = (
  defaultInit: reactRouterDom.URLSearchParamsInit | undefined,
) => {
  const location = reactRouterDom.useLocation()
  const [params, setParams] = useState<URLSearchParams>(
    new URLSearchParams(createSearchParams(defaultInit ?? location.search)),
  )
  const setParamsMock: SetParams = (nextInit) => {
    const newSearchParams = createSearchParams(
      typeof nextInit === 'function' ? nextInit(params) : nextInit,
    )
    setParams(newSearchParams)
  }

  return [params, setParamsMock]
}

Then in the actual test you can use it with jest or vi (vitest mocking lib)

Vitests:

    vi.spyOn(reactRouterDom, 'useSearchParams').mockImplementation(useSearchParamsMock)

Jest

    jest.spyOn(reactRouterDom, 'useSearchParams').mockImplementation(useSearchParamsMock)
Pasargadae answered 7/7, 2023 at 16:20 Comment(0)
P
0

I was able to mock it like this. Enough to get my test to pass, but would love comments on how to elaborate on it.

import { useSearchParams } from 'next/navigation';

jest.mock('next/navigation', () => ({
  useSearchParams: jest.fn(),
}));

const mockGet = jest.fn();
mockGet.mockReturnValue('5000');

(useSearchParams as jest.Mock).mockReturnValue({
  get: mockGet,
});
Piggery answered 19/9, 2023 at 18:58 Comment(0)
G
0

After a long time trying many things, this works for me using mockImplementation function:

describe('test', () => {
  test('should ...', async () => {
    ...
    useSearchParams.mockImplementation(() => [new URLSearchParams({ autoplay: '1' })]);...
  });

});
Gonzalo answered 28/2, 2024 at 13:20 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.