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

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

Related

How to test this custom hook (useRef)

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

Jest React: How to mock window eventListener set in componentDidMount

Here I need to add tests for handleBeforeUnload in window eventListener but I am getting error, How will I resolve it?
expect(jest.fn())[.not].toHaveBeenCalled()
Matcher error: received value must be a mock or spy function
Received has value: undefined
map.beforeunload();
expect(wrapper.handleBeforeUnload).toHaveBeenCalled();
My component
componentDidMount() {
window.addEventListener('beforeunload', (event) => {
this.handleBeforeUnload();
});
}
handleBeforeUnload () {
}
My test spec:
it('should call handleBeforeUnload', () => {
const historyMock = { listen: jest.fn(), replace: jest.fn() };
const map = {};
window.addEventListener = jest.fn((event, cb) => {
map[event] = cb;
});
const wrapper = shallow(
<AppRoutes history={historyMock} getDctkConfig={getDctkConfig} />,
);
map.beforeunload();
expect(wrapper.handleBeforeUnload).toHaveBeenCalled();
});
You didn't spy the.handleBeforeUnload() method of the component class. You can spy it through Component.prototype.handleBeforeUnload. Besides, you can mock the implementation of the .addEventListener() method and invoke the listener function manually.
index.tsx:
import React, { Component } from 'react';
export default class AppRoutes extends Component {
componentDidMount() {
window.addEventListener('beforeunload', (event) => {
this.handleBeforeUnload();
});
}
handleBeforeUnload() {}
render() {
return <div>app routes</div>;
}
}
index.test.tsx:
import { shallow } from 'enzyme';
import React from 'react';
import AppRoutes from './';
describe('69346085', () => {
afterEach(() => {
jest.restoreAllMocks();
});
test('should pass', () => {
const handleBeforeUnloadSpy = jest.spyOn(AppRoutes.prototype, 'handleBeforeUnload');
jest
.spyOn(window, 'addEventListener')
.mockImplementation((type: string, listener: EventListenerOrEventListenerObject) => {
typeof listener === 'function' && listener({} as Event);
});
shallow(<AppRoutes />);
expect(handleBeforeUnloadSpy).toHaveBeenCalled();
});
});
test result:
PASS examples/69346085/index.test.tsx (9.191 s)
69346085
✓ should pass (7 ms)
-----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
index.tsx | 100 | 100 | 100 | 100 |
-----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 9.814 s, estimated 10 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

Assert onBlur() funciton in functional component jest

I wrote a test case to call onBlur method, but I'm getting an error when I try to assert it. Here is the above test case.
it("call the handlingBlurEmail method", () => {
const wrapper = mount(
<App childRef={() => {}} />
);
const comp = wrapper.find({ id: "email" }).first();
comp.prop("onBlur")({
target: { id: "email", value: "test#gmail.com" }
});
expect(
wrapper
.find("AccountForm")
.state()
.onBlur()
).toHaveBeenCalled();
});
and the function for which I'm writing test case is
mailReference = React.createRef();
handlingEmailBlur = events => {
this.mailReference.current.validate(events.target.value);
};
render = () {
......
return (
<div className="Form1">
onBlur={this.handlingEmailBlur}
</div>
)
.....
}
Please let me know how to add assert statement in order to call the onBlur() method in the above test case
Here is the unit testing solution:
index.tsx:
import React, { Component } from 'react';
class App extends Component {
mailReference = React.createRef();
handlingEmailBlur = (events) => {
this.mailReference.current.validate(events.target.value);
};
render() {
return (
<div className="Form1" onBlur={this.handlingEmailBlur}>
some component
</div>
);
}
}
export default App;
index.spec.tsx:
import App from './index';
import { mount } from 'enzyme';
import React from 'react';
describe('59455504', () => {
afterEach(() => {
jest.restoreAllMocks();
});
it('call the handlingBlurEmail method', () => {
const mailReference = { current: { validate: jest.fn() } };
jest.spyOn(React, 'createRef').mockReturnValue(mailReference);
const wrapper = mount(<App childRef={() => {}} />);
const mEvent = {
target: { id: 'email', value: 'test#gmail.com' },
};
wrapper.find('.Form1').prop('onBlur')(mEvent);
expect(mailReference.current.validate).toBeCalledWith(mEvent.target.value);
});
});
Unit test result with 100% coverage:
PASS src/stackoverflow/59455504/index.spec.jsx (8.328s)
59455504
✓ call the handlingBlurEmail method (40ms)
-----------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
-----------|----------|----------|----------|----------|-------------------|
All files | 100 | 100 | 100 | 100 | |
index.jsx | 100 | 100 | 100 | 100 | |
-----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 9.769s
Source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/59455504

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