skip to Main Content

I’m trying to test the debounce with Jest in a component I’ve made that uses URL params for a search. The handleSearch function uses the useDebounceCallback hook from usehooks-ts (which uses lodash debounce under the hood), and returns a Carbon component.

export function ParamSearch({
    defaultSearchValue,
    placeholder = 'Search',
}: {
    defaultSearchValue: string;
    placeholder: string;
}) {
    const router = useRouter();
    const pathname = usePathname();
    const searchParams = useSearchParams();
    
    const handleSearch = useDebounceCallback((event: '' | ChangeEvent<HTMLInputElement>) => {
        const { value } = event. target;
        const params = new URLSearchParams(searchParams);
    
        if (value === '') {
           params. delete( 'search');
        } else {
           params.set('search', value);
        }
    
        router.push(${pathname}?${params.toString()});
    }, 500);

    return (
        <TableToolbarSearch
            id="search"
            labelText={placeholder} 
            placeholder={placeholder}
            defaultValue={defaultSearchValue}
            onChange={handleSearch}
        />
    );
}

So far, my test looks like this. The first expect passes, but the second routerMock.push is never called.

jest-mock('usehooks-ts', () => ({
    useDebounceCallback: jest.fn((fn) => {
        setTimeout (() => fn, 500)
    }),
}));

describe('ParamSearch', () => {

    beforeEach(() => {
        jest.useFakeTimers();
    });

    afterEach(() => {
        jest.runOnlyPendingTimers(); 
        jest.useRealTimers();
    });

    it('calls handleSearch as debounced callback after delay', async () => { 
        usePathname.mockReturnValue('/test-search');
        useSearchParams.mockReturnValue(new Map([['search', '']]));
        const { getByPlaceholderText } = render(
            <ParamSearch placeholder={'Search'} defaultSearchValue={''} />
        );
        
        const searchInput = getByPlaceholderText('Search');
        fireEvent.change(searchInput, { target: { value: 'x'} });
        expect(routerMock.push).not.toHaveBeenCalled();
        
        jest.advanceTimersByTime(500);

        expect(routerMock.push).toHaveBeenCalledWith('/test-search?search=x');
    });
});

I feel like I’m doing something wrong with the fake timers, can anybody help?

2

Answers


  1. The mock to the useDebounce hook is unnecessary if you are using jest fake timer, it should work without mocking.

    Just remove these line:

    jest.mock('usehooks-ts', () => ({
        useDebounceCallback: jest.fn((fn) => {
            setTimeout (() => fn, 500)
        }),
    }));
    

    And I doubt that the setTimeout you provided to the mock function will not be affected by jest fake timer hijack process.

    Login or Signup to reply.
  2. There is no need to mock usehooks-ts module and useDebounceCallback hook. Use the real implementation of it. You should use fake timer. Then you can advance timer like this test case

    e.g.

    index.tsx:

    import React, { ChangeEvent } from 'react';
    import { useDebounceCallback } from 'usehooks-ts';
    
    export const ParamSearch = ({ defaultSearchValue, placeholder = 'Search' }: { defaultSearchValue: string; placeholder: string }) => {
      const pathname = '/test-search';
    
      const handleSearch = useDebounceCallback((event: ChangeEvent<HTMLInputElement>) => {
        const { value } = event.target;
        const params = new URLSearchParams();
    
        if (value === '') {
          params.delete('search');
        } else {
          params.set('search', value);
        }
    
        console.log(`${pathname}?${params.toString()}`);
      }, 500);
    
      return <input placeholder={placeholder} defaultValue={defaultSearchValue} onChange={handleSearch} />;
    };
    

    index.test.tsx:

    import { fireEvent, render } from '@testing-library/react';
    import React from 'react';
    import { ParamSearch } from '.';
    
    describe('ParamSearch', () => {
      beforeEach(() => {
        jest.useFakeTimers();
      });
    
      afterEach(() => {
        jest.useRealTimers();
      });
    
      it('calls handleSearch as debounced callback after delay', async () => {
        const logSpy = jest.spyOn(console, 'log').mockImplementation();
    
        const { getByPlaceholderText } = render(<ParamSearch placeholder={'Search'} defaultSearchValue={''} />);
    
        const searchInput = getByPlaceholderText('Search');
        fireEvent.change(searchInput, { target: { value: 'x' } });
        expect(logSpy).not.toHaveBeenCalled();
    
        jest.advanceTimersByTime(500);
    
        expect(logSpy).toHaveBeenCalledWith('/test-search?search=x');
      });
    });
    

    Test result:

    PASS  stackoverflow/78839472/index.test.tsx
      ParamSearch
        √ calls handleSearch as debounced callback after delay (22 ms)                                                                                                                                                       
                                                                                                                                                                                                                             
    Test Suites: 1 passed, 1 total                                                                                                                                                                                           
    Tests:       1 passed, 1 total                                                                                                                                                                                           
    Snapshots:   0 total
    Time:        0.971 s, estimated 9 s
    Ran all test suites related to changed files.
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search