Testing a custom React hook that shows an exit prompt - reactjs

I apologize if this question has been asked before. I did some searching but couldn't find an answer. I'm fairly new to React, so I may be missing something obvious.
I have inherited some TypeScript React code that includes a custom React hook to display a prompt when a user reloads or exits a page when there are edits on the page.
import { useState, useEffect } from 'react';
const initBeforeUnload = (showExitPrompt: boolean) => {
window.onbeforeunload = (event: BeforeUnloadEvent) => {
if (showExitPrompt) {
const e = event || window.event;
e.preventDefault();
if (e) {
e.returnValue = '';
}
return '';
}
};
};
// Hook
const useExitPrompt = (
bool: boolean
): [boolean, (showPrompt: boolean) => void] => {
const [showExitPrompt, setShowExitPrompt] = useState<boolean>(bool);
window.onload = () => {
initBeforeUnload(showExitPrompt);
};
useEffect(() => {
initBeforeUnload(showExitPrompt);
}, [showExitPrompt]);
useEffect(() => () => {
initBeforeUnload(false);
}, []);
return [showExitPrompt, setShowExitPrompt];
};
export default useExitPrompt;
No unit tests were included with this code when I inherited it, so I'm writing tests now. So far I've come up with the following:
/**
* #jest-environment jsdom
*/
import { renderHook, act } from '#testing-library/react-hooks';
import useExitPrompt from './useExitPrompt';
describe('useExitPrompt', () => {
it('should update the value of showExitPrompt', () => {
const { result } = renderHook(({ usePrompt }) => useExitPrompt(usePrompt),
{
initialProps: {
usePrompt: true
}
});
const [initialShowExitPrompt, setShowExitPrompt] = result.current;
expect(initialShowExitPrompt).toBe(true);
act(() => {
setShowExitPrompt(false);
});
const [updatedShowExitPrompt] = result.current;
expect(updatedShowExitPrompt).toBe(false);
});
});
This test passes, but it doesn't really get to the meat of what the hook is about: when showExitPrompt is true and the page is unloaded, is the exit prompt displayed? (In fact, it really only tests that the useState hook works, which is not what I want at all.) Unfortunately, I've run into a brick wall in trying to figure out how to test this; any suggestions?

Related

How test custom hooks with setTimeOut

We have a functionality, when the content is not visible on the screen then we scroll content till the end of content. here is the hooks i have written. quite new to testing Lib it would be great if someone have already written unit test for something like this.
import { useEffect, useRef } from 'react';
export default function useOnScreen() {
const ref = useRef<HTMLElement | null>();
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
const scrollToView = (timer: number) => {
const scrollRef = ref?.current;
if (ref) {
timeoutRef.current && clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => {
scrollRef?.current.scrollIntoView({
behavior: 'smooth',
block: 'end',
inline: 'end',
});
}, timer);
}
};
return [ref, scrollToView];
}
Need help here to write test case
import useOnScreen from '../useOnScreen';
import {renderHook, act} from '#testing-library/react-hooks';
describe('useOnScreen', () => {
it('useOnScreen', () => {
const [ref, scrollToView] = renderHook(() => useOnScreen());
});
});

Test custom hook parameters how many times it is rendered

I want to write Unit Test for the customhook useImageLoad. I want to check how many times "onImageLoad" parameter is called.
Initially I was using onImageLoad inside useEffect but it was rendering every seconds continously. So I have to use it with useCallback hook to run only once. Now its working fine as expected but I want to write a unit test which could get me how many times onImageLoad function has been called.
Below is the function:-
useImageLoad.ts
import { useEffect, useCallback, useState } from 'react';
export const useImageLoad = (src: string, onImageLoad: () => void = () => {}) => {
const [isImageLoaded, setImageIsLoaded] = useState(false);
useCallback(() => {
onImageLoad();
}, [onImageLoad]); // render 2 times
useEffect(() => {
const newImage = new Image();
newImage.src = src;
newImage.onload = () => {
setImageIsLoaded(true);
};
return () => {
newImage.onload = null;
};
}, [src]);
return isImageLoaded;
};
I am trying some way by looking into some tutorial but couldn't help myself how to excetute and run test.
useImageLoad.spec.ts
import React from 'react';
import { render } from '#testing-library/react';
import { useImageLoad } from './useImageLoad';
type HookWrapperProps = {
src: string;
onImageLoad: () => void;
};
// This is just a simple wrapper component since it's awkward to test hooks that rely on
// React lifecycle methods.
const HookWrapper = ({ src, onImageLoad }: HookWrapperProps) => {
const isImageLoaded = useImageLoad(src, onImageLoad);
console.log(isImageLoaded, 'isImageLoaded');
return <div>{isImageLoaded}</div>;
};
describe('UseImageLoad tests', () => {
it('should return a repository locator', async () => {
const result = render(<HookWrapper src="test" onImageLoad={() => {}} />);
});
});
Is it possible to achieve same with renderHook ?
I am totally noob in unit test. Excuse my code. Any help will be greatly appreciated. Thanks in advance.

Testing window with renderHooks

I have this custom hook:
import { useEffect } from 'react'
const useBeforeUnload = () => {
useEffect(() => {
const handleBeforeUnload = ev => {
console.log('Test')
}
window.addEventListener('beforeunload', handleBeforeUnload)
return () => window.removeEventListener('beforeunload', handleBeforeUnload)
}, [])
}
export default useBeforeUnload
and I'm trying to get a simple test to work that checks to see if window.addEventListener is called:
import { renderHook } from '#testing-library/react-hooks'
import useBeforeUnload from './useBeforeUnload'
const spy = jest.fn()
delete window.addEventListener
window.addEventListener = spy
describe('useBeforeUnload', () => {
describe('When the hook is initialised', () => {
beforeEach(() => {
renderHook(() => useBeforeUnload())
})
test('It should register the correct event listener', () => {
expect(spy).toHaveBeenCalledTimes(1)
})
})
})
but it always fails saying that the listener was called 6 times???
This is because react-dom is also adding event handlers (for the error event) and these are the handlers that increase your number of calls
One thing you could do is to assert against what you want it to add rather than how many times
expect(spy).toHaveBeenCalledWith("beforeunload",expect.anything());

unit test custom hook with jest and react testing library

I am trying to unit test a custom hook using jest and react testing library in scenario where error is thrown but I am not able to catch the actual error message, here is my code so far:
my first hook:
import react from 'react';
const useFirstHook = () => {
//I will add conditional logic later
throw new Error('my custom error is thrown')
const test1 = 'I am test 1';
return {
test1
};
};
export default useFirstHook;
test.js
import React from 'react';
import { render } from '#testing-library/react';
import useFirstHook from './useFirstHook';
describe('useFirstHook', () => {
//I also tried adding jest.spy but no luck
/* beforeAll(() => {
jest.spyOn(console, 'error').mockImplementation(() => {})
}); */
it('test 1', () => {
let result;
const TestComponent = () => {
result = useFirstHook()
return null;
};
render(<TestComponent />)
//expect()
});
});
my logic is to first create a hook, unit test it and then create component, add hook there and test that component with hook integration as well. what am I missing, or my approach is completely wrong ?
A good approach would be testing the component itself, that already contains the hook.
In case you consider that the hook needs to be test without the component, you can use #testing-library/react-hooks package, something like:
const useFirstHook = (shouldThrow = false) => {
// throw onmount
useEffect(() => {
if (shouldThrow) throw new Error('my custom error is thrown');
}, [shouldThrow]);
return {
test1: 'I am test 1'
};
};
describe('useFirstHook', () => {
it('should not throw', () => {
const { result } = renderHook(() => useFirstHook(false));
expect(result.current.test1).toEqual('I am test 1');
});
it('should throw', () => {
try {
const { result } = renderHook(() => useFirstHook(true));
expect(result.current).toBe(undefined);
} catch (err) {
expect(err).toEqual(Error('my custom error is thrown'));
}
});
});

Where should I use act when testing an async react hook?

When testing an async react hook with #testing-library/react-hooks I see an error message. The error message mentions wrapping code in act(...) but I'm not sure where I should do this.
I have tried to wrap parts of the code in act(...) but each attempt leads to the test failing.
// day.js
import { useState, useEffect } from 'react';
import { getDay } from '../api/day';
export function useDay() {
const [state, set] = useState({ loading: false });
useEffect(() => {
let canSet = true;
set({ loading: true });
const setDay = async () => {
const day = await getDay();
if (canSet) {
set(day);
}
};
setDay();
return () => (canSet = false);
}, []);
return state;
}
// day.test.js
import { renderHook, act } from "#testing-library/react-hooks";
import { useDay } from "./day";
jest.mock("../api/day", () => ({
getDay: jest.fn().mockReturnValue({ some: "value" })
}));
describe.only("model/day", () => {
it("returns data", async () => {
const { result, waitForNextUpdate } = renderHook(() => useDay());
await waitForNextUpdate();
expect(result.current).toEqual({ some: "value" });
});
});
// test output
console.error node_modules/react-test-renderer/cjs/react-test-renderer.development.js:102
Warning: An update to TestHook inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
This is a known issue: https://github.com/testing-library/react-testing-library/issues/281
Before 16.9.0-alpha.0 React itself didn't handle the async stuff pretty good, so that has nothing to do with the testing library, really. Read the comments of the issue if you're interested in that.
You have two options now:
Update your React (& react-dom) to 16.9.0-alpha.0
Add a snippet (e. g. in your test setup file) to suppress that warning when console.log tries to print it:
// FIXME Remove when we upgrade to React >= 16.9
const originalConsoleError = console.error;
console.error = (...args) => {
if (/Warning.*not wrapped in act/.test(args[0])) {
return;
}
originalConsoleError(...args);
};

Resources