How to test this custom hook (useRef) - reactjs

I'm trying to test this custom hook but I don't know how to send the useRef as a parameter
ElementRef is using useRef
import { MutableRefObject, useEffect, useState } from "react";
export default function useNearScreen(elementRef: MutableRefObject<HTMLDivElement>, margin = 80) {
const [show, setShow] = useState(false);
useEffect(() => {
const onChange = (entries: IntersectionObserverEntry[]) => {
const el: IntersectionObserverEntry = entries[0];
if (el.isIntersecting) {
setShow(true)
observer.disconnect();
}
}
const observer = new IntersectionObserver(onChange, {
rootMargin: `${margin}px`
})
observer.observe(elementRef.current as Element);
return () => observer.disconnect();
})
return show;
}

First of all, jest use jsdom as its test environment by default. jsdom doesn't support IntersectionObserver, see issue#2032. So we need to mock it and trigger the callback manually.
I will use #testing-library/react-hooks package to test react custom hook.
E.g.
useNearScreen.ts:
import { MutableRefObject, useEffect, useState } from 'react';
export default function useNearScreen(elementRef: MutableRefObject<HTMLDivElement>, margin = 80) {
const [show, setShow] = useState(false);
useEffect(() => {
const onChange = (entries: IntersectionObserverEntry[], observer: IntersectionObserver) => {
const el: IntersectionObserverEntry = entries[0];
if (el.isIntersecting) {
setShow(true);
observer.disconnect();
}
};
const observer = new IntersectionObserver(onChange, {
rootMargin: `${margin}px`,
});
observer.observe(elementRef.current as Element);
return () => observer.disconnect();
});
return show;
}
useNearScreen.test.ts:
import { renderHook } from '#testing-library/react-hooks';
import { MutableRefObject } from 'react';
import { useRef } from 'react';
import useNearScreen from './useNearScreen';
describe('useNearScreen', () => {
test('should pass', () => {
const mObserver = {
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn(),
};
const mIntersectionObserver = jest.fn();
mIntersectionObserver.mockImplementation((callback, options) => {
callback([{ isIntersecting: true }], mObserver);
return mObserver;
});
window.IntersectionObserver = mIntersectionObserver;
const mHTMLDivElement = document.createElement('div');
const { result } = renderHook(() => {
const elementRef = useRef<HTMLDivElement>(mHTMLDivElement);
return useNearScreen(elementRef as MutableRefObject<HTMLDivElement>);
});
expect(result.current).toBe(true);
expect(mIntersectionObserver).toBeCalledWith(expect.any(Function), { rootMargin: '80px' });
expect(mObserver.observe).toBeCalledWith(mHTMLDivElement);
expect(mObserver.disconnect).toBeCalled();
});
});
Test result:
PASS stackoverflow/71118856/useNearScreen.test.ts (9.656 s)
useNearScreen
✓ should pass (16 ms)
------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
------------------|---------|----------|---------|---------|-------------------
All files | 100 | 66.67 | 100 | 100 |
useNearScreen.ts | 100 | 66.67 | 100 | 100 | 9
------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 10.581 s

Related

Jest test for scrolling events does not appear to be calling the callback

This is my ScrollWatcher.tsx component
import { useEffect } from 'react';
interface Props {
onReachBottom: () => void;
}
export const ScrollWatcher = ({ onReachBottom }: Props) => {
useEffect(() => {
const handleScroll = () => {
if (
window.innerHeight + document.documentElement.scrollTop ===
document.documentElement.offsetHeight
) {
onReachBottom();
}
};
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, [onReachBottom]);
return <div />;
};
This is my test
import '#testing-library/jest-dom';
import { fireEvent, render } from '#testing-library/react';
import { ScrollWatcher } from './ScrollWatcher';
describe('ScrollWatcher', () => {
it('should call the onReachBottom function when the user scrolls to the bottom of the page', () => {
const onReachBottom = jest.fn();
const { container } = render(<ScrollWatcher onReachBottom={onReachBottom} />);
const scrollableContainer = container.parentElement;
fireEvent.scroll(scrollableContainer, { target: { scrollingElement: { scrollTop: 100 } } });
expect(onReachBottom).not.toHaveBeenCalled();
fireEvent.scroll(scrollableContainer, {
target: { scrollingElement: { scrollTop: scrollableContainer.scrollHeight } },
});
expect(onReachBottom).toHaveBeenCalled();
});
});
And if I mount this component on my page and scroll to the bottom it works. But if I do it in the test I'm getting this
expect(jest.fn()).toHaveBeenCalled()
Expected number of calls: >= 1
Received number of calls: 0
Is my test written incorrectly? What's the appropriate way to test this?
Jestjs use JSDOM underly to simulate a browser-like environment. But JSDOM has not implemented a layout system yet. See Unimplemented parts of the web platform, issues#2843 and issues#135.
So you have to mock the window.innerHeight, document.documentElement.scrollTop, and document.documentElement.offsetHeight properties.
E.g.
ScrollWatcher.tsx:
import React from 'react';
import { useEffect } from 'react';
interface Props {
onReachBottom: () => void;
}
export const ScrollWatcher = ({ onReachBottom }: Props) => {
useEffect(() => {
const handleScroll = () => {
if (window.innerHeight + document.documentElement.scrollTop === document.documentElement.offsetHeight) {
onReachBottom();
}
};
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, [onReachBottom]);
return <div />;
};
ScrollWatcher.test.tsx:
import React from 'react';
import { render } from '#testing-library/react';
import '#testing-library/jest-dom';
import { ScrollWatcher } from './ScrollWatcher';
function scroll(scrollTop, offsetHeight) {
const event = document.createEvent('Event');
event.initEvent('scroll', true, true);
Object.defineProperty(document.documentElement, 'scrollTop', {
writable: true,
configurable: true,
value: scrollTop,
});
Object.defineProperty(document.documentElement, 'offsetHeight', {
writable: true,
configurable: true,
value: offsetHeight,
});
window.dispatchEvent(event);
}
describe('ScrollWatcher', () => {
it('should call the onReachBottom function when the user scrolls to the bottom of the page', () => {
const onReachBottom = jest.fn();
Object.defineProperty(window, 'innerHeight', {
writable: true,
configurable: true,
value: 50,
});
render(<ScrollWatcher onReachBottom={onReachBottom} />);
scroll(0, 100);
expect(onReachBottom).not.toHaveBeenCalled();
scroll(50, 100);
expect(onReachBottom).toHaveBeenCalled();
});
});
Test result:
PASS stackoverflow/75392262/ScrollWatcher.test.tsx (12.661 s)
ScrollWatcher
✓ should call the onReachBottom function when the user scrolls to the bottom of the page (21 ms)
-------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-------------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
ScrollWatcher.tsx | 100 | 100 | 100 | 100 |
-------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 13.661 s

React Testing Library - testing hooks with React.context

I have question about react-testing-library with custom hooks
My tests seem to pass when I use context in custom hook, but when I update context value in hooks cleanup function and not pass.
So can someone explain why this is or isn't a good way to test the custom hook ?
The provider and hook code:
// component.tsx
import * as React from "react";
const CountContext = React.createContext({
count: 0,
setCount: (c: number) => {},
});
export const CountProvider = ({ children }) => {
const [count, setCount] = React.useState(0);
const value = { count, setCount };
return <CountContext.Provider value={value}>{children}</CountContext.Provider>;
};
export const useCount = () => {
const { count, setCount } = React.useContext(Context);
React.useEffect(() => {
return () => setCount(50);
}, []);
return { count, setCount };
};
The test code:
// component.spec.tsx
import * as React from "react";
import { act, render, screen } from "#testing-library/react";
import { CountProvider, useCount } from "./component";
describe("useCount", () => {
it("should save count when unmount and restore count", () => {
const Wrapper = ({ children }) => {
return <ContextStateProvider>{children}</ContextStateProvider>;
};
const Component = () => {
const { count, setCount } = useCount();
return (
<div>
<div data-testid="foo">{count}</div>
</div>
);
};
const { unmount, rerender, getByTestId, getByText } = render(
<Component />, { wrapper: Wrapper }
);
expect(getByTestId("foo").textContent).toBe("0");
unmount();
rerender(<Component />);
// I Expected: "50" but Received: "0". but I dont understand why
expect(getByTestId("foo").textContent).toBe("50");
});
});
When you call render, the rendered component tree is like this:
base element(document.body by default) -> container(createElement('div') by default) -> wrapper(CountProvider) -> Component
When you unmount the component instance, Wrapper will also be unmounted. See here.
When you rerender a new component instance, it just uses a new useCount hook and the default context value(you doesn' provide a context provider for rerender) in the useContext. So the count will always be 0. From the doc React.createContext:
The defaultValue argument is only used when a component does not have a matching Provider above it in the tree.
You should NOT unmount the CountProvider wrapper, you may want to just unmount the Component. So that the component will receive the latest context value after mutate it.
So, the test component should be designed like this:
component.tsx:
import React from 'react';
const CountContext = React.createContext({
count: 0,
setCount: (c: number) => {},
});
export const CountProvider = ({ children }) => {
const [count, setCount] = React.useState(0);
return <CountContext.Provider value={{ count, setCount }}>{children}</CountContext.Provider>;
};
export const useCount = () => {
const { count, setCount } = React.useContext(CountContext);
React.useEffect(() => {
return () => setCount(50);
}, []);
return { count, setCount };
};
component.test.tsx:
import React, { useState } from 'react';
import { fireEvent, render } from '#testing-library/react';
import { CountProvider, useCount } from './component';
describe('useCount', () => {
it('should save count when unmount and restore count', () => {
const Wrapper: React.ComponentType = ({ children }) => {
const [visible, setVisible] = useState(true);
return (
<CountProvider>
{visible && children}
<button data-testid="toggle" onClick={() => setVisible((pre) => !pre)}></button>
</CountProvider>
);
};
const Component = () => {
const { count } = useCount();
return <div data-testid="foo">{count}</div>;
};
const { getByTestId } = render(<Component />, { wrapper: Wrapper });
expect(getByTestId('foo').textContent).toBe('0');
fireEvent.click(getByTestId('toggle')); // unmount the Component
fireEvent.click(getByTestId('toggle')); // mount the Component again
expect(getByTestId('foo').textContent).toBe('50');
});
});
Test result:
PASS stackoverflow/67749630/component.test.tsx (8.46 s)
useCount
✓ should save count when unmount and restore count (54 ms)
---------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
---------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 80 | 100 |
component.tsx | 100 | 100 | 80 | 100 |
---------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 8.988 s, estimated 9 s
Also take a look at this example: Codesandbox

How to mock and test scrollBy with Jest and React-Testing-Library?

Given the following component:
import * as React from "react";
import "./styles.css";
export default function App() {
const scrollContainerRef = React.useRef<HTMLDivElement | null>(null);
const handleClick = () => {
scrollContainerRef?.current?.scrollBy({ top: 0, left: 100 });
};
return (
<div aria-label="wrapper" ref={scrollContainerRef}>
<button onClick={handleClick}>click</button>
</div>
);
}
How do I write a test using Jest and React Testing library to check that when the button is clicked, scrollBy is triggered on the wrapper?
I have tried the following and it doesn't seem to be working:
test('Clicking on button should trigger scroll',() => {
const myMock = jest.fn();
Object.defineProperty(HTMLElement.prototype, 'scrollBy', {
configurable: true,
value: myMock(),
})
render(<MyComponent />)
fireEvent.click(screen.getByText(/click/i))
expect(myMock).toHaveBeenCalledWith({top: 0, left: 100})
})
Since jsdom does NOT implements Element.scrollBy() method, see PR. We can create a mocked ref object with getter and setter to intercept React's assignment process to ref.current, and install spy or add mock in the process.
E.g.
App.tsx:
import * as React from 'react';
export default function App() {
const scrollContainerRef = React.useRef<HTMLDivElement | null>(null);
const handleClick = () => {
scrollContainerRef?.current?.scrollBy({ top: 0, left: 100 });
};
return (
<div aria-label="wrapper" ref={scrollContainerRef}>
<button onClick={handleClick}>click</button>
</div>
);
}
App.test.tsx:
import React, { useRef } from 'react';
import { render, screen, fireEvent } from '#testing-library/react';
import { mocked } from 'ts-jest/utils';
import App from './App';
jest.mock('react', () => {
return {
...jest.requireActual<typeof React>('react'),
useRef: jest.fn(),
};
});
const useMockRef = mocked(useRef);
describe('63702104', () => {
afterAll(() => {
jest.resetAllMocks();
});
test('should pass', () => {
const ref = { current: {} };
const mScrollBy = jest.fn();
Object.defineProperty(ref, 'current', {
set(_current) {
if (_current) {
_current.scrollBy = mScrollBy;
}
this._current = _current;
},
get() {
return this._current;
},
});
useMockRef.mockReturnValueOnce(ref);
render(<App />);
fireEvent.click(screen.getByText(/click/i));
expect(mScrollBy).toBeCalledWith({ top: 0, left: 100 });
});
});
test result:
PASS examples/63702104/App.test.tsx (9.436 s)
63702104
✓ should pass (33 ms)
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 100 | 75 | 100 | 100 |
App.tsx | 100 | 75 | 100 | 100 | 7
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 10.21 s

React Hooks jest testing - method is not a function

I'm getting fed up with trying to test hooks but I feel so close with this approach. Here me out.
I've got this test running and it gives me this error:
'TypeError: handleCount is not a function'
describe("<Content />", () => {
const setCount = jest.fn();
let activeTab = 'Year';
test("Ensure that handleCount is fired if activeTab is the type year", () => {
handleYearTab(setCount, activeTab);
});
});
So this makes sense but I'm not sure how I can mock the method that it is complaining about. this is my component that I'm trying to test:
/**
* Get new count from getTotalAttendances
* #param dates | New date picked by the user
* #param setCount | Hook function
* #param activeTab | Type of tab
*/
function handleCount(
dates: object,
setCount: Function,
activeTab?: string,
) {
const totalCount = new GetTotal(dates, activeTab);
setCount(totalCount.totalAttendances());
}
/**
* Handle count for the year tab.
* #param setCount | Hook function
* #param activeTab | Type of tab
*/
export function handleYearTab(
setCount: Function,
activeTab: string,
) {
if (activeTab === 'Year') {
handleCount(new Date(), setCount, activeTab);
}
}
const Content: FC<Props> = ({ activeTab }) => {
const [count, setCount] = useState<number>(0);
useEffect(() => {
handleYearTab(setCount, activeTab);
});
return (
<Container>
<TotalAttendences count={count} />
</Container>
);
}
export default Content;
I'm really curious how you would go about mocking the handleCount method.
Here is the unit test solution using jestjs and react-dom/test-utils:
index.tsx:
import React, { FC, useState, useEffect } from 'react';
import { GetTotal } from './getTotal';
interface Props {
activeTab: string;
}
function handleCount(dates: object, setCount: Function, activeTab?: string) {
const totalCount = new GetTotal(dates, activeTab);
setCount(totalCount.totalAttendances());
}
export function handleYearTab(setCount: Function, activeTab: string) {
if (activeTab === 'Year') {
handleCount(new Date(), setCount, activeTab);
}
}
const Content: FC<Props> = ({ activeTab }) => {
const [count, setCount] = useState<number>(0);
useEffect(() => {
handleYearTab(setCount, activeTab);
});
return <div>{count}</div>;
};
export default Content;
getTotal.ts:
export class GetTotal {
constructor(dates, activeTab) {}
public totalAttendances(): number {
return 1;
}
}
index.test.tsx:
import Content from './';
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { act } from 'react-dom/test-utils';
import { GetTotal } from './getTotal';
describe('60638277', () => {
let container;
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
unmountComponentAtNode(container);
container.remove();
container = null;
});
it('should handle year tab', async () => {
const totalAttendancesSpy = jest.spyOn(GetTotal.prototype, 'totalAttendances').mockReturnValue(100);
const mProps = { activeTab: 'Year' };
await act(async () => {
render(<Content {...mProps}></Content>, container);
});
expect(container.querySelector('div').textContent).toBe('100');
expect(totalAttendancesSpy).toBeCalled();
totalAttendancesSpy.mockRestore();
});
it('should render initial count', async () => {
const mProps = { activeTab: '' };
await act(async () => {
render(<Content {...mProps}></Content>, container);
});
expect(container.querySelector('div').textContent).toBe('0');
});
});
unit test results with coverage report:
PASS stackoverflow/60638277/index.test.tsx (9.331s)
60638277
✓ should handle year tab (32ms)
✓ should render initial count (11ms)
-------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-------------|---------|----------|---------|---------|-------------------
All files | 95.24 | 100 | 85.71 | 94.12 |
getTotal.ts | 80 | 100 | 66.67 | 75 | 4
index.tsx | 100 | 100 | 100 | 100 |
-------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 10.691s
source code: https://github.com/mrdulin/react-apollo-graphql-starter-kit/tree/master/stackoverflow/60638277

How to mock/spy addEventListener method which is called on ref in ReactJS?

I am doing snapshot testing for one component which has ref on its div. The component looks like -
import React, { PureComponent } from 'react';
class SearchFlightBuilder extends PureComponent {
scrollRef = React.createRef();
state = {
loading: true,
error: false,
filteredList: [],
pageIndex: 0,
scrollCalled: false,
};
handleScroll = (event) => {
// make sure scroll should be called once
if ((this.scrollRef.current.scrollTop + this.scrollRef.current.clientHeight >= this.scrollRef.current.scrollHeight) && !this.state.scrollCalled) {
this.setState({
pageIndex: this.state.pageIndex + 1
});
this.setState({scrollCalled: true});
}
};
componentDidMount = () => {
this.scrollRef.current.addEventListener('scroll', this.handleScroll);
};
removeScrollEvent = () => {
this.scrollRef.current.removeEventListener('scroll', this.handleScroll);
};
render() {
return (
<div className={c('Search-flight-builder')} ref={this.scrollRef}>
<p>Hello</P
</div>
);
}
};
export default SearchFlightBuilder;
And testing file looks like this -
import React from 'react';
import { shallow, mount, render } from 'enzyme';
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import SearchFlightBuilder from './SearchFlightBuilder';
configure({ adapter: new Adapter() });
const testFlightBuilder = () => <SearchFlightBuilder />;
describe('SearchFlightBuilder', () => {
it('should render correctly', () => {
const component = shallow(<SearchFlightBuilder />);
expect(component).toMatchSnapshot();
});
});
When I am running the tests, I am getting this error -
TypeError: Cannot read property 'addEventListener' of null. I tried various approaches, but none of the approach works. Please help me here. I am using enzyme library here.
Here is my unit test strategy:
index.tsx:
import React, { PureComponent } from 'react';
class SearchFlightBuilder extends PureComponent {
scrollRef: any = React.createRef();
state = {
loading: true,
error: false,
filteredList: [],
pageIndex: 0,
scrollCalled: false
};
handleScroll = event => {
// make sure scroll should be called once
if (
this.scrollRef.current.scrollTop + this.scrollRef.current.clientHeight >= this.scrollRef.current.scrollHeight &&
!this.state.scrollCalled
) {
this.setState({
pageIndex: this.state.pageIndex + 1
});
this.setState({ scrollCalled: true });
}
};
componentDidMount = () => {
this.scrollRef.current.addEventListener('scroll', this.handleScroll);
};
removeScrollEvent = () => {
this.scrollRef.current.removeEventListener('scroll', this.handleScroll);
};
render() {
return (
<div className="Search-flight-builder" ref={this.scrollRef}>
<p>Hello</p>
</div>
);
}
}
export default SearchFlightBuilder;
Since clientHeight and scrollHeight properties are read-only, so they need to be mocked using Object.defineProperty.
index.spec.tsx:
import React from 'react';
import { shallow } from 'enzyme';
import SearchFlightBuilder from './';
describe('SearchFlightBuilder', () => {
afterEach(() => {
jest.restoreAllMocks();
});
it('should handle scroll, pageindex + 1', () => {
const mDiv = document.createElement('div');
const events = {};
const addEventListenerSpy = jest.spyOn(mDiv, 'addEventListener').mockImplementation((event, handler) => {
events[event] = handler;
});
mDiv.scrollTop = 1;
Object.defineProperty(mDiv, 'clientHeight', { value: 1 });
Object.defineProperty(mDiv, 'scrollHeight', { value: 1 });
const mRef = { current: mDiv };
const createRefSpy = jest.spyOn(React, 'createRef').mockReturnValueOnce(mRef);
const component = shallow(<SearchFlightBuilder />);
expect(createRefSpy).toBeCalledTimes(1);
expect(addEventListenerSpy).toBeCalledWith('scroll', component.instance()['handleScroll']);
events['scroll']();
expect(component.state('pageIndex')).toBe(1);
expect(component.state('scrollCalled')).toBeTruthy();
});
});
Unit test result with coverage report:
PASS src/stackoverflow/57943619/index.spec.tsx (8.618s)
SearchFlightBuilder
✓ should handle scroll, pageindex + 1 (15ms)
-----------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
-----------|----------|----------|----------|----------|-------------------|
All files | 94.44 | 85.71 | 80 | 93.75 | |
index.tsx | 94.44 | 85.71 | 80 | 93.75 | 32 |
-----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 9.916s
Source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/57943619

Resources