I am writing an Electron app, using React for the front-end and JEST + React Testing Library for running tests. I have the following simplified code in a module:
import React from 'react';
import { ipcRenderer } from 'electron';
import Paper from '#material-ui/core/Paper';
import LinearProgress from '#material-ui/core/LinearProgress';
const AccountCheckModule = () => {
const [listingsCount, setListingsCount] = React.useState(0);
React.useEffect(() => {
ipcRenderer.on('count-listings', (event, count) => {
setListingsCount(count);
});
ipcRenderer.send('count-listings');
// Cleanup the listener events so that memory leaks are avoided.
return function cleanup() {
ipcRenderer.removeAllListeners('count-listings');
};
}, []);
return (
<Paper elevation={2} data-testid="paper">
<p
className={classes.listingsNumberTracker}
data-testid="free-listings-counter"
>
Free listings: {listingsCount}/100
</p>
<BorderLinearProgress
className={classes.margin}
variant="determinate"
color="secondary"
value={listingsCount}
data-testid="border-linear-progress"
/>
</Paper>
);
};
export default AccountCheckModule;
Basically, React.useEffect() runs once, calls ipcRenderer.send('count-listings'); and sets up a listener to wait for the response from the main process. The main process responds with a listings count number and when received is used to update the listingsCount state -> setListingsCount(count)
Is it possible to mock this listener function to return a 'count' number using Jest.
ipcRenderer.on('count-listings', (event, count) => {
setListingsCount(count);
});
If yes, how would you go about achieving this?
Here is a unit test solution, I create a simple electron module to simulate the real electron node module and simplify your component JSX element.
E.g.
index.tsx:
import React from 'react';
import { ipcRenderer } from './electron';
const AccountCheckModule = () => {
const [listingsCount, setListingsCount] = React.useState(0);
React.useEffect(() => {
ipcRenderer.on('count-listings', (event, count) => {
setListingsCount(count);
});
ipcRenderer.send('count-listings', 2);
// Cleanup the listener events so that memory leaks are avoided.
return function cleanup() {
ipcRenderer.removeAllListeners('count-listings');
};
}, []);
return <div>{listingsCount}</div>;
};
export default AccountCheckModule;
electron.ts:
export const ipcRenderer = {
events: {},
on(event, handler) {
this.events[event] = handler;
},
send(event, data) {
this.events[event](event, data);
},
removeAllListeners(event) {
this.events[event] = undefined;
}
};
index.spec.tsx:
import React from 'react';
import { render, act } from '#testing-library/react';
import { ipcRenderer } from './electron';
import AccountCheckModule from './';
describe('AccountCheckModule', () => {
afterEach(() => {
jest.restoreAllMocks();
});
it('should render correct', async () => {
const events = {};
const onSpy = jest.spyOn(ipcRenderer, 'on').mockImplementation((event, handler) => {
events[event] = handler;
});
const sendSpy = jest.spyOn(ipcRenderer, 'send').mockImplementation((event, data) => {
events[event](event, data);
});
const { getByText, container } = render(<AccountCheckModule></AccountCheckModule>);
const mCount = 666;
act(() => {
ipcRenderer.send('count-listings', mCount);
});
const element = getByText(mCount.toString());
expect(element).toBeDefined();
expect(onSpy).toBeCalledWith('count-listings', expect.any(Function));
expect(sendSpy).toBeCalledWith('count-listings', mCount);
expect(container).toMatchSnapshot();
});
});
Unit test result with 100% coverage report for SFC:
PASS src/stackoverflow/58048849/index.spec.tsx
AccountCheckModule
✓ should render correct (47ms)
-------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
-------------|----------|----------|----------|----------|-------------------|
All files | 88.89 | 100 | 71.43 | 87.5 | |
electron.ts | 50 | 100 | 33.33 | 50 | 4,7 |
index.tsx | 100 | 100 | 100 | 100 | |
-------------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 1 passed, 1 total
Time: 4.247s, estimated 11s
index.spec.tsx.snap:
// Jest Snapshot v1
exports[`AccountCheckModule should render correct 1`] = `
<div>
<div>
666
</div>
</div>
`;
Source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/58048849
Related
Given the following component (<Button> is a custom component that's <button>-like)
const MyElement = ({
onRemove,
}) => {
const [isRemoving, setIsRemoving] = useState(false);
const handleRemove = (event) => {
event.stopPropagation();
setIsRemoving(true);
onRemove().finally(() => setIsRemoving(false));
};
return (
<Button
status={isRemoving ? 'busy' : 'selected'}
onClick={handleRemove}
>
Remove
</Button>
);
};
I want to test that the button's status turn busy before becoming selected once the onRemove function resolves. How do I do this with user events?
You can create a mock onRemove async function and resolve the promise manually after setIsRemove(true). So that you can assert the isRemoving to be true firstly and assert it to be false after the promise is resolved.
MyElement.tsx:
import React, { useState } from 'react';
export const MyElement = ({ onRemove }) => {
const [isRemoving, setIsRemoving] = useState(false);
const handleRemove = (event) => {
event.stopPropagation();
setIsRemoving(true);
onRemove().finally(() => setIsRemoving(false));
};
return (
<>
<button onClick={handleRemove}>Remove</button>
<p>{isRemoving ? 'busy' : 'selected'}</p>
</>
);
};
MyElement.test.tsx:
import { fireEvent, render, screen } from '#testing-library/react';
import '#testing-library/jest-dom/extend-expect';
import React from 'react';
import { MyElement } from './MyElement';
describe('72858536', () => {
test('should pass', async () => {
let _resolve;
const onRemoveMock = () => new Promise((resolve) => (_resolve = resolve));
render(<MyElement onRemove={onRemoveMock} />);
fireEvent.click(screen.getByText(/remove/i));
expect(screen.getByText(/busy/)).toBeInTheDocument();
_resolve();
expect(await screen.findByText(/selected/)).toBeInTheDocument();
});
});
Test result:
PASS stackoverflow/72858536/MyElement.test.tsx
72858536
✓ should pass (40 ms)
---------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
---------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
MyElement.tsx | 100 | 100 | 100 | 100 |
---------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.778 s, estimated 16 s
hello I have a simple app that connects with a topic and then updating he's state, seems to be working when playing around, but I have a problem with how to properly write a test for it
There is a library I using https://www.npmjs.com/package/pubsub-js
import React, { useState, useEffect } from 'react'
import PubSub from 'pubsub-js'
const App = () => {
const [data, setData] = useState([])
const mySub = (msg, pubData) => {
setData(pubData)
}
useEffect(() => {
const token = PubSub.subscribe('TOPIC', mySub);
return () => {
PubSub.unsubscribe(token)
};
},[]);
return (
<input onChange={() => {}} value={data && data[0]?.value}
)
};
import React from 'react'
import PubSub from 'pubsub-js'
import { mount } from 'enzyme'
import App from '.'
const mocked = [{ value: 1}]
describe('should receive some date', () => {
it('xx', () => {
const app = mount(<App />)
PubSub.publish('TOPIC', mocked);
PubSub.publishSync('TOPIC', mocked);
// WHAT NEXT?
})
})
I've tried some
spies, finding input and checking values, but nothing change... why?
How should I test this Function component?
Since the PubSub.publishSync() operation will trigger mySub event handler that updates the state of the component. Make sure you wrap the code that causes React state updates into act(), otherwise, you will get a warning:
Warning: An update to App inside a test was not wrapped in act(...).
E.g.
index.jsx:
import React, { useState, useEffect } from 'react';
import PubSub from 'pubsub-js';
export const App = () => {
const [data, setData] = useState('a');
const mySub = (msg, pubData) => {
setData(pubData);
};
useEffect(() => {
const token = PubSub.subscribe('TOPIC', mySub);
return () => {
PubSub.unsubscribe(token);
};
}, []);
return <div>{data}</div>;
};
index.test.jsx:
import React from 'react';
import PubSub from 'pubsub-js';
import { mount } from 'enzyme';
import { App } from './';
import { act } from 'react-dom/test-utils';
describe('67422968', () => {
it('should receive data and update the state', () => {
const wrapper = mount(<App />);
expect(wrapper.text()).toBe('a');
act(() => {
PubSub.publishSync('TOPIC', 'b');
});
expect(wrapper.text()).toBe('b');
});
});
test result:
PASS examples/67422968/index.test.tsx (7.254 s)
67422968
✓ should receive data and update the state (28 ms)
-----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------|---------|----------|---------|---------|-------------------
All files | 91.67 | 100 | 75 | 90.91 |
index.tsx | 91.67 | 100 | 75 | 90.91 | 15
-----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 7.902 s, estimated 8 s
package versions:
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.5",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"pubsub-js": "^1.9.3"
I have a React Scroll to Top component where we add this component Below our Router so that while moving across page we donot maintain scroll position .
I was trying to write Test case for this Component But Jest and Enzyme dosent seem to recognise this as a component when doing shallow rendering. I am using typescript and this is the component.
scrollToTop.ts
export const ScrollToTop = ({history}: IRouterResetScroll) => {
useEffect(() => {
const unListen = history.listen(() => {
window.scrollTo(0, 0);
});
return () => {
unListen();
}
}, []);
return null;
}
export default withRouter(ScrollToTop);
Here is my unit test strategy, the hardest part for your code to be tested is the history.listen(handler), so we can mock the implementation of history.listen method, we defined a queue to store the handlers. After mount the component, the mocked history will execute history.listen with a function as parameter. This function will be stored in the queue we defined before. We can get this function from the queue in unit test case and trigger it manually.
index.tsx:
import { useEffect } from 'react';
import { withRouter } from 'react-router-dom';
type IRouterResetScroll = any;
export const ScrollToTop = ({ history }: IRouterResetScroll) => {
useEffect(() => {
const unListen = history.listen(() => {
window.scrollTo(0, 0);
});
return () => {
unListen();
};
}, []);
return null;
};
export default withRouter(ScrollToTop);
index.spec.tsx:
import React from 'react';
import { ScrollToTop } from './';
import { mount } from 'enzyme';
describe('ScrollToTop', () => {
it('should scroll to top', () => {
const queue: any[] = [];
const mUnListen = jest.fn();
const mHistory = {
listen: jest.fn().mockImplementation(fn => {
queue.push(fn);
return mUnListen;
})
};
window.scrollTo = jest.fn();
const wrapper = mount(<ScrollToTop history={mHistory}></ScrollToTop>);
queue[0]();
expect(mHistory.listen).toBeCalledWith(expect.any(Function));
expect(window.scrollTo).toBeCalledWith(0, 0);
wrapper.unmount();
expect(mUnListen).toBeCalledTimes(1);
});
});
Unit test result with 100% coverage:
PASS src/stackoverflow/58786973/index.spec.tsx
ScrollToTop
✓ should scroll to top (39ms)
-----------|----------|----------|----------|----------|-------------------|
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: 4.041s, estimated 9s
Source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/58786973
I have a functional component in my React code as below:
const Compo = ({funcA}) => {
useEffect(() => {
window.addEventListener('x', funcB, false);
return () => {
window.removeEventListener('x', funcB, false);
}
});
const funcB = () => {funcA()};
return (
<button
onClick={() => funcA()}
/>
);
};
Compo.propTypes = {
funcA: func.isRequired
}
export default Compo;
I need to test the above functional component to make sure the event listeners are added and removed as mentioned in the useEffect() hook.
Here is what my test file looks like -
const addEventSpy = jest.spyOn(window, 'addEventListener');
const removeEventSpy = jest.spyOn(window, 'removeEventListener');
let props = mockProps = {funcA: jest.fn()};
const wrapper = mount(<Compo {...props} />);
const callBack = wrapper.instance().funcB; <===== ERROR ON THIS LINE
expect(addEventSpy).toHaveBeenCalledWith('x', callBack, false);
wrapper.unmount();
expect(removeEventSpy).toHaveBeenCalledWith('x', callBack, false);
However, I get the below error on the line where I declare the 'callBack' constant (highlighted above in the code) :
TypeError: Cannot read property 'funcB' of null
Effectively, it renders the component ok, but wrapper.instance() is evaluating as null, which is throwing the above error.
Would anyone please know what am I missing to fix the above error?
This is my unit test strategy:
index.tsx:
import React, { useEffect } from 'react';
const Compo = ({ funcA }) => {
useEffect(() => {
window.addEventListener('x', funcB, false);
return () => {
window.removeEventListener('x', funcB, false);
};
}, []);
const funcB = () => {
funcA();
};
return <button onClick={funcB} />;
};
export default Compo;
index.spec.tsx:
import React from 'react';
import { mount } from 'enzyme';
import Compo from './';
describe('Compo', () => {
afterEach(() => {
jest.restoreAllMocks();
});
it('should call funcA', () => {
const events = {};
jest.spyOn(window, 'addEventListener').mockImplementation((event, handle, options?) => {
events[event] = handle;
});
jest.spyOn(window, 'removeEventListener').mockImplementation((event, handle, options?) => {
events[event] = undefined;
});
const mProps = { funcA: jest.fn() };
const wrapper = mount(<Compo {...mProps}></Compo>);
expect(wrapper.find('button')).toBeDefined();
events['x']();
expect(window.addEventListener).toBeCalledWith('x', expect.any(Function), false);
expect(mProps.funcA).toBeCalledTimes(1);
wrapper.unmount();
expect(window.removeEventListener).toBeCalledWith('x', expect.any(Function), false);
});
});
Unit test result with 100% coverage:
PASS src/stackoverflow/57797518/index.spec.tsx (8.125s)
Compo
✓ should call funcA (51ms)
-----------|----------|----------|----------|----------|-------------------|
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.556s
Source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/57797518
I have a react component like this:
<A href="/products" onClick={(e) => this.onClick(tf)}>my link</A>
There is an onClick handler attached to the link that will either execute a separate function, or allow the link to propagate and for the user to be redirected:
onClick(e, tf) {
e.stopPropagation();
if(tf){
e.preventDefault();
doSomethingElse();
}
// If execution gets here, then the link will follow through to /products
}
How do I test this using Enzyme / Jest?
Here is the unit test solution:
index.jsx:
import React, { Component } from 'react';
class Link extends Component {
onClick(e, tf) {
e.stopPropagation();
if (tf) {
e.preventDefault();
}
}
render() {
const { tf } = this.props;
return (
<a href="/products" onClick={(e) => this.onClick(e, tf)}>
my link
</a>
);
}
}
export default Link;
index.test.jsx:
import Link from './index';
import React from 'react';
import { shallow } from 'enzyme';
describe('46213271', () => {
it('should handle click, call stopPropagation and preventDefault', () => {
const mProps = { tf: 'tf' };
const wrapper = shallow(<Link {...mProps}></Link>);
expect(wrapper.exists()).toBeTruthy();
const mEvent = { stopPropagation: jest.fn(), preventDefault: jest.fn() };
wrapper.simulate('click', mEvent);
expect(mEvent.stopPropagation).toBeCalledTimes(1);
expect(mEvent.preventDefault).toBeCalledTimes(1);
});
it('should handle click, call stopPropagation', () => {
const mProps = { tf: '' };
const wrapper = shallow(<Link {...mProps}></Link>);
expect(wrapper.exists()).toBeTruthy();
const mEvent = { stopPropagation: jest.fn(), preventDefault: jest.fn() };
wrapper.simulate('click', mEvent);
expect(mEvent.stopPropagation).toBeCalledTimes(1);
expect(mEvent.preventDefault).not.toBeCalled();
});
});
Unit test results with 100% coverage:
PASS src/stackoverflow/46213271/index.test.jsx (17.292s)
46213271
✓ should handle click, call stopPropagation and preventDefault (13ms)
✓ should handle click, call stopPropagation (2ms)
-----------|----------|----------|----------|----------|-------------------|
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: 2 passed, 2 total
Snapshots: 0 total
Time: 19.864s
Source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/46213271