'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