react-native-testing-library: fireEvent seems to always trigger event (on disabled TouchableOpacity)

I’m trying to figure out why it seems like the fireEvent.press function always seem to trigger a callback even though it shouldn’t be registered. My use case is that I’m trying to write a test that makes sure that onPress isn’t called if the component is “disabled”.

Trying to mimic the behaviour below…

  const MyComponent = _props => (
    <View>
      <TouchableHighlight testID={'touchable'}>
        <Text>{'Foo'}</Text>
      </TouchableHighlight>
    </View>
  )

  const pressedCallback = jest.fn()
  const { getByTestId } = render(<MyComponent onPress={() => pressedCallback('bar')} />)
  fireEvent.press(getByTestId('touchable'))
  expect(pressedCallback).not.toBeCalled() //Expected mock function not to be called but it was called with: ["bar"]

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 8
  • Comments: 30 (15 by maintainers)

Most upvoted comments

could it be this bug is back again? Because Im testing with the extended version that is disabled like this:

expect(something).toBeDisabled();

and this passes correctly but then I do this:

fireEvent.press(something);
expect(function).toHaveBeenCalledTimes(0);

and this fails saying it was called 1 time 😢

Im using "@testing-library/react-native": "^9.1.0"

For example this works:

  const MyComponent = ({ disabled, onPress }) => (
    <View>
      <TouchableHighlight onPress={disabled ? () => {} : onPress} testID={'touchable'}>
        <Text>{'Foo'}</Text>
      </TouchableHighlight>
    </View>
  )

But then again I’m changing implementation to satisfy the test 😅

@TDAK1509 I encourage you to use https://github.com/jest-community/snapshot-diff in such cases 😃

Yup, we have an idea on how to make it more generic without changing implementation of fireEvent. Bare with us while we’re fixing this.

@alexakasanjeev we’re not currently working on this. But the proper place to fix it is the Touchable* components in RN core. I won’t patch the library to work around specific logic that lives on the native side.

For example, there’s a new Pressable component coming up, which may or may not have this issue. Shall we patch it then as well? What about other components? I hope you see my point. This is an upstream “bug” (or limitation) and I only leave it open for folks to be aware.

I can only advise to always wrap RN touchable components into custom components which you can control. This will also make touchables unified across your app, so it’s worth doing anyway. A sample implementation: https://github.com/react-native-community/react-native-platform-touchable/blob/master/PlatformTouchable.js of such primitive

We also just came across this issues. I have added a mock in the setup.js to keep it tucked away and ignore moving forward 🙄

jest.mock('TouchableOpacity', () => {
  const RealComponent = require.requireActual('TouchableOpacity')
  const MockTouchableOpacity = props => {
    const { children, disabled, onPress } = props
    return React.createElement(
      'TouchableOpacity',
      { ...props, onPress: disabled ? () => {} : onPress },
      children,
    )
  }
  MockTouchableOpacity.propTypes = RealComponent.propTypes
  return MockTouchableOpacity
})

I test the disable status by using snapshot to check the style. (When disabled, my button has a different color)

    // Default is disabled
    it("matches snapshot", () => {
      expect(wrapper).toMatchSnapshot();
    });

    // Button is enabled when an image is selected
    it("matches snapshot when image is selected", () => {
      const { getAllByTestId } = wrapper;
      const checkBox = getAllByTestId("checkbox");
      fireEvent.press(checkBox[0]);
      expect(wrapper).toMatchSnapshot();
    });

After doing this, in snapshot file there will be 2 cases for the button, which will be disabled state and enabled state with different colors.

<View
    accessible={true}
    focusable={true}
    onClick={[Function]}
    onResponderGrant={[Function]}
    onResponderMove={[Function]}
    onResponderRelease={[Function]}
    onResponderTerminate={[Function]}
    onResponderTerminationRequest={[Function]}
    onStartShouldSetResponder={[Function]}
    style={
      Object {
        "alignItems": "center",
        "borderRadius": 50,
        "bottom": 80,
        "justifyContent": "center",
        "opacity": 1,
        "position": "absolute",
        "right": 30,
        "zIndex": 10,
      }
    }
    testID="deleteButton"
  >
    <Text
      allowFontScaling={false}
      style={
        Array [
          Object {
            "color": "#948f8f", <--- grey for disabled state
            "fontSize": 40,
          },
          undefined,
          Object {
            "fontFamily": "anticon",
            "fontStyle": "normal",
            "fontWeight": "normal",
          },
          Object {},
        ]
      }
    >
      
    </Text>
  </View>
<View
    accessible={true}
    focusable={true}
    onClick={[Function]}
    onResponderGrant={[Function]}
    onResponderMove={[Function]}
    onResponderRelease={[Function]}
    onResponderTerminate={[Function]}
    onResponderTerminationRequest={[Function]}
    onStartShouldSetResponder={[Function]}
    style={
      Object {
        "alignItems": "center",
        "borderRadius": 50,
        "bottom": 80,
        "justifyContent": "center",
        "opacity": 1,
        "position": "absolute",
        "right": 30,
        "zIndex": 10,
      }
    }
    testID="deleteButton"
  >
    <Text
      allowFontScaling={false}
      style={
        Array [
          Object {
            "color": "#000", <--- black for enabled state
            "fontSize": 40,
          },
          undefined,
          Object {
            "fontFamily": "anticon",
            "fontStyle": "normal",
            "fontWeight": "normal",
          },
          Object {},
        ]
      }
    >
      
    </Text>
  </View>

I’d extract disabled to a variable since it’s reused, but yes.

For test purposes you’ll need to make sure by yourself that your events won’t fire when disabled prop is passed, there’s not really other way until the mocks are fixed.

We may, however, consider some special treatment for Touchable* components when in RN context (usual), as these are special and e.g. cancel-out event bubbling, unlike one the web where events propagate up freely unless stopped explicitly by the user.

Just merged #30 so the bug is fixed now (not released yet). As for disabled prop, this is something that should be fixed in RN mocks tbh. We’ll need to think about simulating native events capturing as close as possible though (e.g. including capturing phase).

I think it makes sense to stop bubbling just before root component, that would prevent this and seems like a sane solution, but haven’t thought it through yet