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.  * @flow
    
  8.  */
    
  9. 
    
  10. import type {Fiber, FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
    
  11. import type {
    
  12.   PublicInstance,
    
  13.   Instance,
    
  14.   TextInstance,
    
  15. } from './ReactFiberConfigTestHost';
    
  16. 
    
  17. import * as React from 'react';
    
  18. import * as Scheduler from 'scheduler/unstable_mock';
    
  19. import {
    
  20.   getPublicRootInstance,
    
  21.   createContainer,
    
  22.   updateContainer,
    
  23.   flushSync,
    
  24.   injectIntoDevTools,
    
  25.   batchedUpdates,
    
  26. } from 'react-reconciler/src/ReactFiberReconciler';
    
  27. import {findCurrentFiberUsingSlowPath} from 'react-reconciler/src/ReactFiberTreeReflection';
    
  28. import {
    
  29.   Fragment,
    
  30.   FunctionComponent,
    
  31.   ClassComponent,
    
  32.   HostComponent,
    
  33.   HostHoistable,
    
  34.   HostSingleton,
    
  35.   HostPortal,
    
  36.   HostText,
    
  37.   HostRoot,
    
  38.   ContextConsumer,
    
  39.   ContextProvider,
    
  40.   Mode,
    
  41.   ForwardRef,
    
  42.   Profiler,
    
  43.   MemoComponent,
    
  44.   SimpleMemoComponent,
    
  45.   IncompleteClassComponent,
    
  46.   ScopeComponent,
    
  47. } from 'react-reconciler/src/ReactWorkTags';
    
  48. import isArray from 'shared/isArray';
    
  49. import getComponentNameFromType from 'shared/getComponentNameFromType';
    
  50. import ReactVersion from 'shared/ReactVersion';
    
  51. import {checkPropStringCoercion} from 'shared/CheckStringCoercion';
    
  52. 
    
  53. import {getPublicInstance} from './ReactFiberConfigTestHost';
    
  54. import {ConcurrentRoot, LegacyRoot} from 'react-reconciler/src/ReactRootTags';
    
  55. import {allowConcurrentByDefault} from 'shared/ReactFeatureFlags';
    
  56. 
    
  57. const act = React.unstable_act;
    
  58. 
    
  59. // TODO: Remove from public bundle
    
  60. 
    
  61. type TestRendererOptions = {
    
  62.   createNodeMock: (element: React$Element<any>) => any,
    
  63.   unstable_isConcurrent: boolean,
    
  64.   unstable_strictMode: boolean,
    
  65.   unstable_concurrentUpdatesByDefault: boolean,
    
  66.   ...
    
  67. };
    
  68. 
    
  69. type ReactTestRendererJSON = {
    
  70.   type: string,
    
  71.   props: {[propName: string]: any, ...},
    
  72.   children: null | Array<ReactTestRendererNode>,
    
  73.   $$typeof?: symbol, // Optional because we add it with defineProperty().
    
  74. };
    
  75. type ReactTestRendererNode = ReactTestRendererJSON | string;
    
  76. 
    
  77. type FindOptions = {
    
  78.   // performs a "greedy" search: if a matching node is found, will continue
    
  79.   // to search within the matching node's children. (default: true)
    
  80.   deep?: boolean,
    
  81. };
    
  82. 
    
  83. export type Predicate = (node: ReactTestInstance) => ?boolean;
    
  84. 
    
  85. const defaultTestOptions = {
    
  86.   createNodeMock: function () {
    
  87.     return null;
    
  88.   },
    
  89. };
    
  90. 
    
  91. function toJSON(inst: Instance | TextInstance): ReactTestRendererNode | null {
    
  92.   if (inst.isHidden) {
    
  93.     // Omit timed out children from output entirely. This seems like the least
    
  94.     // surprising behavior. We could perhaps add a separate API that includes
    
  95.     // them, if it turns out people need it.
    
  96.     return null;
    
  97.   }
    
  98.   switch (inst.tag) {
    
  99.     case 'TEXT':
    
  100.       return inst.text;
    
  101.     case 'INSTANCE': {
    
  102.       /* eslint-disable no-unused-vars */
    
  103.       // We don't include the `children` prop in JSON.
    
  104.       // Instead, we will include the actual rendered children.
    
  105.       const {children, ...props} = inst.props;
    
  106.       /* eslint-enable */
    
  107.       let renderedChildren = null;
    
  108.       if (inst.children && inst.children.length) {
    
  109.         for (let i = 0; i < inst.children.length; i++) {
    
  110.           const renderedChild = toJSON(inst.children[i]);
    
  111.           if (renderedChild !== null) {
    
  112.             if (renderedChildren === null) {
    
  113.               renderedChildren = [renderedChild];
    
  114.             } else {
    
  115.               renderedChildren.push(renderedChild);
    
  116.             }
    
  117.           }
    
  118.         }
    
  119.       }
    
  120.       const json: ReactTestRendererJSON = {
    
  121.         type: inst.type,
    
  122.         props: props,
    
  123.         children: renderedChildren,
    
  124.       };
    
  125.       Object.defineProperty(json, '$$typeof', {
    
  126.         value: Symbol.for('react.test.json'),
    
  127.       });
    
  128.       return json;
    
  129.     }
    
  130.     default:
    
  131.       throw new Error(`Unexpected node type in toJSON: ${inst.tag}`);
    
  132.   }
    
  133. }
    
  134. 
    
  135. function childrenToTree(node: null | Fiber) {
    
  136.   if (!node) {
    
  137.     return null;
    
  138.   }
    
  139.   const children = nodeAndSiblingsArray(node);
    
  140.   if (children.length === 0) {
    
  141.     return null;
    
  142.   } else if (children.length === 1) {
    
  143.     return toTree(children[0]);
    
  144.   }
    
  145.   return flatten(children.map(toTree));
    
  146. }
    
  147. 
    
  148. // $FlowFixMe[missing-local-annot]
    
  149. function nodeAndSiblingsArray(nodeWithSibling) {
    
  150.   const array = [];
    
  151.   let node = nodeWithSibling;
    
  152.   while (node != null) {
    
  153.     array.push(node);
    
  154.     node = node.sibling;
    
  155.   }
    
  156.   return array;
    
  157. }
    
  158. 
    
  159. // $FlowFixMe[missing-local-annot]
    
  160. function flatten(arr) {
    
  161.   const result = [];
    
  162.   const stack = [{i: 0, array: arr}];
    
  163.   while (stack.length) {
    
  164.     const n = stack.pop();
    
  165.     while (n.i < n.array.length) {
    
  166.       const el = n.array[n.i];
    
  167.       n.i += 1;
    
  168.       if (isArray(el)) {
    
  169.         stack.push(n);
    
  170.         stack.push({i: 0, array: el});
    
  171.         break;
    
  172.       }
    
  173.       result.push(el);
    
  174.     }
    
  175.   }
    
  176.   return result;
    
  177. }
    
  178. 
    
  179. function toTree(node: null | Fiber): $FlowFixMe {
    
  180.   if (node == null) {
    
  181.     return null;
    
  182.   }
    
  183.   switch (node.tag) {
    
  184.     case HostRoot:
    
  185.       return childrenToTree(node.child);
    
  186.     case HostPortal:
    
  187.       return childrenToTree(node.child);
    
  188.     case ClassComponent:
    
  189.       return {
    
  190.         nodeType: 'component',
    
  191.         type: node.type,
    
  192.         props: {...node.memoizedProps},
    
  193.         instance: node.stateNode,
    
  194.         rendered: childrenToTree(node.child),
    
  195.       };
    
  196.     case FunctionComponent:
    
  197.     case SimpleMemoComponent:
    
  198.       return {
    
  199.         nodeType: 'component',
    
  200.         type: node.type,
    
  201.         props: {...node.memoizedProps},
    
  202.         instance: null,
    
  203.         rendered: childrenToTree(node.child),
    
  204.       };
    
  205.     case HostHoistable:
    
  206.     case HostSingleton:
    
  207.     case HostComponent: {
    
  208.       return {
    
  209.         nodeType: 'host',
    
  210.         type: node.type,
    
  211.         props: {...node.memoizedProps},
    
  212.         instance: null, // TODO: use createNodeMock here somehow?
    
  213.         rendered: flatten(nodeAndSiblingsArray(node.child).map(toTree)),
    
  214.       };
    
  215.     }
    
  216.     case HostText:
    
  217.       return node.stateNode.text;
    
  218.     case Fragment:
    
  219.     case ContextProvider:
    
  220.     case ContextConsumer:
    
  221.     case Mode:
    
  222.     case Profiler:
    
  223.     case ForwardRef:
    
  224.     case MemoComponent:
    
  225.     case IncompleteClassComponent:
    
  226.     case ScopeComponent:
    
  227.       return childrenToTree(node.child);
    
  228.     default:
    
  229.       throw new Error(
    
  230.         `toTree() does not yet know how to handle nodes with tag=${node.tag}`,
    
  231.       );
    
  232.   }
    
  233. }
    
  234. 
    
  235. const validWrapperTypes = new Set([
    
  236.   FunctionComponent,
    
  237.   ClassComponent,
    
  238.   HostComponent,
    
  239.   ForwardRef,
    
  240.   MemoComponent,
    
  241.   SimpleMemoComponent,
    
  242.   // Normally skipped, but used when there's more than one root child.
    
  243.   HostRoot,
    
  244. ]);
    
  245. 
    
  246. function getChildren(parent: Fiber) {
    
  247.   const children = [];
    
  248.   const startingNode = parent;
    
  249.   let node: Fiber = startingNode;
    
  250.   if (node.child === null) {
    
  251.     return children;
    
  252.   }
    
  253.   node.child.return = node;
    
  254.   node = node.child;
    
  255.   outer: while (true) {
    
  256.     let descend = false;
    
  257.     if (validWrapperTypes.has(node.tag)) {
    
  258.       children.push(wrapFiber(node));
    
  259.     } else if (node.tag === HostText) {
    
  260.       if (__DEV__) {
    
  261.         checkPropStringCoercion(node.memoizedProps, 'memoizedProps');
    
  262.       }
    
  263.       children.push('' + node.memoizedProps);
    
  264.     } else {
    
  265.       descend = true;
    
  266.     }
    
  267.     if (descend && node.child !== null) {
    
  268.       node.child.return = node;
    
  269.       node = node.child;
    
  270.       continue;
    
  271.     }
    
  272.     while (node.sibling === null) {
    
  273.       if (node.return === startingNode) {
    
  274.         break outer;
    
  275.       }
    
  276.       node = (node.return: any);
    
  277.     }
    
  278.     (node.sibling: any).return = node.return;
    
  279.     node = (node.sibling: any);
    
  280.   }
    
  281.   return children;
    
  282. }
    
  283. 
    
  284. class ReactTestInstance {
    
  285.   _fiber: Fiber;
    
  286. 
    
  287.   _currentFiber(): Fiber {
    
  288.     // Throws if this component has been unmounted.
    
  289.     const fiber = findCurrentFiberUsingSlowPath(this._fiber);
    
  290. 
    
  291.     if (fiber === null) {
    
  292.       throw new Error(
    
  293.         "Can't read from currently-mounting component. This error is likely " +
    
  294.           'caused by a bug in React. Please file an issue.',
    
  295.       );
    
  296.     }
    
  297. 
    
  298.     return fiber;
    
  299.   }
    
  300. 
    
  301.   constructor(fiber: Fiber) {
    
  302.     if (!validWrapperTypes.has(fiber.tag)) {
    
  303.       throw new Error(
    
  304.         `Unexpected object passed to ReactTestInstance constructor (tag: ${fiber.tag}). ` +
    
  305.           'This is probably a bug in React.',
    
  306.       );
    
  307.     }
    
  308. 
    
  309.     this._fiber = fiber;
    
  310.   }
    
  311. 
    
  312.   get instance(): $FlowFixMe {
    
  313.     const tag = this._fiber.tag;
    
  314.     if (
    
  315.       tag === HostComponent ||
    
  316.       tag === HostHoistable ||
    
  317.       tag === HostSingleton
    
  318.     ) {
    
  319.       return getPublicInstance(this._fiber.stateNode);
    
  320.     } else {
    
  321.       return this._fiber.stateNode;
    
  322.     }
    
  323.   }
    
  324. 
    
  325.   get type(): any {
    
  326.     return this._fiber.type;
    
  327.   }
    
  328. 
    
  329.   get props(): Object {
    
  330.     return this._currentFiber().memoizedProps;
    
  331.   }
    
  332. 
    
  333.   get parent(): ?ReactTestInstance {
    
  334.     let parent = this._fiber.return;
    
  335.     while (parent !== null) {
    
  336.       if (validWrapperTypes.has(parent.tag)) {
    
  337.         if (parent.tag === HostRoot) {
    
  338.           // Special case: we only "materialize" instances for roots
    
  339.           // if they have more than a single child. So we'll check that now.
    
  340.           if (getChildren(parent).length < 2) {
    
  341.             return null;
    
  342.           }
    
  343.         }
    
  344.         return wrapFiber(parent);
    
  345.       }
    
  346.       parent = parent.return;
    
  347.     }
    
  348.     return null;
    
  349.   }
    
  350. 
    
  351.   get children(): Array<ReactTestInstance | string> {
    
  352.     return getChildren(this._currentFiber());
    
  353.   }
    
  354. 
    
  355.   // Custom search functions
    
  356.   find(predicate: Predicate): ReactTestInstance {
    
  357.     return expectOne(
    
  358.       this.findAll(predicate, {deep: false}),
    
  359.       `matching custom predicate: ${predicate.toString()}`,
    
  360.     );
    
  361.   }
    
  362. 
    
  363.   findByType(type: any): ReactTestInstance {
    
  364.     return expectOne(
    
  365.       this.findAllByType(type, {deep: false}),
    
  366.       `with node type: "${getComponentNameFromType(type) || 'Unknown'}"`,
    
  367.     );
    
  368.   }
    
  369. 
    
  370.   findByProps(props: Object): ReactTestInstance {
    
  371.     return expectOne(
    
  372.       this.findAllByProps(props, {deep: false}),
    
  373.       `with props: ${JSON.stringify(props)}`,
    
  374.     );
    
  375.   }
    
  376. 
    
  377.   findAll(
    
  378.     predicate: Predicate,
    
  379.     options: ?FindOptions = null,
    
  380.   ): Array<ReactTestInstance> {
    
  381.     return findAll(this, predicate, options);
    
  382.   }
    
  383. 
    
  384.   findAllByType(
    
  385.     type: any,
    
  386.     options: ?FindOptions = null,
    
  387.   ): Array<ReactTestInstance> {
    
  388.     return findAll(this, node => node.type === type, options);
    
  389.   }
    
  390. 
    
  391.   findAllByProps(
    
  392.     props: Object,
    
  393.     options: ?FindOptions = null,
    
  394.   ): Array<ReactTestInstance> {
    
  395.     return findAll(
    
  396.       this,
    
  397.       node => node.props && propsMatch(node.props, props),
    
  398.       options,
    
  399.     );
    
  400.   }
    
  401. }
    
  402. 
    
  403. function findAll(
    
  404.   root: ReactTestInstance,
    
  405.   predicate: Predicate,
    
  406.   options: ?FindOptions,
    
  407. ): Array<ReactTestInstance> {
    
  408.   const deep = options ? options.deep : true;
    
  409.   const results = [];
    
  410. 
    
  411.   if (predicate(root)) {
    
  412.     results.push(root);
    
  413.     if (!deep) {
    
  414.       return results;
    
  415.     }
    
  416.   }
    
  417. 
    
  418.   root.children.forEach(child => {
    
  419.     if (typeof child === 'string') {
    
  420.       return;
    
  421.     }
    
  422.     results.push(...findAll(child, predicate, options));
    
  423.   });
    
  424. 
    
  425.   return results;
    
  426. }
    
  427. 
    
  428. function expectOne(
    
  429.   all: Array<ReactTestInstance>,
    
  430.   message: string,
    
  431. ): ReactTestInstance {
    
  432.   if (all.length === 1) {
    
  433.     return all[0];
    
  434.   }
    
  435. 
    
  436.   const prefix =
    
  437.     all.length === 0
    
  438.       ? 'No instances found '
    
  439.       : `Expected 1 but found ${all.length} instances `;
    
  440. 
    
  441.   throw new Error(prefix + message);
    
  442. }
    
  443. 
    
  444. function propsMatch(props: Object, filter: Object): boolean {
    
  445.   for (const key in filter) {
    
  446.     if (props[key] !== filter[key]) {
    
  447.       return false;
    
  448.     }
    
  449.   }
    
  450.   return true;
    
  451. }
    
  452. 
    
  453. // $FlowFixMe[missing-local-annot]
    
  454. function onRecoverableError(error) {
    
  455.   // TODO: Expose onRecoverableError option to userspace
    
  456.   // eslint-disable-next-line react-internal/no-production-logging, react-internal/warning-args
    
  457.   console.error(error);
    
  458. }
    
  459. 
    
  460. function create(
    
  461.   element: React$Element<any>,
    
  462.   options: TestRendererOptions,
    
  463. ): {
    
  464.   _Scheduler: typeof Scheduler,
    
  465.   root: void,
    
  466.   toJSON(): Array<ReactTestRendererNode> | ReactTestRendererNode | null,
    
  467.   toTree(): mixed,
    
  468.   update(newElement: React$Element<any>): any,
    
  469.   unmount(): void,
    
  470.   getInstance(): React$Component<any, any> | PublicInstance | null,
    
  471.   unstable_flushSync: typeof flushSync,
    
  472. } {
    
  473.   let createNodeMock = defaultTestOptions.createNodeMock;
    
  474.   let isConcurrent = false;
    
  475.   let isStrictMode = false;
    
  476.   let concurrentUpdatesByDefault = null;
    
  477.   if (typeof options === 'object' && options !== null) {
    
  478.     if (typeof options.createNodeMock === 'function') {
    
  479.       // $FlowFixMe[incompatible-type] found when upgrading Flow
    
  480.       createNodeMock = options.createNodeMock;
    
  481.     }
    
  482.     if (options.unstable_isConcurrent === true) {
    
  483.       isConcurrent = true;
    
  484.     }
    
  485.     if (options.unstable_strictMode === true) {
    
  486.       isStrictMode = true;
    
  487.     }
    
  488.     if (allowConcurrentByDefault) {
    
  489.       if (options.unstable_concurrentUpdatesByDefault !== undefined) {
    
  490.         concurrentUpdatesByDefault =
    
  491.           options.unstable_concurrentUpdatesByDefault;
    
  492.       }
    
  493.     }
    
  494.   }
    
  495.   let container = {
    
  496.     children: ([]: Array<Instance | TextInstance>),
    
  497.     createNodeMock,
    
  498.     tag: 'CONTAINER',
    
  499.   };
    
  500.   let root: FiberRoot | null = createContainer(
    
  501.     container,
    
  502.     isConcurrent ? ConcurrentRoot : LegacyRoot,
    
  503.     null,
    
  504.     isStrictMode,
    
  505.     concurrentUpdatesByDefault,
    
  506.     '',
    
  507.     onRecoverableError,
    
  508.     null,
    
  509.   );
    
  510. 
    
  511.   if (root == null) {
    
  512.     throw new Error('something went wrong');
    
  513.   }
    
  514. 
    
  515.   updateContainer(element, root, null, null);
    
  516. 
    
  517.   const entry = {
    
  518.     _Scheduler: Scheduler,
    
  519. 
    
  520.     root: undefined, // makes flow happy
    
  521.     // we define a 'getter' for 'root' below using 'Object.defineProperty'
    
  522.     toJSON(): Array<ReactTestRendererNode> | ReactTestRendererNode | null {
    
  523.       if (root == null || root.current == null || container == null) {
    
  524.         return null;
    
  525.       }
    
  526.       if (container.children.length === 0) {
    
  527.         return null;
    
  528.       }
    
  529.       if (container.children.length === 1) {
    
  530.         return toJSON(container.children[0]);
    
  531.       }
    
  532.       if (
    
  533.         container.children.length === 2 &&
    
  534.         container.children[0].isHidden === true &&
    
  535.         container.children[1].isHidden === false
    
  536.       ) {
    
  537.         // Omit timed out children from output entirely, including the fact that we
    
  538.         // temporarily wrap fallback and timed out children in an array.
    
  539.         return toJSON(container.children[1]);
    
  540.       }
    
  541.       let renderedChildren = null;
    
  542.       if (container.children && container.children.length) {
    
  543.         for (let i = 0; i < container.children.length; i++) {
    
  544.           const renderedChild = toJSON(container.children[i]);
    
  545.           if (renderedChild !== null) {
    
  546.             if (renderedChildren === null) {
    
  547.               renderedChildren = [renderedChild];
    
  548.             } else {
    
  549.               renderedChildren.push(renderedChild);
    
  550.             }
    
  551.           }
    
  552.         }
    
  553.       }
    
  554.       return renderedChildren;
    
  555.     },
    
  556.     toTree() {
    
  557.       if (root == null || root.current == null) {
    
  558.         return null;
    
  559.       }
    
  560.       return toTree(root.current);
    
  561.     },
    
  562.     update(newElement: React$Element<any>): number | void {
    
  563.       if (root == null || root.current == null) {
    
  564.         return;
    
  565.       }
    
  566.       updateContainer(newElement, root, null, null);
    
  567.     },
    
  568.     unmount() {
    
  569.       if (root == null || root.current == null) {
    
  570.         return;
    
  571.       }
    
  572.       updateContainer(null, root, null, null);
    
  573.       // $FlowFixMe[incompatible-type] found when upgrading Flow
    
  574.       container = null;
    
  575.       root = null;
    
  576.     },
    
  577.     getInstance() {
    
  578.       if (root == null || root.current == null) {
    
  579.         return null;
    
  580.       }
    
  581.       return getPublicRootInstance(root);
    
  582.     },
    
  583. 
    
  584.     unstable_flushSync: flushSync,
    
  585.   };
    
  586. 
    
  587.   Object.defineProperty(
    
  588.     entry,
    
  589.     'root',
    
  590.     ({
    
  591.       configurable: true,
    
  592.       enumerable: true,
    
  593.       get: function () {
    
  594.         if (root === null) {
    
  595.           throw new Error("Can't access .root on unmounted test renderer");
    
  596.         }
    
  597.         const children = getChildren(root.current);
    
  598.         if (children.length === 0) {
    
  599.           throw new Error("Can't access .root on unmounted test renderer");
    
  600.         } else if (children.length === 1) {
    
  601.           // Normally, we skip the root and just give you the child.
    
  602.           return children[0];
    
  603.         } else {
    
  604.           // However, we give you the root if there's more than one root child.
    
  605.           // We could make this the behavior for all cases but it would be a breaking change.
    
  606.           // $FlowFixMe[incompatible-use] found when upgrading Flow
    
  607.           return wrapFiber(root.current);
    
  608.         }
    
  609.       },
    
  610.     }: Object),
    
  611.   );
    
  612. 
    
  613.   return entry;
    
  614. }
    
  615. 
    
  616. const fiberToWrapper = new WeakMap<Fiber, ReactTestInstance>();
    
  617. function wrapFiber(fiber: Fiber): ReactTestInstance {
    
  618.   let wrapper = fiberToWrapper.get(fiber);
    
  619.   if (wrapper === undefined && fiber.alternate !== null) {
    
  620.     wrapper = fiberToWrapper.get(fiber.alternate);
    
  621.   }
    
  622.   if (wrapper === undefined) {
    
  623.     wrapper = new ReactTestInstance(fiber);
    
  624.     fiberToWrapper.set(fiber, wrapper);
    
  625.   }
    
  626.   return wrapper;
    
  627. }
    
  628. 
    
  629. // Enable ReactTestRenderer to be used to test DevTools integration.
    
  630. injectIntoDevTools({
    
  631.   findFiberByHostInstance: (() => {
    
  632.     throw new Error('TestRenderer does not support findFiberByHostInstance()');
    
  633.   }: any),
    
  634.   bundleType: __DEV__ ? 1 : 0,
    
  635.   version: ReactVersion,
    
  636.   rendererPackageName: 'react-test-renderer',
    
  637. });
    
  638. 
    
  639. export {
    
  640.   Scheduler as _Scheduler,
    
  641.   create,
    
  642.   /* eslint-disable-next-line camelcase */
    
  643.   batchedUpdates as unstable_batchedUpdates,
    
  644.   act,
    
  645. };