I'm updating an app that was originally built in React 16 and all tests created with enzyme. I have updated to React 17 and am rewriting all tests using React testing library. I'm having trouble figuring out how to test methods on a parent or grandparent component when the method is called in the child/grandchild.
The structure looks like
App
=> SubmissionForm
=> Tab1
(it's a multi-tabbed form and each tab is a separate component)
When the user clicks 'submit' on Tab1
, it calls a series of methods on SubmissionForm
, ending in verifyRecaptchaScore
.
verifyRecaptchaScore
calls execute
from the package react-google-invisible-recaptcha
, which calls the callback onResolved
. Both the Recaptcha
component and the onResolved
callback are on the App
component.
(App.js)
...
async onResolved() {
const token = await this.recaptcha.current.getResponse();
this.props.apiSubmission.handleInput({
target: { name: "reCaptchaValue", value: token }
});
}
...
<Recaptcha
ref={refCaptcha}
sitekey={sitekey}
onResolved={this.onResolved}
/>
I'm trying to write tests for the methods on the App
component. In the old app structure, using enzyme, I was able to test the onResolved
method by just calling it programmatically in the test, using wrapper.instance().onResolved()
some relevant excerpts of the old test below:
...
const unconnectedSetup = async () => {
const setupProps = { ...defaultProps };
return shallow(<AppUnconnected {...setupProps} />);
};
...
describe("<App />", () => {
...
describe("Misc methods", () => {
it("onResolved calls recaptcha.getResponse and saves recaptcha token to redux store", async () => {
wrapper = await unconnectedSetup();
getResponseMock = jest
.fn()
.mockImplementation(() => Promise.resolve("token"));
wrapper.instance().recaptcha = {
getResponse: getResponseMock
};
await wrapper.update();
await wrapper.instance().onResolved();
expect(getResponseMock.mock.calls.length).toBe(1);
await getResponseMock().then(() => {
expect(handleInputMock).toHaveBeenCalledWith({
target: { name: "reCaptchaValue", value: "token" }
});
});
});
...
});
With React testing library, I understand that I'm supposed to try to mimic the user experience rather than call methods programmatically. Which would mean something like:
- Load
App
- User clicks
Next
button to loadTab1
- User clicks
Submit
button on Tab1 which triggers the chain of methods that eventually callsonResolved
but I can't get this to work. I'm able to simulate the user click that loads Tab1
but when I try to simulate a user click on the Submit
button on that tab I can't get it to call any of the other methods, and I'm struggling to even figure out where or how it's failing.
I have tried both
userEvent.click(submitButton);
and
fireEvent.submit(tab1Form);
but neither one seems to work.
Code that's not working:
describe("Misc methods", () => {
it("onResolved calls recaptcha.getResponse and saves recaptcha token to redux store", async () => {
getResponseMock = jest
.fn()
.mockImplementation(() => Promise.resolve("token"));
const props = {
recaptcha: {
current: {
getResponseMock
}
},
formValues: {
...generateSubmissionBody
}
};
// simulate user click 'Next'
const user = userEvent.setup();
const { getByTestId, getByRole } = await setup({ ...props });
const nextButton = getByTestId("button-next");
await userEvent.click(nextButton);
// check that tab 1 renders after clicking 'Next'
const tab1Form = getByRole("form");
await waitFor(() => {
expect(tab1Form).toBeInTheDocument();
});
// simulate submit with default formValues
const submitButton = getByTestId("button-submit");
await waitFor(() => {
userEvent.click(submitButton);
// have also tried fireEvent.submit(tab1Form);
});
// expect the mock to have been called once
await waitFor(() => {
expect(getResponseMock).toHaveBeenCalled();
});
// the above test fails; mock is not called.
// restore mock
getResponseMock.mockRestore();
});
Is this even the right approach for testing this method? If the method is on App
but is triggered from Tab1
does anybody have any better suggestions for how to test this? Or how to test a recaptcha callback in general?
If this is the right approach, any suggestions for how to troubleshoot why none of the intermediate methods (or onResolved
) are being called when I simulate the user click on Submit
? When I try this:
const handleSubmit = jest.fn();
tab1Form.onsubmit = handleSubmit;
// simulate submit with default formValues
await waitFor(() => {
fireEvent.submit(tab1Form);
expect(handleSubmit).toHaveBeenCalled();
});
the test passes, so I know that fireEvent.submit()
is simulating the submission; i just can't get it to work with the real methods that it's supposed to call onSubmit.