1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  *
    
  7.  * @emails react-core
    
  8.  * @jest-environment node
    
  9.  */
    
  10. 
    
  11. 'use strict';
    
  12. 
    
  13. let PropTypes;
    
  14. let RCTEventEmitter;
    
  15. let React;
    
  16. let ReactNative;
    
  17. let ResponderEventPlugin;
    
  18. let UIManager;
    
  19. let createReactNativeComponentClass;
    
  20. 
    
  21. // Parallels requireNativeComponent() in that it lazily constructs a view config,
    
  22. // And registers view manager event types with ReactNativeViewConfigRegistry.
    
  23. const fakeRequireNativeComponent = (uiViewClassName, validAttributes) => {
    
  24.   const getViewConfig = () => {
    
  25.     const viewConfig = {
    
  26.       uiViewClassName,
    
  27.       validAttributes,
    
  28.       bubblingEventTypes: {
    
  29.         topTouchCancel: {
    
  30.           phasedRegistrationNames: {
    
  31.             bubbled: 'onTouchCancel',
    
  32.             captured: 'onTouchCancelCapture',
    
  33.           },
    
  34.         },
    
  35.         topTouchEnd: {
    
  36.           phasedRegistrationNames: {
    
  37.             bubbled: 'onTouchEnd',
    
  38.             captured: 'onTouchEndCapture',
    
  39.           },
    
  40.         },
    
  41.         topTouchMove: {
    
  42.           phasedRegistrationNames: {
    
  43.             bubbled: 'onTouchMove',
    
  44.             captured: 'onTouchMoveCapture',
    
  45.           },
    
  46.         },
    
  47.         topTouchStart: {
    
  48.           phasedRegistrationNames: {
    
  49.             bubbled: 'onTouchStart',
    
  50.             captured: 'onTouchStartCapture',
    
  51.           },
    
  52.         },
    
  53.       },
    
  54.       directEventTypes: {},
    
  55.     };
    
  56. 
    
  57.     return viewConfig;
    
  58.   };
    
  59. 
    
  60.   return createReactNativeComponentClass(uiViewClassName, getViewConfig);
    
  61. };
    
  62. 
    
  63. beforeEach(() => {
    
  64.   jest.resetModules();
    
  65. 
    
  66.   PropTypes = require('prop-types');
    
  67.   RCTEventEmitter =
    
  68.     require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface').RCTEventEmitter;
    
  69.   React = require('react');
    
  70.   ReactNative = require('react-native-renderer');
    
  71.   ResponderEventPlugin =
    
  72.     require('react-native-renderer/src/legacy-events/ResponderEventPlugin').default;
    
  73.   UIManager =
    
  74.     require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface').UIManager;
    
  75.   createReactNativeComponentClass =
    
  76.     require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface')
    
  77.       .ReactNativeViewConfigRegistry.register;
    
  78. });
    
  79. 
    
  80. it('fails to register the same event name with different types', () => {
    
  81.   const InvalidEvents = createReactNativeComponentClass('InvalidEvents', () => {
    
  82.     if (!__DEV__) {
    
  83.       // Simulate a registration error in prod.
    
  84.       throw new Error('Event cannot be both direct and bubbling: topChange');
    
  85.     }
    
  86. 
    
  87.     // This view config has the same bubbling and direct event name
    
  88.     // which will fail to register in development.
    
  89.     return {
    
  90.       uiViewClassName: 'InvalidEvents',
    
  91.       validAttributes: {
    
  92.         onChange: true,
    
  93.       },
    
  94.       bubblingEventTypes: {
    
  95.         topChange: {
    
  96.           phasedRegistrationNames: {
    
  97.             bubbled: 'onChange',
    
  98.             captured: 'onChangeCapture',
    
  99.           },
    
  100.         },
    
  101.       },
    
  102.       directEventTypes: {
    
  103.         topChange: {
    
  104.           registrationName: 'onChange',
    
  105.         },
    
  106.       },
    
  107.     };
    
  108.   });
    
  109. 
    
  110.   // The first time this renders,
    
  111.   // we attempt to register the view config and fail.
    
  112.   expect(() => ReactNative.render(<InvalidEvents />, 1)).toThrow(
    
  113.     'Event cannot be both direct and bubbling: topChange',
    
  114.   );
    
  115. 
    
  116.   // Continue to re-register the config and
    
  117.   // fail so that we don't mask the above failure.
    
  118.   expect(() => ReactNative.render(<InvalidEvents />, 1)).toThrow(
    
  119.     'Event cannot be both direct and bubbling: topChange',
    
  120.   );
    
  121. });
    
  122. 
    
  123. it('fails if unknown/unsupported event types are dispatched', () => {
    
  124.   expect(RCTEventEmitter.register).toHaveBeenCalledTimes(1);
    
  125.   const EventEmitter = RCTEventEmitter.register.mock.calls[0][0];
    
  126.   const View = fakeRequireNativeComponent('View', {});
    
  127. 
    
  128.   ReactNative.render(<View onUnspecifiedEvent={() => {}} />, 1);
    
  129. 
    
  130.   expect(UIManager.__dumpHierarchyForJestTestsOnly()).toMatchSnapshot();
    
  131.   expect(UIManager.createView).toHaveBeenCalledTimes(1);
    
  132. 
    
  133.   const target = UIManager.createView.mock.calls[0][0];
    
  134. 
    
  135.   expect(() => {
    
  136.     EventEmitter.receiveTouches(
    
  137.       'unspecifiedEvent',
    
  138.       [{target, identifier: 17}],
    
  139.       [0],
    
  140.     );
    
  141.   }).toThrow('Unsupported top level event type "unspecifiedEvent" dispatched');
    
  142. });
    
  143. 
    
  144. it('handles events', () => {
    
  145.   expect(RCTEventEmitter.register).toHaveBeenCalledTimes(1);
    
  146.   const EventEmitter = RCTEventEmitter.register.mock.calls[0][0];
    
  147.   const View = fakeRequireNativeComponent('View', {foo: true});
    
  148. 
    
  149.   const log = [];
    
  150.   ReactNative.render(
    
  151.     <View
    
  152.       foo="outer"
    
  153.       onTouchEnd={() => log.push('outer touchend')}
    
  154.       onTouchEndCapture={() => log.push('outer touchend capture')}
    
  155.       onTouchStart={() => log.push('outer touchstart')}
    
  156.       onTouchStartCapture={() => log.push('outer touchstart capture')}>
    
  157.       <View
    
  158.         foo="inner"
    
  159.         onTouchEndCapture={() => log.push('inner touchend capture')}
    
  160.         onTouchEnd={() => log.push('inner touchend')}
    
  161.         onTouchStartCapture={() => log.push('inner touchstart capture')}
    
  162.         onTouchStart={() => log.push('inner touchstart')}
    
  163.       />
    
  164.     </View>,
    
  165.     1,
    
  166.   );
    
  167. 
    
  168.   expect(UIManager.__dumpHierarchyForJestTestsOnly()).toMatchSnapshot();
    
  169.   expect(UIManager.createView).toHaveBeenCalledTimes(2);
    
  170. 
    
  171.   // Don't depend on the order of createView() calls.
    
  172.   // Stack creates views outside-in; fiber creates them inside-out.
    
  173.   const innerTag = UIManager.createView.mock.calls.find(
    
  174.     args => args[3].foo === 'inner',
    
  175.   )[0];
    
  176. 
    
  177.   EventEmitter.receiveTouches(
    
  178.     'topTouchStart',
    
  179.     [{target: innerTag, identifier: 17}],
    
  180.     [0],
    
  181.   );
    
  182.   EventEmitter.receiveTouches(
    
  183.     'topTouchEnd',
    
  184.     [{target: innerTag, identifier: 17}],
    
  185.     [0],
    
  186.   );
    
  187. 
    
  188.   expect(log).toEqual([
    
  189.     'outer touchstart capture',
    
  190.     'inner touchstart capture',
    
  191.     'inner touchstart',
    
  192.     'outer touchstart',
    
  193.     'outer touchend capture',
    
  194.     'inner touchend capture',
    
  195.     'inner touchend',
    
  196.     'outer touchend',
    
  197.   ]);
    
  198. });
    
  199. 
    
  200. // @gate !disableLegacyContext || !__DEV__
    
  201. it('handles events on text nodes', () => {
    
  202.   expect(RCTEventEmitter.register).toHaveBeenCalledTimes(1);
    
  203.   const EventEmitter = RCTEventEmitter.register.mock.calls[0][0];
    
  204.   const Text = fakeRequireNativeComponent('RCTText', {});
    
  205. 
    
  206.   class ContextHack extends React.Component {
    
  207.     static childContextTypes = {isInAParentText: PropTypes.bool};
    
  208.     getChildContext() {
    
  209.       return {isInAParentText: true};
    
  210.     }
    
  211.     render() {
    
  212.       return this.props.children;
    
  213.     }
    
  214.   }
    
  215. 
    
  216.   const log = [];
    
  217.   ReactNative.render(
    
  218.     <ContextHack>
    
  219.       <Text>
    
  220.         <Text
    
  221.           onTouchEnd={() => log.push('string touchend')}
    
  222.           onTouchEndCapture={() => log.push('string touchend capture')}
    
  223.           onTouchStart={() => log.push('string touchstart')}
    
  224.           onTouchStartCapture={() => log.push('string touchstart capture')}>
    
  225.           Text Content
    
  226.         </Text>
    
  227.         <Text
    
  228.           onTouchEnd={() => log.push('number touchend')}
    
  229.           onTouchEndCapture={() => log.push('number touchend capture')}
    
  230.           onTouchStart={() => log.push('number touchstart')}
    
  231.           onTouchStartCapture={() => log.push('number touchstart capture')}>
    
  232.           {123}
    
  233.         </Text>
    
  234.       </Text>
    
  235.     </ContextHack>,
    
  236.     1,
    
  237.   );
    
  238. 
    
  239.   expect(UIManager.createView).toHaveBeenCalledTimes(5);
    
  240. 
    
  241.   // Don't depend on the order of createView() calls.
    
  242.   // Stack creates views outside-in; fiber creates them inside-out.
    
  243.   const innerTagString = UIManager.createView.mock.calls.find(
    
  244.     args => args[3] && args[3].text === 'Text Content',
    
  245.   )[0];
    
  246.   const innerTagNumber = UIManager.createView.mock.calls.find(
    
  247.     args => args[3] && args[3].text === '123',
    
  248.   )[0];
    
  249. 
    
  250.   EventEmitter.receiveTouches(
    
  251.     'topTouchStart',
    
  252.     [{target: innerTagString, identifier: 17}],
    
  253.     [0],
    
  254.   );
    
  255.   EventEmitter.receiveTouches(
    
  256.     'topTouchEnd',
    
  257.     [{target: innerTagString, identifier: 17}],
    
  258.     [0],
    
  259.   );
    
  260. 
    
  261.   EventEmitter.receiveTouches(
    
  262.     'topTouchStart',
    
  263.     [{target: innerTagNumber, identifier: 18}],
    
  264.     [0],
    
  265.   );
    
  266.   EventEmitter.receiveTouches(
    
  267.     'topTouchEnd',
    
  268.     [{target: innerTagNumber, identifier: 18}],
    
  269.     [0],
    
  270.   );
    
  271. 
    
  272.   expect(log).toEqual([
    
  273.     'string touchstart capture',
    
  274.     'string touchstart',
    
  275.     'string touchend capture',
    
  276.     'string touchend',
    
  277.     'number touchstart capture',
    
  278.     'number touchstart',
    
  279.     'number touchend capture',
    
  280.     'number touchend',
    
  281.   ]);
    
  282. });
    
  283. 
    
  284. it('handles when a responder is unmounted while a touch sequence is in progress', () => {
    
  285.   const EventEmitter = RCTEventEmitter.register.mock.calls[0][0];
    
  286.   const View = fakeRequireNativeComponent('View', {id: true});
    
  287. 
    
  288.   function getViewById(id) {
    
  289.     return UIManager.createView.mock.calls.find(
    
  290.       args => args[3] && args[3].id === id,
    
  291.     )[0];
    
  292.   }
    
  293. 
    
  294.   function getResponderId() {
    
  295.     const responder = ResponderEventPlugin._getResponder();
    
  296.     if (responder === null) {
    
  297.       return null;
    
  298.     }
    
  299.     const props = responder.memoizedProps;
    
  300.     return props ? props.id : null;
    
  301.   }
    
  302. 
    
  303.   const log = [];
    
  304.   ReactNative.render(
    
  305.     <View id="parent">
    
  306.       <View key={1}>
    
  307.         <View
    
  308.           id="one"
    
  309.           onResponderEnd={() => log.push('one responder end')}
    
  310.           onResponderStart={() => log.push('one responder start')}
    
  311.           onStartShouldSetResponder={() => true}
    
  312.         />
    
  313.       </View>
    
  314.       <View key={2}>
    
  315.         <View
    
  316.           id="two"
    
  317.           onResponderEnd={() => log.push('two responder end')}
    
  318.           onResponderStart={() => log.push('two responder start')}
    
  319.           onStartShouldSetResponder={() => true}
    
  320.         />
    
  321.       </View>
    
  322.     </View>,
    
  323.     1,
    
  324.   );
    
  325. 
    
  326.   EventEmitter.receiveTouches(
    
  327.     'topTouchStart',
    
  328.     [{target: getViewById('one'), identifier: 17}],
    
  329.     [0],
    
  330.   );
    
  331. 
    
  332.   expect(getResponderId()).toBe('one');
    
  333.   expect(log).toEqual(['one responder start']);
    
  334.   log.splice(0);
    
  335. 
    
  336.   ReactNative.render(
    
  337.     <View id="parent">
    
  338.       <View key={2}>
    
  339.         <View
    
  340.           id="two"
    
  341.           onResponderEnd={() => log.push('two responder end')}
    
  342.           onResponderStart={() => log.push('two responder start')}
    
  343.           onStartShouldSetResponder={() => true}
    
  344.         />
    
  345.       </View>
    
  346.     </View>,
    
  347.     1,
    
  348.   );
    
  349. 
    
  350.   // TODO Verify the onResponderEnd listener has been called (before the unmount)
    
  351.   // expect(log).toEqual(['one responder end']);
    
  352.   // log.splice(0);
    
  353. 
    
  354.   EventEmitter.receiveTouches(
    
  355.     'topTouchEnd',
    
  356.     [{target: getViewById('two'), identifier: 17}],
    
  357.     [0],
    
  358.   );
    
  359. 
    
  360.   expect(getResponderId()).toBeNull();
    
  361.   expect(log).toEqual([]);
    
  362. 
    
  363.   EventEmitter.receiveTouches(
    
  364.     'topTouchStart',
    
  365.     [{target: getViewById('two'), identifier: 17}],
    
  366.     [0],
    
  367.   );
    
  368. 
    
  369.   expect(getResponderId()).toBe('two');
    
  370.   expect(log).toEqual(['two responder start']);
    
  371. });
    
  372. 
    
  373. it('handles events without target', () => {
    
  374.   const EventEmitter = RCTEventEmitter.register.mock.calls[0][0];
    
  375. 
    
  376.   const View = fakeRequireNativeComponent('View', {id: true});
    
  377. 
    
  378.   function getViewById(id) {
    
  379.     return UIManager.createView.mock.calls.find(
    
  380.       args => args[3] && args[3].id === id,
    
  381.     )[0];
    
  382.   }
    
  383. 
    
  384.   function getResponderId() {
    
  385.     const responder = ResponderEventPlugin._getResponder();
    
  386.     if (responder === null) {
    
  387.       return null;
    
  388.     }
    
  389.     const props = responder.memoizedProps;
    
  390.     return props ? props.id : null;
    
  391.   }
    
  392. 
    
  393.   const log = [];
    
  394. 
    
  395.   function render(renderFirstComponent) {
    
  396.     ReactNative.render(
    
  397.       <View id="parent">
    
  398.         <View key={1}>
    
  399.           {renderFirstComponent ? (
    
  400.             <View
    
  401.               id="one"
    
  402.               onResponderEnd={() => log.push('one responder end')}
    
  403.               onResponderStart={() => log.push('one responder start')}
    
  404.               onStartShouldSetResponder={() => true}
    
  405.             />
    
  406.           ) : null}
    
  407.         </View>
    
  408.         <View key={2}>
    
  409.           <View
    
  410.             id="two"
    
  411.             onResponderEnd={() => log.push('two responder end')}
    
  412.             onResponderStart={() => log.push('two responder start')}
    
  413.             onStartShouldSetResponder={() => true}
    
  414.           />
    
  415.         </View>
    
  416.       </View>,
    
  417.       1,
    
  418.     );
    
  419.   }
    
  420. 
    
  421.   render(true);
    
  422. 
    
  423.   EventEmitter.receiveTouches(
    
  424.     'topTouchStart',
    
  425.     [{target: getViewById('one'), identifier: 17}],
    
  426.     [0],
    
  427.   );
    
  428. 
    
  429.   // Unmounting component 'one'.
    
  430.   render(false);
    
  431. 
    
  432.   EventEmitter.receiveTouches(
    
  433.     'topTouchEnd',
    
  434.     [{target: getViewById('one'), identifier: 17}],
    
  435.     [0],
    
  436.   );
    
  437. 
    
  438.   expect(getResponderId()).toBe(null);
    
  439. 
    
  440.   EventEmitter.receiveTouches(
    
  441.     'topTouchStart',
    
  442.     [{target: getViewById('two'), identifier: 18}],
    
  443.     [0],
    
  444.   );
    
  445. 
    
  446.   expect(getResponderId()).toBe('two');
    
  447. 
    
  448.   EventEmitter.receiveTouches(
    
  449.     'topTouchEnd',
    
  450.     [{target: getViewById('two'), identifier: 18}],
    
  451.     [0],
    
  452.   );
    
  453. 
    
  454.   expect(getResponderId()).toBe(null);
    
  455. 
    
  456.   expect(log).toEqual([
    
  457.     'one responder start',
    
  458.     'two responder start',
    
  459.     'two responder end',
    
  460.   ]);
    
  461. });
    
  462. 
    
  463. it('dispatches event with target as instance', () => {
    
  464.   const EventEmitter = RCTEventEmitter.register.mock.calls[0][0];
    
  465. 
    
  466.   const View = fakeRequireNativeComponent('View', {id: true});
    
  467. 
    
  468.   function getViewById(id) {
    
  469.     return UIManager.createView.mock.calls.find(
    
  470.       args => args[3] && args[3].id === id,
    
  471.     )[0];
    
  472.   }
    
  473. 
    
  474.   const ref1 = React.createRef();
    
  475.   const ref2 = React.createRef();
    
  476. 
    
  477.   ReactNative.render(
    
  478.     <View id="parent">
    
  479.       <View
    
  480.         ref={ref1}
    
  481.         id="one"
    
  482.         onResponderStart={event => {
    
  483.           expect(ref1.current).not.toBeNull();
    
  484.           // Check for referential equality
    
  485.           expect(ref1.current).toBe(event.target);
    
  486.           expect(ref1.current).toBe(event.currentTarget);
    
  487.         }}
    
  488.         onStartShouldSetResponder={() => true}
    
  489.       />
    
  490.       <View
    
  491.         ref={ref2}
    
  492.         id="two"
    
  493.         onResponderStart={event => {
    
  494.           expect(ref2.current).not.toBeNull();
    
  495.           // Check for referential equality
    
  496.           expect(ref2.current).toBe(event.target);
    
  497.           expect(ref2.current).toBe(event.currentTarget);
    
  498.         }}
    
  499.         onStartShouldSetResponder={() => true}
    
  500.       />
    
  501.     </View>,
    
  502.     1,
    
  503.   );
    
  504. 
    
  505.   EventEmitter.receiveTouches(
    
  506.     'topTouchStart',
    
  507.     [{target: getViewById('one'), identifier: 17}],
    
  508.     [0],
    
  509.   );
    
  510. 
    
  511.   EventEmitter.receiveTouches(
    
  512.     'topTouchEnd',
    
  513.     [{target: getViewById('one'), identifier: 17}],
    
  514.     [0],
    
  515.   );
    
  516. 
    
  517.   EventEmitter.receiveTouches(
    
  518.     'topTouchStart',
    
  519.     [{target: getViewById('two'), identifier: 18}],
    
  520.     [0],
    
  521.   );
    
  522. 
    
  523.   EventEmitter.receiveTouches(
    
  524.     'topTouchEnd',
    
  525.     [{target: getViewById('two'), identifier: 18}],
    
  526.     [0],
    
  527.   );
    
  528. 
    
  529.   expect.assertions(6);
    
  530. });