14

I have a component. It has a button. Upon pressing the button, I am changing the style of the button text (color) using setState function. When I am testing the changed component, the test is failing because the change happens asynchronously. I want to do something as is given here (https://testing-library.com/docs/dom-testing-library/api-async/)

const button = screen.getByRole('button', { name: 'Click Me' })
fireEvent.click(button)
await screen.findByText('Clicked once')
fireEvent.click(button)
await screen.findByText('Clicked twice')

But rather than waiting for the text to change. I want to wait for the text color to change. Thanks

This is the code for my button

<Button onPress = {() => {this.setState({state : 1});}}>
<Text style = {style}>Button Text</Text>
</Button>

So when this button is pressed. state is set to 1. And in render :

if(this.state.state === 1) style = style1
else style = style2;

But it can be seen from logs that render is called after the test checks for the styles. So How can I wait for the render to complete before checking if the font color has been changed?

Here is the testing code

test('The button text style changes after press', () => {
  const {getByText} = render(<Component/>);
  fireEvent.press(getByText('button'));
  expect(getByText('button')).toHaveStyle({
    color : '#ffffff'
  });
})
bsheps
  • 1,438
  • 1
  • 15
  • 26
Prateek Mishra
  • 311
  • 1
  • 2
  • 9

2 Answers2

19

It looks like you have a custom button, not a native button. I'm guessing your component is something like this:

import React from "react";
import {Text, TouchableOpacity} from "react-native";

const Button = ({pressHandler, children}) => (
  <TouchableOpacity onPress={pressHandler}>
    {children}
  </TouchableOpacity>
);

const ColorChangingButton = ({text}) => {
  const [color, setColor] = React.useState("red");
  const toggleColor = () => setTimeout(() => 
    setColor(color === "green" ? "red" : "green"), 1000
  );
  return (
    <Button pressHandler={toggleColor}>
      <Text style={{color}}>{text}</Text>
    </Button>
  );
};
export default ColorChangingButton;

If so, you can test it with waitFor as described here:

import React from "react";
import {
  fireEvent, 
  render,
  waitFor,
} from "@testing-library/react-native";
import ColorChangingButton from "../src/components/ColorChangingButton";

it("should change the button's text color", async () => {
  const text = "foobar";
  const {getByText} = render(<ColorChangingButton text={text} />);
  fireEvent.press(getByText(text));
  await waitFor(() => {
    expect(getByText(text)).toHaveStyle({color: "green"});
  });
});

For a native button which has rigid semantics for changing colors and doesn't accept children, instead using title="foo", a call to debug() shows that it expands to a few nested elements. You can use

const text = within(getByRole("button")).getByText(/./);
expect(text).toHaveStyle({color: "green"});

inside the waitFor callback to dip into the button's text child and wait for it to have the desired color.

I used the same packages/versions for this post as shown in React Testing Library: Test if Elements have been mapped/rendered.

ggorlen
  • 44,755
  • 7
  • 76
  • 106
  • 1
    Although this answer is accepted, unfortunately, (1) this does not scale (2) this produces fragile test code (3) this is not a general approach to the problem. – DarkTrick Oct 14 '22 at 06:26
  • 2
    @DarkTrick Thanks for the feedback, but your assertions are vague and unactionable. Why doesn't it "scale"? Why is it fragile? Why isn't it a general approach to the problem? Please clarify with your own answer, or let me know what I can do specifically to improve this answer. I'll check out the docs and Kent's articles and try to figure out what's wrong in the meantime. It's true that querying by text and testing CSS props like this aren't good practice, but the key point of the answer is the `waitFor` rather than the queries. It's a contrived, minimal example to illustrate a single point. – ggorlen Oct 14 '22 at 14:31
  • Thank you for comming back to me! (1) Not scaling: Imagine you have several async changes you need to wait for in order to trigger another event. You would need to `waitFor` every single component specifically. This is what a human would do (so, theoretically correct), but it will bloat your test code and as soon as you change one component, you will need to change all/the waitFor-statement (impractical). (2) Fragility: To reduce code, you might be tempted to wait only for the *last* component to finish. This will soon break, if you change the UI a little bit. – DarkTrick Oct 16 '22 at 11:48
  • (3) A more general (+scalabe + less-fragile) approach would be to *wait for all asynchronous actions to finish* and then do whatever you need to do (next action) or check (`expect`) \n Perhaps my comment was a little short sighted on a theoretical level, but I think it's valid on a practical one. \n I'd like to emphasize, that the following is not about this very specific use case, but for the approach of *specifying a waitFor on a specific component*. – DarkTrick Oct 16 '22 at 11:49
  • 1
    Thanks for clarifying, but I still don't follow what you're proposing as a better solution than `waitFor`. – ggorlen Oct 17 '22 at 13:19
  • I don't have a better one at hand; still searching myself. – DarkTrick Oct 21 '22 at 12:16
0

You can try

<Text style = {this.state.state === 1 ? style1 : style2}>Button Text</Text>

This will consequently lead to the style being defined all time. So you don't have to wait for the setState to complete.
Edit
You can use the callback provided by setState function to perform your tests for styles.

this.setState({
   state : 1
} , () => {
    //this is called only after the state is changed
    //perform your test here
})
Rohit Aggarwal
  • 1,048
  • 1
  • 14
  • 32
  • 1
    No. I want to change the style on button Pressed. It's getting changed but asynchronously. So my test finishes before the button is re-rendered which is causing it to fail. I want to wait for the re-render. – Prateek Mishra Jun 16 '21 at 17:31
  • 1
    Yes that can be done. But the way unit testing works is that your tests should be written independent to the code. Please refer the link I have shared in the question description, on that page you will see that the author has used `await screen.findByText('Clicked once')` So in the test the author is waiting for the text 'Clicked once' to appear, because he is changing the text on his component. In the same way I want to wait for the new font color to appear. So I wanted to know if there was any way which helps me to do this in react testing library or jest. – Prateek Mishra Jun 17 '21 at 08:23