import semver from 'semver';
import typeof ReactTestRenderer from 'react-test-renderer';
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
import type Store from 'react-devtools-shared/src/devtools/store';
import type {ProfilingDataFrontend} from 'react-devtools-shared/src/devtools/views/Profiler/types';
import type {ElementType} from 'react-devtools-shared/src/frontend/types';
import type {Node as ReactNode} from 'react';
import {ReactVersion} from '../../../../ReactVersions';
const requestedReactVersion = process.env.REACT_VERSION || ReactVersion;
export function getActDOMImplementation(): () => void | Promise<void> {
if (semver.lt(requestedReactVersion, '17.0.0')) {
require('react-dom/test-utils');
return cb => cb();
}
if (semver.lt(requestedReactVersion, '18.0.0')) {
const ReactDOMTestUtils = require('react-dom/test-utils');
return ReactDOMTestUtils.act;
}
const React = require('react');
if (React.unstable_act) {
return React.unstable_act;
}
if (React.act) {
return React.act;
}
throw new Error("Couldn't find any available act implementation");
}
export function getActTestRendererImplementation(): () => void | Promise<void> {
if (semver.lt(requestedReactVersion, '17.0.0')) {
require('react-test-renderer');
return cb => cb();
}
const RTR = require('react-test-renderer');
if (RTR.act) {
return RTR.act;
}
throw new Error(
"Couldn't find any available act implementation in react-test-renderer",
);
}
export function act(
callback: Function,
recursivelyFlush: boolean = true,
): void {
const actTestRenderer = getActTestRendererImplementation();
const actDOM = getActDOMImplementation();
actDOM(() => {
actTestRenderer(() => {
callback();
});
});
if (recursivelyFlush) {
while (jest.getTimerCount() > 0) {
actDOM(() => {
actTestRenderer(() => {
jest.runAllTimers();
});
});
}
}
}
export async function actAsync(
cb: () => *,
recursivelyFlush: boolean = true,
): Promise<void> {
const actTestRenderer = getActTestRendererImplementation();
const actDOM = getActDOMImplementation();
await actDOM(async () => {
await actTestRenderer(async () => {
await cb();
});
});
if (recursivelyFlush) {
while (jest.getTimerCount() > 0) {
await actDOM(async () => {
await actTestRenderer(async () => {
jest.runAllTimers();
});
});
}
} else {
await actDOM(async () => {
await actTestRenderer(async () => {
jest.runOnlyPendingTimers();
});
});
}
}
type RenderImplementation = {
render: (elements: ?ReactNode) => () => void,
unmount: () => void,
createContainer: () => void,
getContainer: () => ?HTMLElement,
};
export function getLegacyRenderImplementation(): RenderImplementation {
let ReactDOM;
let container;
const containersToRemove = [];
beforeEach(() => {
ReactDOM = require('react-dom');
createContainer();
});
afterEach(() => {
containersToRemove.forEach(c => document.body.removeChild(c));
containersToRemove.splice(0, containersToRemove.length);
ReactDOM = null;
container = null;
});
function render(elements) {
withErrorsOrWarningsIgnored(
['ReactDOM.render has not been supported since React 18'],
() => {
ReactDOM.render(elements, container);
},
);
return unmount;
}
function unmount() {
ReactDOM.unmountComponentAtNode(container);
}
function createContainer() {
container = document.createElement('div');
document.body.appendChild(container);
containersToRemove.push(container);
}
function getContainer() {
return container;
}
return {
render,
unmount,
createContainer,
getContainer,
};
}
export function getModernRenderImplementation(): RenderImplementation {
let ReactDOMClient;
let container;
let root;
const containersToRemove = [];
beforeEach(() => {
ReactDOMClient = require('react-dom/client');
createContainer();
});
afterEach(() => {
containersToRemove.forEach(c => document.body.removeChild(c));
containersToRemove.splice(0, containersToRemove.length);
ReactDOMClient = null;
container = null;
root = null;
});
function render(elements) {
if (root == null) {
root = ReactDOMClient.createRoot(container);
}
root.render(elements);
return unmount;
}
function unmount() {
root.unmount();
}
function createContainer() {
container = document.createElement('div');
document.body.appendChild(container);
root = null;
containersToRemove.push(container);
}
function getContainer() {
return container;
}
return {
render,
unmount,
createContainer,
getContainer,
};
}
export const getVersionedRenderImplementation: () => RenderImplementation =
semver.lt(requestedReactVersion, '18.0.0')
? getLegacyRenderImplementation
: getModernRenderImplementation;
export function beforeEachProfiling(): void {
jest.mock('scheduler', () => jest.requireActual('scheduler/unstable_mock'));
jest
.spyOn(performance, 'now')
.mockImplementation(
jest.requireActual('scheduler/unstable_mock').unstable_now,
);
}
export function createDisplayNameFilter(
source: string,
isEnabled: boolean = true,
) {
const Types = require('react-devtools-shared/src/frontend/types');
let isValid = true;
try {
new RegExp(source);
} catch (error) {
isValid = false;
}
return {
type: Types.ComponentFilterDisplayName,
isEnabled,
isValid,
value: source,
};
}
export function createHOCFilter(isEnabled: boolean = true) {
const Types = require('react-devtools-shared/src/frontend/types');
return {
type: Types.ComponentFilterHOC,
isEnabled,
isValid: true,
};
}
export function createEnvironmentNameFilter(
env: string,
isEnabled: boolean = true,
) {
const Types = require('react-devtools-shared/src/frontend/types');
return {
type: Types.ComponentFilterEnvironmentName,
isEnabled,
isValid: true,
value: env,
};
}
export function createElementTypeFilter(
elementType: ElementType,
isEnabled: boolean = true,
) {
const Types = require('react-devtools-shared/src/frontend/types');
return {
type: Types.ComponentFilterElementType,
isEnabled,
value: elementType,
};
}
export function createLocationFilter(
source: string,
isEnabled: boolean = true,
) {
const Types = require('react-devtools-shared/src/frontend/types');
let isValid = true;
try {
new RegExp(source);
} catch (error) {
isValid = false;
}
return {
type: Types.ComponentFilterLocation,
isEnabled,
isValid,
value: source,
};
}
export function getRendererID(): number {
if (global.agent == null) {
throw Error('Agent unavailable.');
}
const ids = Object.keys(global.agent._rendererInterfaces);
const id = ids.find(innerID => {
const rendererInterface = global.agent._rendererInterfaces[innerID];
return rendererInterface.renderer.rendererPackageName === 'react-dom';
});
if (id == null) {
throw Error('Could not find renderer.');
}
return parseInt(id, 10);
}
export function legacyRender(elements, container) {
if (container == null) {
container = document.createElement('div');
}
const ReactDOM = require('react-dom');
withErrorsOrWarningsIgnored(
['ReactDOM.render has not been supported since React 18'],
() => {
ReactDOM.render(elements, container);
},
);
return () => {
ReactDOM.unmountComponentAtNode(container);
};
}
export function requireTestRenderer(): ReactTestRenderer {
let hook;
try {
hook = global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
delete global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
return require('react-test-renderer');
} finally {
global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = hook;
}
}
export function exportImportHelper(bridge: FrontendBridge, store: Store): void {
const {
prepareProfilingDataExport,
prepareProfilingDataFrontendFromExport,
} = require('react-devtools-shared/src/devtools/views/Profiler/utils');
const {profilerStore} = store;
expect(profilerStore.profilingData).not.toBeNull();
const profilingDataFrontendInitial =
((profilerStore.profilingData: any): ProfilingDataFrontend);
expect(profilingDataFrontendInitial.imported).toBe(false);
const profilingDataExport = prepareProfilingDataExport(
profilingDataFrontendInitial,
);
const serializedProfilingDataExport = JSON.stringify(
profilingDataExport,
null,
2,
);
const parsedProfilingDataExport = JSON.parse(serializedProfilingDataExport);
const profilingDataFrontend = prepareProfilingDataFrontendFromExport(
(parsedProfilingDataExport: any),
);
expect(profilingDataFrontend.imported).toBe(true);
expect(profilingDataFrontendInitial.dataForRoots).toEqual(
profilingDataFrontend.dataForRoots,
);
expect(profilingDataFrontendInitial.timelineData).toEqual(
profilingDataFrontend.timelineData,
);
act(() => {
profilerStore.profilingData = profilingDataFrontend;
});
}
export function withErrorsOrWarningsIgnored<T: void | Promise<void>>(
errorOrWarningMessages: string[],
fn: () => T,
): T {
const prev = global._ignoredErrorOrWarningMessages || [];
let resetIgnoredErrorOrWarningMessages = true;
try {
global._ignoredErrorOrWarningMessages = [
...prev,
...errorOrWarningMessages,
];
const maybeThenable = fn();
if (
maybeThenable !== undefined &&
typeof maybeThenable.then === 'function'
) {
resetIgnoredErrorOrWarningMessages = false;
return maybeThenable.then(
() => {
global._ignoredErrorOrWarningMessages = prev;
},
() => {
global._ignoredErrorOrWarningMessages = prev;
},
);
}
} finally {
if (resetIgnoredErrorOrWarningMessages) {
global._ignoredErrorOrWarningMessages = prev;
}
}
}
export function overrideFeatureFlags(overrideFlags) {
jest.mock('react-devtools-feature-flags', () => {
const actualFlags = jest.requireActual('react-devtools-feature-flags');
return {
...actualFlags,
...overrideFlags,
};
});
}
export function normalizeCodeLocInfo(str) {
if (typeof str === 'object' && str !== null) {
str = str.stack;
}
if (typeof str !== 'string') {
return str;
}
str = str.replace(/Check your code at .+?:\d+/g, 'Check your code at **');
return str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) {
return '\n in ' + name + ' (at **)';
});
}