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 typeof ReactTestRenderer from 'react-test-renderer';
    
  11. 
    
  12. import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
    
  13. import type Store from 'react-devtools-shared/src/devtools/store';
    
  14. import type {ProfilingDataFrontend} from 'react-devtools-shared/src/devtools/views/Profiler/types';
    
  15. import type {ElementType} from 'react-devtools-shared/src/frontend/types';
    
  16. 
    
  17. export function act(
    
  18.   callback: Function,
    
  19.   recursivelyFlush: boolean = true,
    
  20. ): void {
    
  21.   const {act: actTestRenderer} = require('react-test-renderer');
    
  22.   const {act: actDOM} = require('react-dom/test-utils');
    
  23. 
    
  24.   actDOM(() => {
    
  25.     actTestRenderer(() => {
    
  26.       callback();
    
  27.     });
    
  28.   });
    
  29. 
    
  30.   if (recursivelyFlush) {
    
  31.     // Flush Bridge operations
    
  32.     while (jest.getTimerCount() > 0) {
    
  33.       actDOM(() => {
    
  34.         actTestRenderer(() => {
    
  35.           jest.runAllTimers();
    
  36.         });
    
  37.       });
    
  38.     }
    
  39.   }
    
  40. }
    
  41. 
    
  42. export async function actAsync(
    
  43.   cb: () => *,
    
  44.   recursivelyFlush: boolean = true,
    
  45. ): Promise<void> {
    
  46.   const {act: actTestRenderer} = require('react-test-renderer');
    
  47.   const {act: actDOM} = require('react-dom/test-utils');
    
  48. 
    
  49.   await actDOM(async () => {
    
  50.     await actTestRenderer(async () => {
    
  51.       await cb();
    
  52.     });
    
  53.   });
    
  54. 
    
  55.   if (recursivelyFlush) {
    
  56.     while (jest.getTimerCount() > 0) {
    
  57.       await actDOM(async () => {
    
  58.         await actTestRenderer(async () => {
    
  59.           jest.runAllTimers();
    
  60.         });
    
  61.       });
    
  62.     }
    
  63.   } else {
    
  64.     await actDOM(async () => {
    
  65.       await actTestRenderer(async () => {
    
  66.         jest.runOnlyPendingTimers();
    
  67.       });
    
  68.     });
    
  69.   }
    
  70. }
    
  71. 
    
  72. export function beforeEachProfiling(): void {
    
  73.   // Mock React's timing information so that test runs are predictable.
    
  74.   jest.mock('scheduler', () => jest.requireActual('scheduler/unstable_mock'));
    
  75. 
    
  76.   // DevTools itself uses performance.now() to offset commit times
    
  77.   // so they appear relative to when profiling was started in the UI.
    
  78.   jest
    
  79.     .spyOn(performance, 'now')
    
  80.     .mockImplementation(
    
  81.       jest.requireActual('scheduler/unstable_mock').unstable_now,
    
  82.     );
    
  83. }
    
  84. 
    
  85. export function createDisplayNameFilter(
    
  86.   source: string,
    
  87.   isEnabled: boolean = true,
    
  88. ) {
    
  89.   const Types = require('react-devtools-shared/src/frontend/types');
    
  90.   let isValid = true;
    
  91.   try {
    
  92.     new RegExp(source); // eslint-disable-line no-new
    
  93.   } catch (error) {
    
  94.     isValid = false;
    
  95.   }
    
  96.   return {
    
  97.     type: Types.ComponentFilterDisplayName,
    
  98.     isEnabled,
    
  99.     isValid,
    
  100.     value: source,
    
  101.   };
    
  102. }
    
  103. 
    
  104. export function createHOCFilter(isEnabled: boolean = true) {
    
  105.   const Types = require('react-devtools-shared/src/frontend/types');
    
  106.   return {
    
  107.     type: Types.ComponentFilterHOC,
    
  108.     isEnabled,
    
  109.     isValid: true,
    
  110.   };
    
  111. }
    
  112. 
    
  113. export function createElementTypeFilter(
    
  114.   elementType: ElementType,
    
  115.   isEnabled: boolean = true,
    
  116. ) {
    
  117.   const Types = require('react-devtools-shared/src/frontend/types');
    
  118.   return {
    
  119.     type: Types.ComponentFilterElementType,
    
  120.     isEnabled,
    
  121.     value: elementType,
    
  122.   };
    
  123. }
    
  124. 
    
  125. export function createLocationFilter(
    
  126.   source: string,
    
  127.   isEnabled: boolean = true,
    
  128. ) {
    
  129.   const Types = require('react-devtools-shared/src/frontend/types');
    
  130.   let isValid = true;
    
  131.   try {
    
  132.     new RegExp(source); // eslint-disable-line no-new
    
  133.   } catch (error) {
    
  134.     isValid = false;
    
  135.   }
    
  136.   return {
    
  137.     type: Types.ComponentFilterLocation,
    
  138.     isEnabled,
    
  139.     isValid,
    
  140.     value: source,
    
  141.   };
    
  142. }
    
  143. 
    
  144. export function getRendererID(): number {
    
  145.   if (global.agent == null) {
    
  146.     throw Error('Agent unavailable.');
    
  147.   }
    
  148.   const ids = Object.keys(global.agent._rendererInterfaces);
    
  149. 
    
  150.   const id = ids.find(innerID => {
    
  151.     const rendererInterface = global.agent._rendererInterfaces[innerID];
    
  152.     return rendererInterface.renderer.rendererPackageName === 'react-dom';
    
  153.   });
    
  154. 
    
  155.   if (id == null) {
    
  156.     throw Error('Could not find renderer.');
    
  157.   }
    
  158. 
    
  159.   return parseInt(id, 10);
    
  160. }
    
  161. 
    
  162. export function legacyRender(elements, container) {
    
  163.   if (container == null) {
    
  164.     container = document.createElement('div');
    
  165.   }
    
  166. 
    
  167.   const ReactDOM = require('react-dom');
    
  168.   withErrorsOrWarningsIgnored(
    
  169.     ['ReactDOM.render is no longer supported in React 18'],
    
  170.     () => {
    
  171.       ReactDOM.render(elements, container);
    
  172.     },
    
  173.   );
    
  174. 
    
  175.   return () => {
    
  176.     ReactDOM.unmountComponentAtNode(container);
    
  177.   };
    
  178. }
    
  179. 
    
  180. export function requireTestRenderer(): ReactTestRenderer {
    
  181.   let hook;
    
  182.   try {
    
  183.     // Hide the hook before requiring TestRenderer, so we don't end up with a loop.
    
  184.     hook = global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
    
  185.     delete global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
    
  186. 
    
  187.     return require('react-test-renderer');
    
  188.   } finally {
    
  189.     global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = hook;
    
  190.   }
    
  191. }
    
  192. 
    
  193. export function exportImportHelper(bridge: FrontendBridge, store: Store): void {
    
  194.   const {
    
  195.     prepareProfilingDataExport,
    
  196.     prepareProfilingDataFrontendFromExport,
    
  197.   } = require('react-devtools-shared/src/devtools/views/Profiler/utils');
    
  198. 
    
  199.   const {profilerStore} = store;
    
  200. 
    
  201.   expect(profilerStore.profilingData).not.toBeNull();
    
  202. 
    
  203.   const profilingDataFrontendInitial =
    
  204.     ((profilerStore.profilingData: any): ProfilingDataFrontend);
    
  205.   expect(profilingDataFrontendInitial.imported).toBe(false);
    
  206. 
    
  207.   const profilingDataExport = prepareProfilingDataExport(
    
  208.     profilingDataFrontendInitial,
    
  209.   );
    
  210. 
    
  211.   // Simulate writing/reading to disk.
    
  212.   const serializedProfilingDataExport = JSON.stringify(
    
  213.     profilingDataExport,
    
  214.     null,
    
  215.     2,
    
  216.   );
    
  217.   const parsedProfilingDataExport = JSON.parse(serializedProfilingDataExport);
    
  218. 
    
  219.   const profilingDataFrontend = prepareProfilingDataFrontendFromExport(
    
  220.     (parsedProfilingDataExport: any),
    
  221.   );
    
  222.   expect(profilingDataFrontend.imported).toBe(true);
    
  223. 
    
  224.   // Sanity check that profiling snapshots are serialized correctly.
    
  225.   expect(profilingDataFrontendInitial.dataForRoots).toEqual(
    
  226.     profilingDataFrontend.dataForRoots,
    
  227.   );
    
  228.   expect(profilingDataFrontendInitial.timelineData).toEqual(
    
  229.     profilingDataFrontend.timelineData,
    
  230.   );
    
  231. 
    
  232.   // Snapshot the JSON-parsed object, rather than the raw string, because Jest formats the diff nicer.
    
  233.   // expect(parsedProfilingDataExport).toMatchSnapshot('imported data');
    
  234. 
    
  235.   act(() => {
    
  236.     // Apply the new exported-then-imported data so tests can re-run assertions.
    
  237.     profilerStore.profilingData = profilingDataFrontend;
    
  238.   });
    
  239. }
    
  240. 
    
  241. /**
    
  242.  * Runs `fn` while preventing console error and warnings that partially match any given `errorOrWarningMessages` from appearing in the console.
    
  243.  * @param errorOrWarningMessages Messages are matched partially (i.e. indexOf), pre-formatting.
    
  244.  * @param fn
    
  245.  */
    
  246. export function withErrorsOrWarningsIgnored<T: void | Promise<void>>(
    
  247.   errorOrWarningMessages: string[],
    
  248.   fn: () => T,
    
  249. ): T {
    
  250.   // withErrorsOrWarningsIgnored() may be nested.
    
  251.   const prev = global._ignoredErrorOrWarningMessages || [];
    
  252. 
    
  253.   let resetIgnoredErrorOrWarningMessages = true;
    
  254.   try {
    
  255.     global._ignoredErrorOrWarningMessages = [
    
  256.       ...prev,
    
  257.       ...errorOrWarningMessages,
    
  258.     ];
    
  259.     const maybeThenable = fn();
    
  260.     if (
    
  261.       maybeThenable !== undefined &&
    
  262.       typeof maybeThenable.then === 'function'
    
  263.     ) {
    
  264.       resetIgnoredErrorOrWarningMessages = false;
    
  265.       return maybeThenable.then(
    
  266.         () => {
    
  267.           global._ignoredErrorOrWarningMessages = prev;
    
  268.         },
    
  269.         () => {
    
  270.           global._ignoredErrorOrWarningMessages = prev;
    
  271.         },
    
  272.       );
    
  273.     }
    
  274.   } finally {
    
  275.     if (resetIgnoredErrorOrWarningMessages) {
    
  276.       global._ignoredErrorOrWarningMessages = prev;
    
  277.     }
    
  278.   }
    
  279. }
    
  280. 
    
  281. export function overrideFeatureFlags(overrideFlags) {
    
  282.   jest.mock('react-devtools-feature-flags', () => {
    
  283.     const actualFlags = jest.requireActual('react-devtools-feature-flags');
    
  284.     return {
    
  285.       ...actualFlags,
    
  286.       ...overrideFlags,
    
  287.     };
    
  288.   });
    
  289. }
    
  290. 
    
  291. export function normalizeCodeLocInfo(str) {
    
  292.   if (typeof str !== 'string') {
    
  293.     return str;
    
  294.   }
    
  295.   // This special case exists only for the special source location in
    
  296.   // ReactElementValidator. That will go away if we remove source locations.
    
  297.   str = str.replace(/Check your code at .+?:\d+/g, 'Check your code at **');
    
  298.   // V8 format:
    
  299.   //  at Component (/path/filename.js:123:45)
    
  300.   // React format:
    
  301.   //    in Component (at filename.js:123)
    
  302.   return str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) {
    
  303.     return '\n    in ' + name + ' (at **)';
    
  304.   });
    
  305. }