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} from 'react-reconciler/src/ReactInternalTypes';
    
  11. import type {Instance} from './ReactFiberConfig';
    
  12. 
    
  13. import {
    
  14.   HostComponent,
    
  15.   HostHoistable,
    
  16.   HostSingleton,
    
  17.   HostText,
    
  18. } from 'react-reconciler/src/ReactWorkTags';
    
  19. import getComponentNameFromType from 'shared/getComponentNameFromType';
    
  20. import {
    
  21.   findFiberRoot,
    
  22.   getBoundingRect,
    
  23.   getInstanceFromNode,
    
  24.   getTextContent,
    
  25.   isHiddenSubtree,
    
  26.   matchAccessibilityRole,
    
  27.   setFocusIfFocusable,
    
  28.   setupIntersectionObserver,
    
  29.   supportsTestSelectors,
    
  30. } from './ReactFiberConfig';
    
  31. 
    
  32. let COMPONENT_TYPE: symbol | number = 0b000;
    
  33. let HAS_PSEUDO_CLASS_TYPE: symbol | number = 0b001;
    
  34. let ROLE_TYPE: symbol | number = 0b010;
    
  35. let TEST_NAME_TYPE: symbol | number = 0b011;
    
  36. let TEXT_TYPE: symbol | number = 0b100;
    
  37. 
    
  38. if (typeof Symbol === 'function' && Symbol.for) {
    
  39.   const symbolFor = Symbol.for;
    
  40.   COMPONENT_TYPE = symbolFor('selector.component');
    
  41.   HAS_PSEUDO_CLASS_TYPE = symbolFor('selector.has_pseudo_class');
    
  42.   ROLE_TYPE = symbolFor('selector.role');
    
  43.   TEST_NAME_TYPE = symbolFor('selector.test_id');
    
  44.   TEXT_TYPE = symbolFor('selector.text');
    
  45. }
    
  46. 
    
  47. type Type = symbol | number;
    
  48. 
    
  49. type ComponentSelector = {
    
  50.   $$typeof: Type,
    
  51.   value: React$AbstractComponent<empty, mixed>,
    
  52. };
    
  53. 
    
  54. type HasPseudoClassSelector = {
    
  55.   $$typeof: Type,
    
  56.   value: Array<Selector>,
    
  57. };
    
  58. 
    
  59. type RoleSelector = {
    
  60.   $$typeof: Type,
    
  61.   value: string,
    
  62. };
    
  63. 
    
  64. type TextSelector = {
    
  65.   $$typeof: Type,
    
  66.   value: string,
    
  67. };
    
  68. 
    
  69. type TestNameSelector = {
    
  70.   $$typeof: Type,
    
  71.   value: string,
    
  72. };
    
  73. 
    
  74. type Selector =
    
  75.   | ComponentSelector
    
  76.   | HasPseudoClassSelector
    
  77.   | RoleSelector
    
  78.   | TextSelector
    
  79.   | TestNameSelector;
    
  80. 
    
  81. export function createComponentSelector(
    
  82.   component: React$AbstractComponent<empty, mixed>,
    
  83. ): ComponentSelector {
    
  84.   return {
    
  85.     $$typeof: COMPONENT_TYPE,
    
  86.     value: component,
    
  87.   };
    
  88. }
    
  89. 
    
  90. export function createHasPseudoClassSelector(
    
  91.   selectors: Array<Selector>,
    
  92. ): HasPseudoClassSelector {
    
  93.   return {
    
  94.     $$typeof: HAS_PSEUDO_CLASS_TYPE,
    
  95.     value: selectors,
    
  96.   };
    
  97. }
    
  98. 
    
  99. export function createRoleSelector(role: string): RoleSelector {
    
  100.   return {
    
  101.     $$typeof: ROLE_TYPE,
    
  102.     value: role,
    
  103.   };
    
  104. }
    
  105. 
    
  106. export function createTextSelector(text: string): TextSelector {
    
  107.   return {
    
  108.     $$typeof: TEXT_TYPE,
    
  109.     value: text,
    
  110.   };
    
  111. }
    
  112. 
    
  113. export function createTestNameSelector(id: string): TestNameSelector {
    
  114.   return {
    
  115.     $$typeof: TEST_NAME_TYPE,
    
  116.     value: id,
    
  117.   };
    
  118. }
    
  119. 
    
  120. function findFiberRootForHostRoot(hostRoot: Instance): Fiber {
    
  121.   const maybeFiber = getInstanceFromNode((hostRoot: any));
    
  122.   if (maybeFiber != null) {
    
  123.     if (typeof maybeFiber.memoizedProps['data-testname'] !== 'string') {
    
  124.       throw new Error(
    
  125.         'Invalid host root specified. Should be either a React container or a node with a testname attribute.',
    
  126.       );
    
  127.     }
    
  128. 
    
  129.     return ((maybeFiber: any): Fiber);
    
  130.   } else {
    
  131.     const fiberRoot = findFiberRoot(hostRoot);
    
  132. 
    
  133.     if (fiberRoot === null) {
    
  134.       throw new Error(
    
  135.         'Could not find React container within specified host subtree.',
    
  136.       );
    
  137.     }
    
  138. 
    
  139.     // The Flow type for FiberRoot is a little funky.
    
  140.     // createFiberRoot() cheats this by treating the root as :any and adding stateNode lazily.
    
  141.     return ((fiberRoot: any).stateNode.current: Fiber);
    
  142.   }
    
  143. }
    
  144. 
    
  145. function matchSelector(fiber: Fiber, selector: Selector): boolean {
    
  146.   const tag = fiber.tag;
    
  147.   switch (selector.$$typeof) {
    
  148.     case COMPONENT_TYPE:
    
  149.       if (fiber.type === selector.value) {
    
  150.         return true;
    
  151.       }
    
  152.       break;
    
  153.     case HAS_PSEUDO_CLASS_TYPE:
    
  154.       return hasMatchingPaths(
    
  155.         fiber,
    
  156.         ((selector: any): HasPseudoClassSelector).value,
    
  157.       );
    
  158.     case ROLE_TYPE:
    
  159.       if (
    
  160.         tag === HostComponent ||
    
  161.         tag === HostHoistable ||
    
  162.         tag === HostSingleton
    
  163.       ) {
    
  164.         const node = fiber.stateNode;
    
  165.         if (
    
  166.           matchAccessibilityRole(node, ((selector: any): RoleSelector).value)
    
  167.         ) {
    
  168.           return true;
    
  169.         }
    
  170.       }
    
  171.       break;
    
  172.     case TEXT_TYPE:
    
  173.       if (
    
  174.         tag === HostComponent ||
    
  175.         tag === HostText ||
    
  176.         tag === HostHoistable ||
    
  177.         tag === HostSingleton
    
  178.       ) {
    
  179.         const textContent = getTextContent(fiber);
    
  180.         if (
    
  181.           textContent !== null &&
    
  182.           textContent.indexOf(((selector: any): TextSelector).value) >= 0
    
  183.         ) {
    
  184.           return true;
    
  185.         }
    
  186.       }
    
  187.       break;
    
  188.     case TEST_NAME_TYPE:
    
  189.       if (
    
  190.         tag === HostComponent ||
    
  191.         tag === HostHoistable ||
    
  192.         tag === HostSingleton
    
  193.       ) {
    
  194.         const dataTestID = fiber.memoizedProps['data-testname'];
    
  195.         if (
    
  196.           typeof dataTestID === 'string' &&
    
  197.           dataTestID.toLowerCase() ===
    
  198.             ((selector: any): TestNameSelector).value.toLowerCase()
    
  199.         ) {
    
  200.           return true;
    
  201.         }
    
  202.       }
    
  203.       break;
    
  204.     default:
    
  205.       throw new Error('Invalid selector type specified.');
    
  206.   }
    
  207. 
    
  208.   return false;
    
  209. }
    
  210. 
    
  211. function selectorToString(selector: Selector): string | null {
    
  212.   switch (selector.$$typeof) {
    
  213.     case COMPONENT_TYPE:
    
  214.       const displayName = getComponentNameFromType(selector.value) || 'Unknown';
    
  215.       return `<${displayName}>`;
    
  216.     case HAS_PSEUDO_CLASS_TYPE:
    
  217.       return `:has(${selectorToString(selector) || ''})`;
    
  218.     case ROLE_TYPE:
    
  219.       return `[role="${((selector: any): RoleSelector).value}"]`;
    
  220.     case TEXT_TYPE:
    
  221.       return `"${((selector: any): TextSelector).value}"`;
    
  222.     case TEST_NAME_TYPE:
    
  223.       return `[data-testname="${((selector: any): TestNameSelector).value}"]`;
    
  224.     default:
    
  225.       throw new Error('Invalid selector type specified.');
    
  226.   }
    
  227. }
    
  228. 
    
  229. function findPaths(root: Fiber, selectors: Array<Selector>): Array<Fiber> {
    
  230.   const matchingFibers: Array<Fiber> = [];
    
  231. 
    
  232.   const stack = [root, 0];
    
  233.   let index = 0;
    
  234.   while (index < stack.length) {
    
  235.     const fiber = ((stack[index++]: any): Fiber);
    
  236.     const tag = fiber.tag;
    
  237.     let selectorIndex = ((stack[index++]: any): number);
    
  238.     let selector = selectors[selectorIndex];
    
  239. 
    
  240.     if (
    
  241.       (tag === HostComponent ||
    
  242.         tag === HostHoistable ||
    
  243.         tag === HostSingleton) &&
    
  244.       isHiddenSubtree(fiber)
    
  245.     ) {
    
  246.       continue;
    
  247.     } else {
    
  248.       while (selector != null && matchSelector(fiber, selector)) {
    
  249.         selectorIndex++;
    
  250.         selector = selectors[selectorIndex];
    
  251.       }
    
  252.     }
    
  253. 
    
  254.     if (selectorIndex === selectors.length) {
    
  255.       matchingFibers.push(fiber);
    
  256.     } else {
    
  257.       let child = fiber.child;
    
  258.       while (child !== null) {
    
  259.         stack.push(child, selectorIndex);
    
  260.         child = child.sibling;
    
  261.       }
    
  262.     }
    
  263.   }
    
  264. 
    
  265.   return matchingFibers;
    
  266. }
    
  267. 
    
  268. // Same as findPaths but with eager bailout on first match
    
  269. function hasMatchingPaths(root: Fiber, selectors: Array<Selector>): boolean {
    
  270.   const stack = [root, 0];
    
  271.   let index = 0;
    
  272.   while (index < stack.length) {
    
  273.     const fiber = ((stack[index++]: any): Fiber);
    
  274.     const tag = fiber.tag;
    
  275.     let selectorIndex = ((stack[index++]: any): number);
    
  276.     let selector = selectors[selectorIndex];
    
  277. 
    
  278.     if (
    
  279.       (tag === HostComponent ||
    
  280.         tag === HostHoistable ||
    
  281.         tag === HostSingleton) &&
    
  282.       isHiddenSubtree(fiber)
    
  283.     ) {
    
  284.       continue;
    
  285.     } else {
    
  286.       while (selector != null && matchSelector(fiber, selector)) {
    
  287.         selectorIndex++;
    
  288.         selector = selectors[selectorIndex];
    
  289.       }
    
  290.     }
    
  291. 
    
  292.     if (selectorIndex === selectors.length) {
    
  293.       return true;
    
  294.     } else {
    
  295.       let child = fiber.child;
    
  296.       while (child !== null) {
    
  297.         stack.push(child, selectorIndex);
    
  298.         child = child.sibling;
    
  299.       }
    
  300.     }
    
  301.   }
    
  302. 
    
  303.   return false;
    
  304. }
    
  305. 
    
  306. export function findAllNodes(
    
  307.   hostRoot: Instance,
    
  308.   selectors: Array<Selector>,
    
  309. ): Array<Instance> {
    
  310.   if (!supportsTestSelectors) {
    
  311.     throw new Error('Test selector API is not supported by this renderer.');
    
  312.   }
    
  313. 
    
  314.   const root = findFiberRootForHostRoot(hostRoot);
    
  315.   const matchingFibers = findPaths(root, selectors);
    
  316. 
    
  317.   const instanceRoots: Array<Instance> = [];
    
  318. 
    
  319.   const stack = Array.from(matchingFibers);
    
  320.   let index = 0;
    
  321.   while (index < stack.length) {
    
  322.     const node = ((stack[index++]: any): Fiber);
    
  323.     const tag = node.tag;
    
  324.     if (
    
  325.       tag === HostComponent ||
    
  326.       tag === HostHoistable ||
    
  327.       tag === HostSingleton
    
  328.     ) {
    
  329.       if (isHiddenSubtree(node)) {
    
  330.         continue;
    
  331.       }
    
  332.       instanceRoots.push(node.stateNode);
    
  333.     } else {
    
  334.       let child = node.child;
    
  335.       while (child !== null) {
    
  336.         stack.push(child);
    
  337.         child = child.sibling;
    
  338.       }
    
  339.     }
    
  340.   }
    
  341. 
    
  342.   return instanceRoots;
    
  343. }
    
  344. 
    
  345. export function getFindAllNodesFailureDescription(
    
  346.   hostRoot: Instance,
    
  347.   selectors: Array<Selector>,
    
  348. ): string | null {
    
  349.   if (!supportsTestSelectors) {
    
  350.     throw new Error('Test selector API is not supported by this renderer.');
    
  351.   }
    
  352. 
    
  353.   const root = findFiberRootForHostRoot(hostRoot);
    
  354. 
    
  355.   let maxSelectorIndex: number = 0;
    
  356.   const matchedNames = [];
    
  357. 
    
  358.   // The logic of this loop should be kept in sync with findPaths()
    
  359.   const stack = [root, 0];
    
  360.   let index = 0;
    
  361.   while (index < stack.length) {
    
  362.     const fiber = ((stack[index++]: any): Fiber);
    
  363.     const tag = fiber.tag;
    
  364.     let selectorIndex = ((stack[index++]: any): number);
    
  365.     const selector = selectors[selectorIndex];
    
  366. 
    
  367.     if (
    
  368.       (tag === HostComponent ||
    
  369.         tag === HostHoistable ||
    
  370.         tag === HostSingleton) &&
    
  371.       isHiddenSubtree(fiber)
    
  372.     ) {
    
  373.       continue;
    
  374.     } else if (matchSelector(fiber, selector)) {
    
  375.       matchedNames.push(selectorToString(selector));
    
  376.       selectorIndex++;
    
  377. 
    
  378.       if (selectorIndex > maxSelectorIndex) {
    
  379.         maxSelectorIndex = selectorIndex;
    
  380.       }
    
  381.     }
    
  382. 
    
  383.     if (selectorIndex < selectors.length) {
    
  384.       let child = fiber.child;
    
  385.       while (child !== null) {
    
  386.         stack.push(child, selectorIndex);
    
  387.         child = child.sibling;
    
  388.       }
    
  389.     }
    
  390.   }
    
  391. 
    
  392.   if (maxSelectorIndex < selectors.length) {
    
  393.     const unmatchedNames = [];
    
  394.     for (let i = maxSelectorIndex; i < selectors.length; i++) {
    
  395.       unmatchedNames.push(selectorToString(selectors[i]));
    
  396.     }
    
  397. 
    
  398.     return (
    
  399.       'findAllNodes was able to match part of the selector:\n' +
    
  400.       `  ${matchedNames.join(' > ')}\n\n` +
    
  401.       'No matching component was found for:\n' +
    
  402.       `  ${unmatchedNames.join(' > ')}`
    
  403.     );
    
  404.   }
    
  405. 
    
  406.   return null;
    
  407. }
    
  408. 
    
  409. export type BoundingRect = {
    
  410.   x: number,
    
  411.   y: number,
    
  412.   width: number,
    
  413.   height: number,
    
  414. };
    
  415. 
    
  416. export function findBoundingRects(
    
  417.   hostRoot: Instance,
    
  418.   selectors: Array<Selector>,
    
  419. ): Array<BoundingRect> {
    
  420.   if (!supportsTestSelectors) {
    
  421.     throw new Error('Test selector API is not supported by this renderer.');
    
  422.   }
    
  423. 
    
  424.   const instanceRoots = findAllNodes(hostRoot, selectors);
    
  425. 
    
  426.   const boundingRects: Array<BoundingRect> = [];
    
  427.   for (let i = 0; i < instanceRoots.length; i++) {
    
  428.     boundingRects.push(getBoundingRect(instanceRoots[i]));
    
  429.   }
    
  430. 
    
  431.   for (let i = boundingRects.length - 1; i > 0; i--) {
    
  432.     const targetRect = boundingRects[i];
    
  433.     const targetLeft = targetRect.x;
    
  434.     const targetRight = targetLeft + targetRect.width;
    
  435.     const targetTop = targetRect.y;
    
  436.     const targetBottom = targetTop + targetRect.height;
    
  437. 
    
  438.     for (let j = i - 1; j >= 0; j--) {
    
  439.       if (i !== j) {
    
  440.         const otherRect = boundingRects[j];
    
  441.         const otherLeft = otherRect.x;
    
  442.         const otherRight = otherLeft + otherRect.width;
    
  443.         const otherTop = otherRect.y;
    
  444.         const otherBottom = otherTop + otherRect.height;
    
  445. 
    
  446.         // Merging all rects to the minimums set would be complicated,
    
  447.         // but we can handle the most common cases:
    
  448.         // 1. completely overlapping rects
    
  449.         // 2. adjacent rects that are the same width or height (e.g. items in a list)
    
  450.         //
    
  451.         // Even given the above constraints,
    
  452.         // we still won't end up with the fewest possible rects without doing multiple passes,
    
  453.         // but it's good enough for this purpose.
    
  454. 
    
  455.         if (
    
  456.           targetLeft >= otherLeft &&
    
  457.           targetTop >= otherTop &&
    
  458.           targetRight <= otherRight &&
    
  459.           targetBottom <= otherBottom
    
  460.         ) {
    
  461.           // Complete overlapping rects; remove the inner one.
    
  462.           boundingRects.splice(i, 1);
    
  463.           break;
    
  464.         } else if (
    
  465.           targetLeft === otherLeft &&
    
  466.           targetRect.width === otherRect.width &&
    
  467.           !(otherBottom < targetTop) &&
    
  468.           !(otherTop > targetBottom)
    
  469.         ) {
    
  470.           // Adjacent vertical rects; merge them.
    
  471.           if (otherTop > targetTop) {
    
  472.             otherRect.height += otherTop - targetTop;
    
  473.             otherRect.y = targetTop;
    
  474.           }
    
  475.           if (otherBottom < targetBottom) {
    
  476.             otherRect.height = targetBottom - otherTop;
    
  477.           }
    
  478. 
    
  479.           boundingRects.splice(i, 1);
    
  480.           break;
    
  481.         } else if (
    
  482.           targetTop === otherTop &&
    
  483.           targetRect.height === otherRect.height &&
    
  484.           !(otherRight < targetLeft) &&
    
  485.           !(otherLeft > targetRight)
    
  486.         ) {
    
  487.           // Adjacent horizontal rects; merge them.
    
  488.           if (otherLeft > targetLeft) {
    
  489.             otherRect.width += otherLeft - targetLeft;
    
  490.             otherRect.x = targetLeft;
    
  491.           }
    
  492.           if (otherRight < targetRight) {
    
  493.             otherRect.width = targetRight - otherLeft;
    
  494.           }
    
  495. 
    
  496.           boundingRects.splice(i, 1);
    
  497.           break;
    
  498.         }
    
  499.       }
    
  500.     }
    
  501.   }
    
  502. 
    
  503.   return boundingRects;
    
  504. }
    
  505. 
    
  506. export function focusWithin(
    
  507.   hostRoot: Instance,
    
  508.   selectors: Array<Selector>,
    
  509. ): boolean {
    
  510.   if (!supportsTestSelectors) {
    
  511.     throw new Error('Test selector API is not supported by this renderer.');
    
  512.   }
    
  513. 
    
  514.   const root = findFiberRootForHostRoot(hostRoot);
    
  515.   const matchingFibers = findPaths(root, selectors);
    
  516. 
    
  517.   const stack = Array.from(matchingFibers);
    
  518.   let index = 0;
    
  519.   while (index < stack.length) {
    
  520.     const fiber = ((stack[index++]: any): Fiber);
    
  521.     const tag = fiber.tag;
    
  522.     if (isHiddenSubtree(fiber)) {
    
  523.       continue;
    
  524.     }
    
  525.     if (
    
  526.       tag === HostComponent ||
    
  527.       tag === HostHoistable ||
    
  528.       tag === HostSingleton
    
  529.     ) {
    
  530.       const node = fiber.stateNode;
    
  531.       if (setFocusIfFocusable(node)) {
    
  532.         return true;
    
  533.       }
    
  534.     }
    
  535.     let child = fiber.child;
    
  536.     while (child !== null) {
    
  537.       stack.push(child);
    
  538.       child = child.sibling;
    
  539.     }
    
  540.   }
    
  541. 
    
  542.   return false;
    
  543. }
    
  544. 
    
  545. const commitHooks: Array<Function> = [];
    
  546. 
    
  547. export function onCommitRoot(): void {
    
  548.   if (supportsTestSelectors) {
    
  549.     commitHooks.forEach(commitHook => commitHook());
    
  550.   }
    
  551. }
    
  552. 
    
  553. export type IntersectionObserverOptions = Object;
    
  554. 
    
  555. export type ObserveVisibleRectsCallback = (
    
  556.   intersections: Array<{ratio: number, rect: BoundingRect}>,
    
  557. ) => void;
    
  558. 
    
  559. export function observeVisibleRects(
    
  560.   hostRoot: Instance,
    
  561.   selectors: Array<Selector>,
    
  562.   callback: (intersections: Array<{ratio: number, rect: BoundingRect}>) => void,
    
  563.   options?: IntersectionObserverOptions,
    
  564. ): {disconnect: () => void} {
    
  565.   if (!supportsTestSelectors) {
    
  566.     throw new Error('Test selector API is not supported by this renderer.');
    
  567.   }
    
  568. 
    
  569.   const instanceRoots = findAllNodes(hostRoot, selectors);
    
  570. 
    
  571.   const {disconnect, observe, unobserve} = setupIntersectionObserver(
    
  572.     instanceRoots,
    
  573.     callback,
    
  574.     options,
    
  575.   );
    
  576. 
    
  577.   // When React mutates the host environment, we may need to change what we're listening to.
    
  578.   const commitHook = () => {
    
  579.     const nextInstanceRoots = findAllNodes(hostRoot, selectors);
    
  580. 
    
  581.     instanceRoots.forEach(target => {
    
  582.       if (nextInstanceRoots.indexOf(target) < 0) {
    
  583.         unobserve(target);
    
  584.       }
    
  585.     });
    
  586. 
    
  587.     nextInstanceRoots.forEach(target => {
    
  588.       if (instanceRoots.indexOf(target) < 0) {
    
  589.         observe(target);
    
  590.       }
    
  591.     });
    
  592.   };
    
  593. 
    
  594.   commitHooks.push(commitHook);
    
  595. 
    
  596.   return {
    
  597.     disconnect: () => {
    
  598.       // Stop listening for React mutations:
    
  599.       const index = commitHooks.indexOf(commitHook);
    
  600.       if (index >= 0) {
    
  601.         commitHooks.splice(index, 1);
    
  602.       }
    
  603. 
    
  604.       // Disconnect the host observer:
    
  605.       disconnect();
    
  606.     },
    
  607.   };
    
  608. }