'How to fire events on multiple layers of custom components with react unit testing
I'm writing a test case for a feature I'm unit testing. I want to edit a text field that's embedded within several custom components but I can't figure out how to access it:
TestFile.test.tsx
it('should be able to change quantity', () =>{
//Mock out dependent function with jest
//Nothing here right now
//Render with the props you want
render(
<MainComponent /> );
//Locate screen components for test
const counter = screen.queryByTestId("counter");
//Perform user actions
fireEvent.change(counter, 4); <--This is the line where I'm trying to access what I want
//Measure against expect cases
expect(counter).toBe(4);
});
MainComponent.tsx
This is the component I'm trying to access. The problem is, `fireEvent.change` won't work because it's a custom component. At least, I think that's why it doesn't work.<Counter
data-testid='counter' <--data-testid is here right now
containerMargin={{ marginTop: -5 }}
/>
Counter.tsx
Inside `Counter` is another custom component, `TextField`<TextField
{...props}
/>
TextField.tsx
And inside TextField is where the text input I want to access is:<TextInput
{...props}
/>
Error Message
When I try to run the test, I get this error: ● Test › should be able to change quantity
Unable to fire a "change" event - please provide a DOM element.
189 |
190 | //Perform user actions
> 191 | fireEvent.change(counter, 4);
| ^
192 |
193 | //Measure against expect cases
194 | expect(counter).toBe(4);
I can't put the data-testid property inside TextField or the TextInput itself, because there's another Counter in the render of the page I'm testing. Any advice or troubleshooting would be helpful, thanks!
Solution 1:[1]
The getByTestId query is an escape hatch.
In the spirit of the guiding principles, it is recommended to use this only after the other queries don't work for your use case. Using data-testid attributes do not resemble how your software is used and should be avoided if possible.
Your text field should be accessible. And the accessibility should be leveraged to access the element.
const input = screen.getByRole('textbox', {name: t('priceMultiple')})
// or
const input = screen.getByLabelText(t('priceMultiple'))
Using fireEvent directly is also an escape hatch.
Most projects have a few use cases for
fireEvent, but the majority of the time you should probably use@testing-library/user-event.
This recommendation is further explained in the user-event docs and in this blog post.
await userEvent.type(input, '4')
Note that setting up a user-event instance and describing the exact workflow a user performs on the component is recommended for most cases. So most tests should look something like this.
const user = userEvent.setup()
render(<MyComponent/>)
await user.tripleClick(screen.getByRole('textbox', {name: /some label/}))
await user.keyboard('4')
// ... assert that the component output is correct ...
await user.tab()
await user.keyboard('567') // edit another field
// ... perform more assertions ...
Solution 2:[2]
Generally speaking , you are right, adding data-testid to a React component is useless unless you propagate this prop down to the DOM element.
Luckily for you <TextField /> accepts inputProps which it propagates to the <input /> element.
For example
export default function BasicTextFields() {
return (
<ChildComponent data-testid="counter" />
);
}
const ChildComponent = (props) => {
return (
<TextField
inputProps={{ "data-testid": props["data-testid"] }}
id="outlined-basic"
label="Outlined"
variant="outlined"
/>
);
};
Then you can use getByTestId in tests.
test("should be able to trigger input change", () => {
render(<BasicTextFields />);
const input = screen.getByTestId("counter");
fireEvent.change(input, { target: { value: "23" } });
expect(input.value).toBe("23");
});
https://codesandbox.io/s/flamboyant-joliot-0f49v?file=/src/App.js
I didn't create the deep hierarchy you created to make my answer cleaner but as long as you propagate the props down to the <TextField /> you should be good. You can also use Context but this is out of this answer's scope.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Philipp Fritsche |
| Solution 2 |
