ReactNativeTestTools.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. /**
  2. * Copyright (c) Facebook, Inc. and its 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. * @flow
  8. * @format
  9. */
  10. /* eslint-env jest */
  11. 'use strict';
  12. const React = require('react');
  13. const ReactTestRenderer = require('react-test-renderer');
  14. const ShallowRenderer = require('react-test-renderer/shallow');
  15. /* $FlowFixMe(>=0.122.0 site=react_native_fb) This comment suppresses an error
  16. * found when Flow v0.122.0 was deployed. To see the error, delete this comment
  17. * and run Flow. */
  18. const shallowRenderer = new ShallowRenderer();
  19. import type {ReactTestRenderer as ReactTestRendererType} from 'react-test-renderer';
  20. export type ReactTestInstance = $PropertyType<ReactTestRendererType, 'root'>;
  21. export type Predicate = (node: ReactTestInstance) => boolean;
  22. type $ReturnType<Fn> = $Call<<Ret, A>((...A) => Ret) => Ret, Fn>;
  23. /* $FlowFixMe(>=0.122.0 site=react_native_fb) This comment suppresses an error
  24. * found when Flow v0.122.0 was deployed. To see the error, delete this comment
  25. * and run Flow. */
  26. export type ReactTestRendererJSON = $ReturnType<ReactTestRenderer.create.toJSON>;
  27. const {
  28. Switch,
  29. Text,
  30. TextInput,
  31. View,
  32. VirtualizedList,
  33. } = require('react-native');
  34. function byClickable(): Predicate {
  35. return withMessage(
  36. node =>
  37. // note: <Text /> lazy-mounts press handlers after the first press,
  38. // so this is a workaround for targeting text nodes.
  39. (node.type === Text &&
  40. node.props &&
  41. typeof node.props.onPress === 'function') ||
  42. // note: Special casing <Switch /> since it doesn't use touchable
  43. (node.type === Switch && node.props && node.props.disabled !== true) ||
  44. (node.type === View &&
  45. node?.props?.onStartShouldSetResponder?.testOnly_pressabilityConfig) ||
  46. // HACK: Find components that use `Pressability`.
  47. node.instance?.state?.pressability != null ||
  48. // TODO: Remove this after deleting `Touchable`.
  49. /* $FlowFixMe(>=0.122.0 site=react_native_fb) This comment suppresses an
  50. * error found when Flow v0.122.0 was deployed. To see the error, delete
  51. * this comment and run Flow. */
  52. (node.instance &&
  53. /* $FlowFixMe(>=0.122.0 site=react_native_fb) This comment suppresses
  54. * an error found when Flow v0.122.0 was deployed. To see the error,
  55. * delete this comment and run Flow. */
  56. typeof node.instance.touchableHandlePress === 'function'),
  57. 'is clickable',
  58. );
  59. }
  60. function byTestID(testID: string): Predicate {
  61. return withMessage(
  62. node => node.props && node.props.testID === testID,
  63. `testID prop equals ${testID}`,
  64. );
  65. }
  66. function byTextMatching(regex: RegExp): Predicate {
  67. return withMessage(
  68. /* $FlowFixMe(>=0.122.0 site=react_native_fb) This comment suppresses an
  69. * error found when Flow v0.122.0 was deployed. To see the error, delete
  70. * this comment and run Flow. */
  71. node => node.props && regex.exec(node.props.children),
  72. `text content matches ${regex.toString()}`,
  73. );
  74. }
  75. function enter(instance: ReactTestInstance, text: string) {
  76. const input = instance.findByType(TextInput);
  77. input.props.onChange && input.props.onChange({nativeEvent: {text}});
  78. input.props.onChangeText && input.props.onChangeText(text);
  79. }
  80. // Returns null if there is no error, otherwise returns an error message string.
  81. function maximumDepthError(
  82. tree: ReactTestRendererType,
  83. maxDepthLimit: number,
  84. ): ?string {
  85. const maxDepth = maximumDepthOfJSON(tree.toJSON());
  86. if (maxDepth > maxDepthLimit) {
  87. return (
  88. `maximumDepth of ${maxDepth} exceeded limit of ${maxDepthLimit} - this is a proxy ` +
  89. 'metric to protect against stack overflow errors:\n\n' +
  90. 'https://fburl.com/rn-view-stack-overflow.\n\n' +
  91. 'To fix, you need to remove native layers from your hierarchy, such as unnecessary View ' +
  92. 'wrappers.'
  93. );
  94. } else {
  95. return null;
  96. }
  97. }
  98. function expectNoConsoleWarn() {
  99. (jest: $FlowFixMe).spyOn(console, 'warn').mockImplementation((...args) => {
  100. expect(args).toBeFalsy();
  101. });
  102. }
  103. function expectNoConsoleError() {
  104. let hasNotFailed = true;
  105. (jest: $FlowFixMe).spyOn(console, 'error').mockImplementation((...args) => {
  106. if (hasNotFailed) {
  107. hasNotFailed = false; // set false to prevent infinite recursion
  108. expect(args).toBeFalsy();
  109. }
  110. });
  111. }
  112. function expectRendersMatchingSnapshot(
  113. name: string,
  114. ComponentProvider: () => React.Element<any>,
  115. unmockComponent: () => mixed,
  116. ) {
  117. let instance;
  118. jest.resetAllMocks();
  119. instance = ReactTestRenderer.create(<ComponentProvider />);
  120. expect(instance).toMatchSnapshot(
  121. 'should deep render when mocked (please verify output manually)',
  122. );
  123. jest.resetAllMocks();
  124. unmockComponent();
  125. instance = shallowRenderer.render(<ComponentProvider />);
  126. expect(instance).toMatchSnapshot(
  127. `should shallow render as <${name} /> when not mocked`,
  128. );
  129. jest.resetAllMocks();
  130. instance = shallowRenderer.render(<ComponentProvider />);
  131. expect(instance).toMatchSnapshot(
  132. `should shallow render as <${name} /> when mocked`,
  133. );
  134. jest.resetAllMocks();
  135. unmockComponent();
  136. instance = ReactTestRenderer.create(<ComponentProvider />);
  137. expect(instance).toMatchSnapshot(
  138. 'should deep render when not mocked (please verify output manually)',
  139. );
  140. }
  141. // Takes a node from toJSON()
  142. function maximumDepthOfJSON(node: ?ReactTestRendererJSON): number {
  143. if (node == null) {
  144. return 0;
  145. } else if (typeof node === 'string' || node.children == null) {
  146. return 1;
  147. } else {
  148. let maxDepth = 0;
  149. node.children.forEach(child => {
  150. maxDepth = Math.max(maximumDepthOfJSON(child) + 1, maxDepth);
  151. });
  152. return maxDepth;
  153. }
  154. }
  155. function renderAndEnforceStrictMode(element: React.Node): any {
  156. expectNoConsoleError();
  157. return renderWithStrictMode(element);
  158. }
  159. function renderWithStrictMode(element: React.Node): ReactTestRendererType {
  160. const WorkAroundBugWithStrictModeInTestRenderer = prps => prps.children;
  161. const StrictMode = (React: $FlowFixMe).StrictMode;
  162. return ReactTestRenderer.create(
  163. <WorkAroundBugWithStrictModeInTestRenderer>
  164. <StrictMode>{element}</StrictMode>
  165. </WorkAroundBugWithStrictModeInTestRenderer>,
  166. );
  167. }
  168. function tap(instance: ReactTestInstance) {
  169. const touchable = instance.find(byClickable());
  170. if (touchable.type === Text && touchable.props && touchable.props.onPress) {
  171. touchable.props.onPress();
  172. } else if (touchable.type === Switch && touchable.props) {
  173. const value = !touchable.props.value;
  174. const {onChange, onValueChange} = touchable.props;
  175. onChange && onChange({nativeEvent: {value}});
  176. onValueChange && onValueChange(value);
  177. } else if (
  178. touchable?.props?.onStartShouldSetResponder?.testOnly_pressabilityConfig
  179. ) {
  180. const {
  181. onPress,
  182. disabled,
  183. } = touchable.props.onStartShouldSetResponder.testOnly_pressabilityConfig();
  184. if (!disabled) {
  185. onPress({nativeEvent: {}});
  186. }
  187. } else {
  188. // Only tap when props.disabled isn't set (or there aren't any props)
  189. if (!touchable.props || !touchable.props.disabled) {
  190. touchable.props.onPress({nativeEvent: {}});
  191. }
  192. }
  193. }
  194. function scrollToBottom(instance: ReactTestInstance) {
  195. const list = instance.findByType(VirtualizedList);
  196. list.props && list.props.onEndReached();
  197. }
  198. // To make error messages a little bit better, we attach a custom toString
  199. // implementation to a predicate
  200. function withMessage(fn: Predicate, message: string): Predicate {
  201. (fn: any).toString = () => message;
  202. return fn;
  203. }
  204. export {byClickable};
  205. export {byTestID};
  206. export {byTextMatching};
  207. export {enter};
  208. export {expectNoConsoleWarn};
  209. export {expectNoConsoleError};
  210. export {expectRendersMatchingSnapshot};
  211. export {maximumDepthError};
  212. export {maximumDepthOfJSON};
  213. export {renderAndEnforceStrictMode};
  214. export {renderWithStrictMode};
  215. export {scrollToBottom};
  216. export {tap};
  217. export {withMessage};