javascriptreactjstypescriptnext.jsjestjs

Testing useDebounceCallback hook in Jest


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?


Solution

  • 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.