Using act doesn't update state? - reactjs

I have a custom hook like so:
import { useState } from 'react';
export default function useOpenClose(initial = false) {
const [isOpen, setOpen] = useState(initial);
const open = () => { setOpen(true); }
const close = () => { setOpen(false); }
return [isOpen, { open, close } ];
}
and as for my tests I have something like this:
import { renderHook, act } from '#testing-library/react-hooks';
import useOpenClose from './useOpenClose';
describe('useOpenClose', () => {
const { result: { current } } = renderHook(() => useOpenClose());
const [isOpen, { open, close }] = current;
test('Should have an open function', () => {
expect(open).toBeInstanceOf(Function)
});
test('Should have an open function', () => {
expect(close).toBeInstanceOf(Function)
});
test('Should have initial value of false', () => {
expect(isOpen).toBe(false);
});
test('Should update value to true', () => {
act(() => open());
console.log(isOpen)
})
});
Where the test "'Should update value to true'", when I log isOpen, it stays false. I'm not exactly sure why it's not updating unless act isn't doing what it's doing?

From the doc Updates:
NOTE: There's a gotcha with updates. renderHook mutates the value of current when updates happen so you cannot destructure its values as the assignment will make a copy locking into the value at that time.
E.g.
useOpenClose.ts:
import { useState } from 'react';
export default function useOpenClose(initial = false) {
const [isOpen, setOpen] = useState(initial);
const open = () => {
setOpen(true);
};
const close = () => {
setOpen(false);
};
return [isOpen, { open, close }] as const;
}
useOpenClose.test.ts:
import { act, renderHook } from '#testing-library/react-hooks';
import useOpenClose from './useOpenClose';
describe('useOpenClose', () => {
test('should pass', () => {
const { result } = renderHook(() => useOpenClose());
// Don't destructure `result`.
expect(result.current[1].open).toBeInstanceOf(Function);
expect(result.current[1].close).toBeInstanceOf(Function);
expect(result.current[0]).toBe(false);
act(() => result.current[1].open());
console.log(result.current[0]);
});
});
Test result:
PASS examples/57315042/useOpenClose.test.ts (13.635 s)
useOpenClose
✓ should pass (45 ms)
console.log
true
at Object.<anonymous> (examples/57315042/useOpenClose.test.ts:12:13)
-----------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------------|---------|----------|---------|---------|-------------------
All files | 87.5 | 100 | 66.67 | 87.5 |
useOpenClose.ts | 87.5 | 100 | 66.67 | 87.5 | 10
-----------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 15.588 s

Related

React Testing-Library testing DOM changes on event fire

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

How to mock keypress event on Window object with Enzyme / Jest?

I am looking for a way to test my hook for React components:
export default function useKeyUp(key: Key, onKeyUp: Function) {
useEffect(() => {
const handleUp = (event: KeyboardEvent) => {
const { key: releasedKey } = event
if (key === releasedKey) {
if (onKeyUp) {
onKeyUp()
}
}
}
window.addEventListener('keyup', handleUp)
return () => {
window.removeEventListener('keyup', handleUp)
}
}, [key, onKeyUp])
}
How can I mock / simulate event fired on the Window object in Jest / Enzyme?
Since enzyme can't test react hook directly like react-hooks-testing-library. We can use the hook inside a component. Then, use mount to render the component.
Use keyboardEvent constructor to create a "keyup" event and dispatch the event via dispatchEvent web API.
E.g.
useKeyUp.ts:
import { useEffect } from 'react';
type Key = string;
export default function useKeyUp(key: Key, onKeyUp: Function) {
useEffect(() => {
const handleUp = (event: KeyboardEvent) => {
const { key: releasedKey } = event;
if (key === releasedKey) {
if (onKeyUp) {
onKeyUp();
}
}
};
window.addEventListener('keyup', handleUp);
return () => {
window.removeEventListener('keyup', handleUp);
};
}, [key, onKeyUp]);
}
useKeyUp.test.tsx:
import { mount } from 'enzyme';
import React from 'react';
import useKeyUp from './useKeyUp';
describe('70938281', () => {
test('should call onKeyUp callback', () => {
const mOnKeyUp = jest.fn();
function TestComp() {
useKeyUp('s', mOnKeyUp);
return null;
}
const wrapper = mount(<TestComp />);
const keyUpEvent = new KeyboardEvent('keyup', { key: 's' });
window.dispatchEvent(keyUpEvent);
expect(mOnKeyUp).toBeCalledTimes(1);
wrapper.unmount();
window.dispatchEvent(keyUpEvent);
expect(mOnKeyUp).toBeCalledTimes(1);
});
test('should NOT call onKeyUp callback', () => {
const mOnKeyUp = jest.fn();
function TestComp() {
useKeyUp('s', mOnKeyUp);
return null;
}
mount(<TestComp />);
const keyUpEvent = new KeyboardEvent('keyup', { key: 'a' });
window.dispatchEvent(keyUpEvent);
expect(mOnKeyUp).not.toBeCalled();
});
});
Test result:
PASS stackoverflow/70938281/useKeyUp.test.tsx (8.484 s)
70938281
✓ should call onKeyUp callback (29 ms)
✓ should NOT call onKeyUp callback (2 ms)
-------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-------------|---------|----------|---------|---------|-------------------
All files | 100 | 75 | 100 | 100 |
useKeyUp.ts | 100 | 75 | 100 | 100 | 9
-------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 8.937 s
jest.config.js:
module.exports = {
preset: 'ts-jest/presets/js-with-ts',
testEnvironment: 'enzyme',
setupFilesAfterEnv: ['jest-enzyme', 'jest-extended'],
setupFiles: ['./jest.setup.js'],
testEnvironmentOptions: {
enzymeAdapter: 'react16',
},
};

How to mock ipcRenderer.on functions inside React functional components

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

Testing React useEffect hook while adding eventListeners

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

How to test useRef with the "current" prop in jest/enzyme

I hope someone can point me in the right direction to test useRef in the component below.
I have a component structured something like below. I am trying to test the functionality within the otherFunction() but I'm not sure how to mock the current property that comes off the component ref. Has anyone done something like this before?
const Component = (props) => {
const thisComponent = useRef(null);
const otherFunction = ({ current, previousSibling }) => {
if (previousSibling) return previousSibling.focus();
if (!previousSibling && current) return current.focus();
}
const handleFocus = () => {
const {current} = thisComponent;
otherFunction(current);
}
return (
<div ref={thisComponent} onFocus={handleFocus}>Stuff In here</div>
);
};
Here is my test strategy for your case. I use jest.spyOn method to spy on React.useRef hook. It will let us mock the different return value of ref object for SFC.
index.tsx:
import React, { RefObject } from 'react';
import { useRef } from 'react';
export const Component = props => {
const thisComponent: RefObject<HTMLDivElement> = useRef(null);
const otherFunction = ({ current, previousSibling }) => {
if (previousSibling) return previousSibling.focus();
if (!previousSibling && current) return current.focus();
};
const handleFocus = () => {
const { current } = thisComponent;
const previousSibling = current ? current.previousSibling : null;
otherFunction({ current, previousSibling });
};
return (
<div ref={thisComponent} onFocus={handleFocus}>
Stuff In here
</div>
);
};
index.spec.tsx:
import React from 'react';
import { Component } from './';
import { shallow } from 'enzyme';
describe('Component', () => {
const focus = jest.fn();
beforeEach(() => {
jest.restoreAllMocks();
jest.resetAllMocks();
});
test('should render correctly', () => {
const wrapper = shallow(<Component></Component>);
const div = wrapper.find('div');
expect(div.text()).toBe('Stuff In here');
});
test('should handle click event correctly when previousSibling does not exist', () => {
const useRefSpy = jest.spyOn(React, 'useRef').mockReturnValueOnce({ current: { focus } });
const wrapper = shallow(<Component></Component>);
wrapper.find('div').simulate('focus');
expect(useRefSpy).toBeCalledTimes(1);
expect(focus).toBeCalledTimes(1);
});
test('should render and handle click event correctly when previousSibling exists', () => {
const useRefSpy = jest.spyOn(React, 'useRef').mockReturnValueOnce({ current: { previousSibling: { focus } } });
const wrapper = shallow(<Component></Component>);
wrapper.find('div').simulate('focus');
expect(useRefSpy).toBeCalledTimes(1);
expect(focus).toBeCalledTimes(1);
});
test('should render and handle click event correctly when current does not exist', () => {
const useRefSpy = jest.spyOn(React, 'useRef').mockReturnValueOnce({ current: null });
const wrapper = shallow(<Component></Component>);
wrapper.find('div').simulate('focus');
expect(useRefSpy).toBeCalledTimes(1);
expect(focus).not.toBeCalled();
});
});
Unit test result with 100% coverage:
PASS src/stackoverflow/56739670/index.spec.tsx (6.528s)
Component
✓ should render correctly (10ms)
✓ should handle click event correctly when previousSibling does not exist (3ms)
✓ should render and handle click event correctly when previousSibling exists (1ms)
✓ should render and handle click event correctly when current does not exist (2ms)
-----------|----------|----------|----------|----------|-------------------|
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: 4 passed, 4 total
Snapshots: 0 total
Time: 7.689s
Source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/56739670

Resources