diff --git a/src/__tests__/fireEvent.test.js b/src/__tests__/fireEvent.test.js index 5a261dba7..fa0b9c255 100644 --- a/src/__tests__/fireEvent.test.js +++ b/src/__tests__/fireEvent.test.js @@ -3,6 +3,7 @@ import React from 'react'; import { View, TouchableOpacity, + Pressable, Text, ScrollView, TextInput, @@ -163,3 +164,81 @@ test('event with multiple handler parameters', () => { expect(handlePress).toHaveBeenCalledWith('param1', 'param2'); }); + +test('should not fire on disabled TouchableOpacity', () => { + const handlePress = jest.fn(); + const screen = render( + + Trigger + + ); + + expect(() => fireEvent.press(screen.getByText('Trigger'))).toThrow( + 'No handler function found for event: "press"' + ); + expect(handlePress).not.toHaveBeenCalled(); +}); + +test('should not fire on disabled Pressable', () => { + const handlePress = jest.fn(); + const screen = render( + + Trigger + + ); + + expect(() => fireEvent.press(screen.getByText('Trigger'))).toThrow( + 'No handler function found for event: "press"' + ); + expect(handlePress).not.toHaveBeenCalled(); +}); + +test('should pass event up on disabled TouchableOpacity', () => { + const handleInnerPress = jest.fn(); + const handleOuterPress = jest.fn(); + const screen = render( + + + Inner Trigger + + + ); + + fireEvent.press(screen.getByText('Inner Trigger')); + expect(handleInnerPress).not.toHaveBeenCalled(); + expect(handleOuterPress).toHaveBeenCalledTimes(1); +}); + +test('should pass event up on disabled Pressable', () => { + const handleInnerPress = jest.fn(); + const handleOuterPress = jest.fn(); + const screen = render( + + + Inner Trigger + + + ); + + fireEvent.press(screen.getByText('Inner Trigger')); + expect(handleInnerPress).not.toHaveBeenCalled(); + expect(handleOuterPress).toHaveBeenCalledTimes(1); +}); + +const TestComponent = ({ onPress }) => { + return ( + + Trigger Test + + ); +}; + +test('is not fooled by non-native disabled prop', () => { + const handlePress = jest.fn(); + const screen = render( + + ); + + fireEvent.press(screen.getByText('Trigger Test')); + expect(handlePress).toHaveBeenCalledTimes(1); +}); diff --git a/src/fireEvent.js b/src/fireEvent.js index 787620c5d..c367679f5 100644 --- a/src/fireEvent.js +++ b/src/fireEvent.js @@ -5,13 +5,19 @@ import { ErrorWithStack } from './helpers/errors'; const findEventHandler = ( element: ReactTestInstance, eventName: string, - callsite?: any + callsite?: any, + nearestHostDescendent?: ReactTestInstance ) => { - const eventHandler = toEventHandlerName(eventName); + const isHostComponent = typeof element.type === 'string'; + const hostElement = isHostComponent ? element : nearestHostDescendent; + const isEventEnabled = + hostElement?.props.onStartShouldSetResponder?.() !== false; + + const eventHandlerName = toEventHandlerName(eventName); - if (typeof element.props[eventHandler] === 'function') { - return element.props[eventHandler]; - } else if (typeof element.props[eventName] === 'function') { + if (typeof element.props[eventHandlerName] === 'function' && isEventEnabled) { + return element.props[eventHandlerName]; + } else if (typeof element.props[eventName] === 'function' && isEventEnabled) { return element.props[eventName]; } @@ -23,7 +29,7 @@ const findEventHandler = ( ); } - return findEventHandler(element.parent, eventName, callsite); + return findEventHandler(element.parent, eventName, callsite, hostElement); }; const invokeEvent = ( diff --git a/website/docs/API.md b/website/docs/API.md index 728cbfd63..e027c6a27 100644 --- a/website/docs/API.md +++ b/website/docs/API.md @@ -188,6 +188,10 @@ test('fire changeText event', () => { }); ``` +:::note +Please note that from version `7.0` `fireEvent` performs checks that should prevent events firing on disabled elements. +::: + An example using `fireEvent` with native events that aren't already aliased by the `fireEvent` api. ```jsx diff --git a/website/docs/MigrationV7.md b/website/docs/MigrationV7.md index 2cadebc82..97852b7a5 100644 --- a/website/docs/MigrationV7.md +++ b/website/docs/MigrationV7.md @@ -44,6 +44,12 @@ To improve compatibility with React Testing Library, and to ease the migration f Please replace all occurrences of these queries in your codebase. +## `fireEvent` support for disabled components + +To improve compatibility with real React Native environment `fireEvent` now performs checks whether the component is disabled before firing an event on it. The checks internally uses `onStartShouldSetResponder` prop to establish should event fire, which should resemble the actual React Native runtime. + +If your code contained any workarounds for preventing events firing on disabled events, you should now be able to remove them. + # Guide for `@testing-library/react-native` users This guide describes steps necessary to migrate from `@testing-library/react-native` from `v6.0` to `v7.0`. Although the name stays the same, this is a different library, sourced at [Callstack GitHub repository](https://github.com/callstack/react-native-testing-library). We made sure the upgrade path is as easy for you as possible. @@ -88,14 +94,6 @@ Cleaning up (unmounting) components after each test is included by default in th You can opt-out of this behavior by running tests with `RNTL_SKIP_AUTO_CLEANUP=true` flag or importing from `@testing-library/react-native/pure`. We encourage you to keep the default though. -## No special handling for `disabled` prop - -The `disabled` prop on "Touchable\*" components is treated in the same manner as any other prop. We realize that with our library you can press "touchable" components even though they're in "disabled" state, however this is something that we strongly believe should be fixed upstream, in React Native core. - -If you feel strongly about this difference, please send a PR to React Native, adding JavaScript logic to "onPress" functions, making them aware of disabled state in JS logic as well (it's handled on native side for at least iOS, which is the default platform that tests are running in). - -As a mitigation, you'll likely need to modify the logic of "touchable" components to bail if they're pressed in disabled state. - ## No [NativeTestInstance](https://www.native-testing-library.com/docs/api-test-instance) abstraction We don't provide any abstraction over `ReactTestInstance` returned by queries, but allow to use it directly to access queried component's `props` or `type` for that example.