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

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.

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.

How to fire and test a real paste event (not simulated by calling the prop) in Jest and Enzyme

I'm trying to unit test a very simple feature in a React app where I'm blocking the user from pasting into a textarea by adding an event.preventDefault() in the event handler, like so:
function handlePaste(event) {
event.preventDefault();
}
// ... pass it down as props
<TextareaComponent onPaste={handlePaste} />
The problem I'm having is that every method I've found of dispatching events in Jest or Enzyme just "simulates" the event by getting the function passed to the onPaste prop and calling it directly with a mock event object. That's not what I'm interested in testing.
Ideally I want to do something like this, testing that the actual value of the input hasn't changed after pasting:
const wrapper = mount(<ParentComponent inputValue="Prefilled text" />);
const input = wrapper.find(TextareaComponent);
expect(input.value).toEqual("Prefilled text")
input.doAPaste("Pasted text")
expect(input.value).not.toEqual("Pasted text")
expect(input.value).toEqual("Prefilled text")
But haven't been able to find a method that works. Any help would be appreciated!
Since you're just testing against a synthetic event (and not some sort of secondary action -- like a pop up that warns the user that pasting is disabled), then the easiest and correct solution is to simulate a paste event, pass it a mocked preventDefault function, and then assert that the mocked function was called.
Attempting to make assertions against a real paste event is pointless as this a React/Javascript implementation (for example, making assertions that a callback function is called when an onPaste/onChange event is triggered). Instead, you'll want to test against what happens as a result of calling the callback function (in this example, making assertions that event.preventDefault was called -- if it wasn't called, then we know the callback function was never executed!).
Working example (click the Tests tab to run the assertions):
To keep it simple, I'm just asserting that the input is initially empty and then only updates the value if an onChange event was triggered. This can very easily be adapted to have some sort of passed in prop influence the default input's value.
App.js
import React, { useCallback, useState } from "react";
const App = () => {
const [value, setValue] = useState("");
const handleChange = useCallback(
({ target: { value } }) => setValue(value),
[]
);
const handlePaste = useCallback((e) => {
e.preventDefault();
}, []);
const resetValue = useCallback(() => {
setValue("");
}, []);
const handleSubmit = useCallback(
(e) => {
e.preventDefault();
console.log(`Submitted value: ${value}`);
setValue("");
},
[value]
);
return (
<form onSubmit={handleSubmit}>
<label htmlFor="foo">
<input
id="foo"
type="text"
data-testid="test-input"
value={value}
onPaste={handlePaste}
onChange={handleChange}
/>
</label>
<br />
<button data-testid="reset-button" type="button" onClick={resetValue}>
Reset
</button>
<button type="submit">Submit</button>
</form>
);
};
export default App;
App.test.js
import React from "react";
import { configure, mount } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import App from "./App";
configure({ adapter: new Adapter() });
const value = "Hello";
describe("App", () => {
let wrapper;
let inputNode;
beforeEach(() => {
wrapper = mount(<App />);
// finding the input node by a 'data-testid'; this is not required, but easier
// when working with multiple form elements and can be easily removed
// when the app is compiled for production
inputNode = () => wrapper.find("[data-testid='test-input']");
});
it("initially displays an empty input", () => {
expect(inputNode()).toHaveLength(1);
expect(inputNode().props().value).toEqual("");
});
it("updates the input's value", () => {
inputNode().simulate("change", { target: { value } });
expect(inputNode().props().value).toEqual(value);
});
it("prevents the input's value from updating from a paste event", () => {
const mockPreventDefault = jest.fn();
const prefilledText = "Goodbye";
// updating input with prefilled text
inputNode().simulate("change", { target: { value: prefilledText } });
// simulating a paste event with a mocked preventDefault
// the target.value isn't required, but included for illustration purposes
inputNode().simulate("paste", {
preventDefault: mockPreventDefault,
target: { value }
});
// asserting that "event.preventDefault" was called
expect(mockPreventDefault).toHaveBeenCalled();
// asserting that the input's value wasn't changed
expect(inputNode().props().value).toEqual(prefilledText);
});
it("resets the input's value", () => {
inputNode().simulate("change", { target: { value } });
wrapper.find("[data-testid='reset-button']").simulate("click");
expect(inputNode().props().value).toEqual("");
});
it("submits the input's value", () => {
inputNode().simulate("change", { target: { value } });
wrapper.find("form").simulate("submit");
expect(inputNode().props().value).toEqual("");
});
});

Use toHaveBeenCalledWith to check EventTarget

I've built a custom Input React component (think wrapper) that renders an HTML input element. When a value is entered the change is passed to the parent component like this:
const handleChange = (event: SyntheticInputEvent<EventTarget>) => {
setCurrentValue(event.target.value);
props.onChange(event);
};
Everything works as expected but now I want to write a test for it:
it('Should render a Text Input', () => {
const onChange = jest.fn();
const { queryByTestId } = renderDom(
<Input type={InputTypesEnum.TEXT} name='car' onChange={onChange} />
);
const textInput = queryByTestId('text-input');
expect(textInput).toBeTruthy();
const event = fireEvent.change(textInput, { target: { value: 'Ford' }});
expect(onChange).toHaveBeenCalledTimes(1);
});
This works fine too except that I want to add a final expect using toHaveBeenCalledWith. I've tried several things but can't figure out how to do it. Any ideas?
Update: I've been reading this: https://reactjs.org/docs/events.html#event-pooling. It appears that if I change handleChange like this:
const handleChange = (event: SyntheticInputEvent<EventTarget>) => {
event.persist();
setCurrentValue(event.target.value);
props.onChange(event);
};
then the received object from onChange does change to include my test data. That said, I don't like the idea of altering an important feature of production code (in this case, event pooling) simply to accommodate a test.
You can do something like this with toHaveBeenCalledWith
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({
target: expect.objectContaining({
value: "Ford"
})
})
);

Jest/Enzyme Shallow testing RFC - not firing jest.fn()

I'm trying to test the onChange prop (and the value) of an input on an RFC. On the tests, trying to simulate the event doesn't fire the jest mock function.
The actual component is connected (with redux) but I'm exporting it also as an unconnected component so I can do a shallow unit test. I'm also using some react-spring hooks for animation.
I've also tried to mount instead of shallow the component but I still get the same problem.
MY Component
export const UnconnectedSearchInput: React.FC<INT.IInputProps> = ({ scrolled, getUserInputRequest }): JSX.Element => {
const [change, setChange] = useState<string>('')
const handleChange = (e: InputVal): void => {
setChange(e.target.value)
}
const handleKeyUp = (): void => {
getUserInputRequest(change)
}
return (
<animated.div
className="search-input"
data-test="component-search-input"
style={animateInputContainer}>
<animated.input
type="text"
name="search"
className="search-input__inp"
data-test="search-input"
style={animateInput}
onChange={handleChange}
onKeyUp={handleKeyUp}
value={change}
/>
</animated.div>
)
}
export default connect(null, { getUserInputRequest })(UnconnectedSearchInput);
My Tests
Here you can see the test that is failing. Commented out code is other things that I-ve tried so far without any luck.
describe('test input and dispatch action', () => {
let changeValueMock
let wrapper
const userInput = 'matrix'
beforeEach(() => {
changeValueMock = jest.fn()
const props = {
handleChange: changeValueMock
}
wrapper = shallow(<UnconnectedSearchInput {...props} />).dive()
// wrapper = mount(<UnconnectedSearchInput {...props} />)
})
test('should update input value', () => {
const input = findByTestAttr(wrapper, 'search-input').dive()
// const component = findByTestAttr(wrapper, 'search-input').last()
expect(input.name()).toBe('input')
expect(changeValueMock).not.toHaveBeenCalled()
input.props().onChange({ target: { value: userInput } }) // not geting called
// input.simulate('change', { target: { value: userInput } })
// used with mount
// act(() => {
// input.props().onChange({ target: { value: userInput } })
// })
// wrapper.update()
expect(changeValueMock).toBeCalledTimes(1)
// expect(input.prop('value')).toBe(userInput);
})
})
Test Error
Nothing too special here.
expect(jest.fn()).toBeCalledTimes(1)
Expected mock function to have been called one time, but it was called zero times.
71 | // wrapper.update()
72 |
> 73 | expect(changeValueMock).toBeCalledTimes(1)
Any help would be greatly appreciated since it's been 2 days now and I cn't figure this out.
you don't have to interact with component internals; instead better use public interface: props and render result
test('should update input value', () => {
expect(findByTestAttr(wrapper, 'search-input').dive().props().value).toEqual('');
findByTestAttr(wrapper, 'search-input').dive().props().onChange({ target: {value: '_test_'} });
expect(findByTestAttr(wrapper, 'search-input').dive().props().value).toEqual('_test_');
}
See you don't need to check if some internal method has been called, what's its name or argument. If you get what you need - and you require to have <input> with some expected value - it does not matter how it happened.
But if function is passed from the outside(through props) you will definitely want to verify if it's called at some expected case
test('should call getUserInputRequest prop on keyUp event', () => {
const getUserInputRequest = jest.fn();
const mockedEvent = { target: { key: 'A' } };
const = wrapper = shallow(<UnconnectedSearchInput getUserInputRequest={getUserInputRequest } />).dive()
findByTestAttr(wrapper, 'search-input').dive().props().onKeyUp(mockedEvent)
expect(getUserInputRequest).toHaveBeenCalledTimes(1);
expect(getUserInputRequest).toHaveBeenCalledWith(mockedEvent);
}
[UPD] seems like caching selector in interm variable like
const input = findByTestAttr(wrapper, 'search-input').dive();
input.props().onChange({ target: {value: '_test_'} });
expect(input.props().value).toEqual('_test_');
does not pass since input refers to stale old object where value does not update.
At enzyme's github I've been answered that it's expected behavior:
This is intended behavior in enzyme v3 - see https://github.com/airbnb/enzyme/blob/master/docs/guides/migration-from-2-to-3.md#calling-props-after-a-state-change.
So yes, exactly - everything must be re-found from the root if anything has changed.

How to test in Jest the MediaQuerList.addListener

I would like to test that my function has been triggered properly (with the right parameters) but I can't find a way to make it...
I have a custom addEventListener take the name of the media query, the media query itself and a dispatch function
// ... Inside my class
addEventListener(name, mediaQuery, dispatch) {
// Initialize the mediaQueryList and store it in our list
const mediaQueryList = window.matchMedia(mediaQuery);
mediaQueryList.addListener(
mediaQueryListEvent => this.onScreenChange(name, mediaQueryListEvent)
);
this.mediaQueries.set(name, {
mediaQueryList,
dispatch
});
// Then we look even for the first time on which breakPoint we are
this.searchAndDispatchBreakpoint(name, mediaQueryList);
}
Any idea how I can test that my onScreenChange has been properly called with the right arguments?
Ok so after hours and hours of search and tries, I figured out a way.
I succeed in mocking the window.matchMedia and the addListener inside
Once done, I just have to test the arguments sent to my method.
Here is my jest test
it('should test the media query addListener method', () => {
const bpManager = new BreakpointManager();
const mediaQuery = 'max-width: 1080px';
const name = 'name';
const mediaQueryListEvent = 'mediaQueryListEvent';
bpManager.onScreenChange = jest.fn();
window.matchMedia = jest.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: e => e(mediaQueryListEvent),
removeListener: jest.fn(),
}));
bpManager.addEventListener(name, mediaQuery, dispatch);
expect(window.matchMedia).toHaveBeenCalledWith(mediaQuery);
expect(bpManager.onScreenChange).toBeCalledWith(name, mediaQueryListEvent);
});
And my method addEventListener
addEventListener(name, mediaQuery, dispatch) {
// Initialize the mediaQueryList and store it in our list
const mediaQueryList = window.matchMedia(mediaQuery);
mediaQueryList.addListener(
mediaQueryListEvent => this.onScreenChange(name, mediaQueryListEvent)
);
this.mediaQueries.set(name, {
mediaQueryList,
dispatch
});
// Then we look even for the first time on which breakPoint we are
this.searchAndDispatchBreakpoint(name, mediaQueryList);
}

Resources