window.dispatchEvent from test code does not trigger window.addEventListener - reactjs

I have below listener added for which I am trying to write test using Jest. However, it seems as though the event I'm dispatching doesn't reach my code.
window.addEventListener('message', (event) => {
if (event.data.type === 'abc') {
console.log(event.data.payload);
}
});
I have tried below 2 approaches and both of them don't seem to work. I'm unable to verify the call using the spy object I'm creating. Please refer to the code below:
const listenerSpy = jest.spyOn(window, 'addEventListener');
const data = {
type: 'abc',
payload: '',
};
const messageEvent = new MessageEvent('message', {data});
window.dispatchEvent(messageEvent);
expect(listenerSpy).toHaveBeenCalledTimes(1);
const listenerSpy = jest.spyOn(window, 'addEventListener');
const data = {
type: 'abc',
payload: '',
};
window.postMessage(data, '*');
expect(listenerSpy).toHaveBeenCalledTimes(1);
For the 1st approach, have also tried using 'new Event('message')'.
With above 2 approaches, I get the error as below:
expect(jest.fn()).toHaveBeenCalledTimes(expected)
Expected number of calls: 1
Received number of calls: 0
102 | window.dispatchEvent(messageEvent);
103 |
> 104 | expect(listenerSpy).toHaveBeenCalledTimes(1);
| ^
I have also tried to follow different websites including below:
https://medium.com/#DavideRama/testing-global-event-listener-within-a-react-component-b9d661e59953
https://github.com/enzymejs/enzyme/issues/426
But no luck there as with typescript, I cannot follow the solution given. I have also tried to find answers on stackoverflow, but the none of solutions suggested seem to work for me.
I am new to react and got stuck with this. Any pointers on this would help.

jsdom fire the message event inside a setTimeout, see this
setTimeout(() => {
fireAnEvent("message", this, MessageEvent, { data: message });
}, 0);
For more info, see issue
So, it's asynchronous and you need to wait for the macro task scheduled by setTimeout to be finished before the test case ends.
index.ts:
export function main() {
window.addEventListener('message', (event) => {
if (event.data.type === 'abc') {
console.log(event.data.payload);
}
});
}
index.test.ts:
import { main } from './';
function flushMessageQueue(ms = 10) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
describe('71912032', () => {
test('should pass', async () => {
const logSpy = jest.spyOn(console, 'log');
main();
const data = { type: 'abc', payload: 'xyz' };
window.postMessage(data, '*');
await flushMessageQueue();
expect(logSpy).toBeCalledWith('xyz');
});
});
Test result:
PASS stackoverflow/71912032/index.test.ts
71912032
✓ should pass (41 ms)
console.log
xyz
at console.<anonymous> (node_modules/jest-mock/build/index.js:845:25)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 4.01 s, estimated 13 s
Also, take a look at this question React Jest: trigger 'addEventListener' 'message' from tests

Have you tried taking a look at this discussion? It seems to have a similar requirement.
Dispatch a Custom Event and test if it was correctly triggered (React TypeScript, Jest)

Related

React Testing Library Unit Test Case: Unable to find node on an unmounted component

I'm having issue with React Unit test cases.
React: v18.2
Node v18.8
Created custom function to render component with ReactIntl. If we use custom component in same file in two different test cases, the second test is failing with below error.
Unable to find node on an unmounted component.
at findCurrentFiberUsingSlowPath (node_modules/react-dom/cjs/react-dom.development.js:4552:13)
at findCurrentHostFiber (node_modules/react-dom/cjs/react-dom.development.js:4703:23)
at findHostInstanceWithWarning (node_modules/react-dom/cjs/react-dom.development.js:28745:21)
at Object.findDOMNode (node_modules/react-dom/cjs/react-dom.development.js:29645:12)
at Transition.performEnter (node_modules/react-transition-group/cjs/Transition.js:280:71)
at node_modules/react-transition-group/cjs/Transition.js:259:27
If I run in different files or test case with setTimeout it is working as expected and there is no error. Please find the other configs below. It is failing even it is same test case.
setUpIntlConfig();
beforeAll(() => server.listen());
afterEach(() => {
server.resetHandlers();
});
afterAll(() => {
jest.clearAllMocks();
server.close();
cleanup();
});
Intl Config:
export const setUpIntlConfig = () => {
if (global.Intl) {
Intl.NumberFormat = IntlPolyfill.NumberFormat;
Intl.DateTimeFormat = IntlPolyfill.DateTimeFormat;
} else {
global.Intl = IntlPolyfill;
}
};
export const RenderWithReactIntl = (component: any) => {
return {
...render(
<IntlProvider locale="en" messages={en}>
{component}
</IntlProvider>
)
};
};
I'm using msw as mock server. Please guide us, if we are missing any configs.
Test cases:
test('fire get resource details with data', async () => {
jest.spyOn(SGWidgets, 'getAuthorizationHeader').mockReturnValue('test-access-token');
process.env = Object.assign(process.env, { REACT_APP_DIAM_API_ENDPOINT: '' });
RenderWithReactIntl(<AllocatedAccess diamUserId={diamUserIdWithData} />);
await waitForElementToBeRemoved(() => screen.getByText(/loading data.../i));
const viewResource = screen.getAllByText(/view resource/i);
fireEvent.click(viewResource[0]);
await waitForElementToBeRemoved(() => screen.getByText(/loading/i));
const ownerName = screen.getByText(/benedicte masson/i);
expect(ownerName).toBeInTheDocument();
});
test('fire get resource details with data----2', async () => {
jest.spyOn(SGWidgets, 'getAuthorizationHeader').mockReturnValue('test-access-token');
process.env = Object.assign(process.env, { REACT_APP_DIAM_API_ENDPOINT: '' });
RenderWithReactIntl(<AllocatedAccess diamUserId={diamUserIdWithData} />);
await waitForElementToBeRemoved(() => screen.getByText(/loading data.../i));
const viewResource = screen.getAllByText(/view resource/i);
fireEvent.click(viewResource[0]);
await waitForElementToBeRemoved(() => screen.getByText(/loading/i));
const ownerName = screen.getByText(/benedicte masson/i);
expect(ownerName).toBeInTheDocument();
});
Can you try these changes:
test('fire get resource details with data----2', async () => {
jest.spyOn(SGWidgets, 'getAuthorizationHeader').mockReturnValue('test-access-token');
process.env = Object.assign(process.env, { REACT_APP_DIAM_API_ENDPOINT: '' });
RenderWithReactIntl(<AllocatedAccess diamUserId={diamUserIdWithData} />);
await waitFor(() => expect(screen.getByText(/loading data.../i)).not.toBeInTheDocument());
const viewResource = screen.getAllByText(/view resource/i);
act(() => {
fireEvent.click(viewResource[0]);
});
await waitFor(() => expect(screen.getByText(/loading/i)).not.toBeInTheDocument());
expect(screen.getByText(/benedicte masson/i)).toBeVisible();
});
I've got into the habit of using act() when altering something that's visible on the screen. A good guide to here: https://testing-library.com/docs/guide-disappearance/
Using getBy* in the waitFor() blocks as above though, you may be better off specifically checking the text's non-existence.
Without seeing your code it's difficult to go any further. I always say keep tests short and simple, we're testing one thing. The more complex they get the more changes for unforeseen errors. It looks like you're rendering, awaiting a modal closure, then a click, then another modal closure, then more text on the screen. I'd split it into two or more tests.

Unit testing a custom hook to ensure that it calls another hook

How can we ensure that a custom hook actually calls a method exposed by another hook?
Let's say, I have a custom hook useName that internally leverages useState.
import { useState } from 'react'
export const useName = () => {
const [name, setState] = useState()
const setName = (firstName: string, lastName: string) => setState([firstName, lastName].join(' '))
return {name, setName}
}
I need to assert that calling setName actually calls `setState'. My test case is written as following:
/**
* #jest-environment jsdom
*/
import * as React from 'react'
import { renderHook, act } from '#testing-library/react-hooks'
import { useName } from './useName'
jest.mock('react')
const setState = jest.fn()
React.useState.mockReturnValue(['ignore', setState]) //overwriting useState
test('ensures that setState is called', () => {
const {result} = renderHook(() => useName())
act(() => {
result.current.setName("Kashif", "Nazar") //I am expecting this to hit jest.fn() declared above.
})
expect(setState).toBeCalled()
})
and I get the following result.
FAIL src/useName.test.ts
✕ ensures that setState is called (3 ms)
● ensures that setState is called
TypeError: Cannot read property 'setName' of undefined
18 |
19 | act(() => {
> 20 | result.current.setName("Kashif", "Nazar")
| ^
21 | })
22 |
23 | expect(setState).toBeCalled()
at src/useName.test.ts:20:24
at batchedUpdates$1 (node_modules/react-dom/cjs/react-dom.development.js:22380:12)
at act (node_modules/react-dom/cjs/react-dom-test-utils.development.js:1042:14)
at Object.<anonymous> (src/useName.test.ts:19:5)
at TestScheduler.scheduleTests (node_modules/#jest/core/build/TestScheduler.js:333:13)
at runJest (node_modules/#jest/core/build/runJest.js:404:19)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
Snapshots: 0 total
Time: 0.32 s, estimated 1 s
Ran all test suites.
Is this possible, and am I doing it the right way?
You should test the returned state instead of the implementation detail(setState). Mock may destroy the functionality of setState. This causes the test case to pass, but the code under test will fail at the actual run time. And mock also make the test vulnerable, when your implementation details change, your test cases have to change accordingly such as mock the new object.
I only test if the interface is satisfied, no matter how the implementation details change, right
useName.ts:
import { useState } from 'react';
export const useName = () => {
const [name, setState] = useState('');
const setName = (firstName: string, lastName: string) => setState([firstName, lastName].join(' '));
return { name, setName };
};
useName.test.ts:
import { renderHook, act } from '#testing-library/react-hooks';
import { useName } from './useName';
describe('70381825', () => {
test('should pass', () => {
const { result } = renderHook(() => {
console.count('render');
return useName();
});
expect(result.current.name).toBe('');
act(() => {
result.current.setName('Kashif', 'Nazar');
});
expect(result.current.name).toBe('Kashif Nazar');
act(() => {
result.current.setName('a', 'b');
});
});
});
Test result:
PASS examples/70381825/useName.test.ts
70381825 - mock way
○ skipped should pass
70381825
✓ should pass (29 ms)
console.count
render: 1
at examples/70381825/useName.test.ts:31:15
console.count
render: 2
at examples/70381825/useName.test.ts:31:15
console.count
render: 3
at examples/70381825/useName.test.ts:31:15
Test Suites: 1 passed, 1 total
Tests: 1 skipped, 1 passed, 2 total
Snapshots: 0 total
Time: 1.251 s, estimated 8 s
Now, if you insist to use a mock way. You should only mock useState hook of React. jest.mock('react') will create mocks for all methods, properties, and functions exported by React, and this will break their functions.
E.g.
useName.test.ts:
import { renderHook, act } from '#testing-library/react-hooks';
import { useName } from './useName';
import React from 'react';
jest.mock('react', () => {
return { ...(jest.requireActual('react') as any), useState: jest.fn() };
});
describe('70381825 - mock way', () => {
test('should pass', () => {
const setState = jest.fn();
(React.useState as jest.MockedFunction<typeof React.useState>).mockReturnValue(['ignore', setState]);
const { result } = renderHook(() => {
console.count('render');
return useName();
});
act(() => {
result.current.setName('a', 'b');
});
expect(result.current.name).toBe('ignore');
expect(setState).toBeCalled();
act(() => {
result.current.setName('c', 'd');
});
});
});
Test result:
PASS examples/70381825/useName.test.ts (7.885 s)
70381825 - mock way
✓ should pass (29 ms)
70381825
○ skipped should pass
console.count
render: 1
at examples/70381825/useName.test.ts:14:15
Test Suites: 1 passed, 1 total
Tests: 1 skipped, 1 passed, 2 total
Snapshots: 0 total
Time: 8.487 s
Ok. Do you know why the mock way only renders one time and the other way renders three times when we call the setName? As I said earlier.

Test a component with useState and setTimeout

Code structure is as same as given below:
FunctionComponent.js
...
const [open, handler] = useState(false);
setTimeout(() => {handler(true);}, 2000);
...
return (
...
<div className={active ? 'open' : 'close'}>
)
comp.test.js
jest.useFakeTimers();
test('test case 1', () => {
expect(wrapper.find('open').length).toBe(0);
jest.advanceTimersByTime(2000);
expect(wrapper.find('open').length).toBe(1);
jest.useRealTimers();
});
The problem is that the expression written in bold in test is saying the length of open class is still 0, so actual and expected are not meeting.
You want to test the outcome of the hook and not the hook itself since that would be like testing React. You effectively want a test where you check for if the open class exists and then doesn't exist (or vice versa), which it looks like you're trying.
In short, to solve your issue you need to use ".open" when selecting the class. I would also suggest using the .exists() check on the class instead of ".length()" and then you can use ".toBeTruthy()" as well.
You could look into improve writing your tests in a Jest/Enzyme combined format as well:
import { shallow } from 'enzyme';
import { FunctionComponent } from './FunctionComponent.jsx';
jest.useFakeTimers();
describe('<FunctionCompnent />', () => {
const mockProps = { prop1: mockProp1, prop2: mockProp2, funcProp3: jest.fn() };
const wrapper = shallow(<FunctionComponent {...mockProps} />);
afterEach(() => {
jest.advanceTimersByTime(2000);
});
afterAll(() => {
jest.useRealTimers();
});
it('should render as closed initially', () => {
expect(wrapper.find('.close').exists()).toBeTruthy();
// you could also add the check for falsy of open if you wanted
// expect(wrapper.find('.open').exists()).toBeFalsy();
});
it('should change to open after 2 seconds (or more)', () => {
expect(wrapper.find('.open').exists()).toBeTruthy();
// you could also add the check for falsy of close if you wanted
// expect(wrapper.find('.close').exists()).toBeFalsy();
});
});
EDIT: Sorry realised I wrote the test backwards after checking your code again, they should be fixed now.

How to simulate/test key press using Jest?

I'm trying to simulate/test the keypress event which calls props.onClick. I successfully simulated the onClick with similar testing code, but the keypress doesn't work.
When I tested it manually by pressing the Enter key, it works as it should, but when I try to write tests for it, it keeps failing and I'm not sure why. I put a logger in the useEffect function, and it gets called during the test, but it fails and gives an error that onClickFnc was never called.
In my button function component, I bind an event listener:
button.js
useEffect(() => {
if (enterEnable) {
window.addEventListener('keydown', onEnterPress);
return () => window.removeEventListener('keydown', onEnterPress);
}
});
const onEnterPress = e => {
if (e.which == 13) {
onClickSync(e);
}
};
const onClickSync = e => {
props.onClick(e);
};
button.test.js
it('check ButtonLoader enable enter to work', () => {
const onClickFnc = jest.fn();
const wrapper = shallow(<ButtonLoader {...props} enterEnable={true} onClick={onClickFnc}/>);
wrapper.find('button').simulate('keypress', {key: 'Enter'})
expect(onClickFnc).toBeCalled();
});
Error:
expect(jest.fn()).toBeCalled()
Expected number of calls: >= 1
Received number of calls: 0
How can I successfully test this?
Try this instead
wrapper.find('button').simulate('keydown', {which: 13})
From what I have seen, simulate doesn't create an event object from the arguments. Instead it passes the object as is.
try with this code
element.simulate('keydown', { key: 'Enter' });
this worked fine for me. hope it also works for you.

React + Jest + Enzyme form onSubmit test fails unless I use timeout

I have a React form wrapper that takes an onSubmit prop of type function, and I want to test if it's being called with the correct form values. If I check it right away, it fails, but if I put the check behind a 100ms timeout, it passes.
So it appears as if there needs to be some kind of processing time for the functions to be executed, but the timeout is so janky...
Example
test('onSubmit function is called with correct form values', () => {
const defaults = {
email: 'superadmin#gmail.com',
password: 'soSoSecret!'
};
const submitFunction = jest.fn();
const wrapper = mount(<LoginForm onSubmit={submitFunction} />);
// input form values
wrapper
.find('input[name="email"]')
.simulate('change', { target: { value: defaults.email } });
wrapper
.find('input[name="password"]')
.simulate('change', { target: { value: defaults.password } });
expect(submitFunction.mock.calls.length).toBe(0);
wrapper.find('form button[type="submit"]').simulate('click');
// if I test right away, it fails, but if I set timeout for 100ms, it passes
// does the form take time to process and the function be called????
jest.useFakeTimers();
setTimeout(() => {
expect(submitFunction.mock.calls.length).toBe(1);
const args = submitFunction.mock.calls[0][0];
expect(args).toEqual(defaults);
}, 100);
});
Is it possible to write a test like this without a timeout? Seems like an issue to me.
Thanks for reading! :D
One reason that I could think of is that you are trying to test something that happens asynchronously, and not right away. In that case, you can try using async await
PFB example:
test('onSubmit function is called with correct form values', async () => {
const defaults = {
email: 'superadmin#gmail.com',
password: 'soSoSecret!'
};
const submitFunction = jest.fn();
const wrapper = mount(<LoginForm onSubmit={submitFunction} />);
// input form values
wrapper
.find('input[name="email"]')
.simulate('change', { target: { value: defaults.email } });
wrapper
.find('input[name="password"]')
.simulate('change', { target: { value: defaults.password } });
expect(submitFunction.mock.calls.length).toBe(0);
await wrapper.find('form button[type="submit"]').simulate('click');
expect(submitFunction.mock.calls.length).toBe(1);
const args = submitFunction.mock.calls[0][0];
expect(args).toEqual(defaults);
});
Notice the async keyword before the test case callback begins, and the await keyword before the submit button click is simulated.

Resources