mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
4c12339ce3
This PR moves `flushSync` out of the reconciler. there is still an internal implementation that is used when these semantics are needed for React methods such as `unmount` on roots. This new isomorphic `flushSync` is only used in builds that no longer support legacy mode. Additionally all the internal uses of flushSync in the reconciler have been replaced with more direct methods. There is a new `updateContainerSync` method which updates a container but forces it to the Sync lane and flushes passive effects if necessary. This combined with flushSyncWork can be used to replace flushSync for all instances of internal usage. We still maintain the original flushSync implementation as `flushSyncFromReconciler` because it will be used as the flushSync implementation for FB builds. This is because it has special legacy mode handling that the new isomorphic implementation does not need to consider. It will be removed from production OSS builds by closure though
662 lines
17 KiB
JavaScript
662 lines
17 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow
|
|
*/
|
|
|
|
import type {Fiber, FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
|
|
import type {
|
|
PublicInstance,
|
|
Instance,
|
|
TextInstance,
|
|
} from './ReactFiberConfigTestHost';
|
|
|
|
import * as React from 'react';
|
|
import * as Scheduler from 'scheduler/unstable_mock';
|
|
import {
|
|
getPublicRootInstance,
|
|
createContainer,
|
|
updateContainer,
|
|
flushSyncFromReconciler,
|
|
injectIntoDevTools,
|
|
batchedUpdates,
|
|
defaultOnUncaughtError,
|
|
defaultOnCaughtError,
|
|
defaultOnRecoverableError,
|
|
} from 'react-reconciler/src/ReactFiberReconciler';
|
|
import {findCurrentFiberUsingSlowPath} from 'react-reconciler/src/ReactFiberTreeReflection';
|
|
import {
|
|
Fragment,
|
|
FunctionComponent,
|
|
ClassComponent,
|
|
HostComponent,
|
|
HostHoistable,
|
|
HostSingleton,
|
|
HostPortal,
|
|
HostText,
|
|
HostRoot,
|
|
ContextConsumer,
|
|
ContextProvider,
|
|
Mode,
|
|
ForwardRef,
|
|
Profiler,
|
|
MemoComponent,
|
|
SimpleMemoComponent,
|
|
IncompleteClassComponent,
|
|
ScopeComponent,
|
|
} from 'react-reconciler/src/ReactWorkTags';
|
|
import isArray from 'shared/isArray';
|
|
import getComponentNameFromType from 'shared/getComponentNameFromType';
|
|
import ReactVersion from 'shared/ReactVersion';
|
|
import {checkPropStringCoercion} from 'shared/CheckStringCoercion';
|
|
|
|
import {getPublicInstance} from './ReactFiberConfigTestHost';
|
|
import {ConcurrentRoot, LegacyRoot} from 'react-reconciler/src/ReactRootTags';
|
|
import {
|
|
allowConcurrentByDefault,
|
|
enableReactTestRendererWarning,
|
|
disableLegacyMode,
|
|
} from 'shared/ReactFeatureFlags';
|
|
|
|
const act = React.act;
|
|
|
|
// TODO: Remove from public bundle
|
|
|
|
type TestRendererOptions = {
|
|
createNodeMock: (element: React$Element<any>) => any,
|
|
unstable_isConcurrent: boolean,
|
|
unstable_strictMode: boolean,
|
|
unstable_concurrentUpdatesByDefault: boolean,
|
|
...
|
|
};
|
|
|
|
type ReactTestRendererJSON = {
|
|
type: string,
|
|
props: {[propName: string]: any, ...},
|
|
children: null | Array<ReactTestRendererNode>,
|
|
$$typeof?: symbol, // Optional because we add it with defineProperty().
|
|
};
|
|
type ReactTestRendererNode = ReactTestRendererJSON | string;
|
|
|
|
type FindOptions = {
|
|
// performs a "greedy" search: if a matching node is found, will continue
|
|
// to search within the matching node's children. (default: true)
|
|
deep?: boolean,
|
|
};
|
|
|
|
export type Predicate = (node: ReactTestInstance) => ?boolean;
|
|
|
|
const defaultTestOptions = {
|
|
createNodeMock: function () {
|
|
return null;
|
|
},
|
|
};
|
|
|
|
function toJSON(inst: Instance | TextInstance): ReactTestRendererNode | null {
|
|
if (inst.isHidden) {
|
|
// Omit timed out children from output entirely. This seems like the least
|
|
// surprising behavior. We could perhaps add a separate API that includes
|
|
// them, if it turns out people need it.
|
|
return null;
|
|
}
|
|
switch (inst.tag) {
|
|
case 'TEXT':
|
|
return inst.text;
|
|
case 'INSTANCE': {
|
|
/* eslint-disable no-unused-vars */
|
|
// We don't include the `children` prop in JSON.
|
|
// Instead, we will include the actual rendered children.
|
|
const {children, ...props} = inst.props;
|
|
/* eslint-enable */
|
|
let renderedChildren = null;
|
|
if (inst.children && inst.children.length) {
|
|
for (let i = 0; i < inst.children.length; i++) {
|
|
const renderedChild = toJSON(inst.children[i]);
|
|
if (renderedChild !== null) {
|
|
if (renderedChildren === null) {
|
|
renderedChildren = [renderedChild];
|
|
} else {
|
|
renderedChildren.push(renderedChild);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
const json: ReactTestRendererJSON = {
|
|
type: inst.type,
|
|
props: props,
|
|
children: renderedChildren,
|
|
};
|
|
Object.defineProperty(json, '$$typeof', {
|
|
value: Symbol.for('react.test.json'),
|
|
});
|
|
return json;
|
|
}
|
|
default:
|
|
throw new Error(`Unexpected node type in toJSON: ${inst.tag}`);
|
|
}
|
|
}
|
|
|
|
function childrenToTree(node: null | Fiber) {
|
|
if (!node) {
|
|
return null;
|
|
}
|
|
const children = nodeAndSiblingsArray(node);
|
|
if (children.length === 0) {
|
|
return null;
|
|
} else if (children.length === 1) {
|
|
return toTree(children[0]);
|
|
}
|
|
return flatten(children.map(toTree));
|
|
}
|
|
|
|
// $FlowFixMe[missing-local-annot]
|
|
function nodeAndSiblingsArray(nodeWithSibling) {
|
|
const array = [];
|
|
let node = nodeWithSibling;
|
|
while (node != null) {
|
|
array.push(node);
|
|
node = node.sibling;
|
|
}
|
|
return array;
|
|
}
|
|
|
|
// $FlowFixMe[missing-local-annot]
|
|
function flatten(arr) {
|
|
const result = [];
|
|
const stack = [{i: 0, array: arr}];
|
|
while (stack.length) {
|
|
const n = stack.pop();
|
|
while (n.i < n.array.length) {
|
|
const el = n.array[n.i];
|
|
n.i += 1;
|
|
if (isArray(el)) {
|
|
stack.push(n);
|
|
stack.push({i: 0, array: el});
|
|
break;
|
|
}
|
|
result.push(el);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function toTree(node: null | Fiber): $FlowFixMe {
|
|
if (node == null) {
|
|
return null;
|
|
}
|
|
switch (node.tag) {
|
|
case HostRoot:
|
|
return childrenToTree(node.child);
|
|
case HostPortal:
|
|
return childrenToTree(node.child);
|
|
case ClassComponent:
|
|
return {
|
|
nodeType: 'component',
|
|
type: node.type,
|
|
props: {...node.memoizedProps},
|
|
instance: node.stateNode,
|
|
rendered: childrenToTree(node.child),
|
|
};
|
|
case FunctionComponent:
|
|
case SimpleMemoComponent:
|
|
return {
|
|
nodeType: 'component',
|
|
type: node.type,
|
|
props: {...node.memoizedProps},
|
|
instance: null,
|
|
rendered: childrenToTree(node.child),
|
|
};
|
|
case HostHoistable:
|
|
case HostSingleton:
|
|
case HostComponent: {
|
|
return {
|
|
nodeType: 'host',
|
|
type: node.type,
|
|
props: {...node.memoizedProps},
|
|
instance: null, // TODO: use createNodeMock here somehow?
|
|
rendered: flatten(nodeAndSiblingsArray(node.child).map(toTree)),
|
|
};
|
|
}
|
|
case HostText:
|
|
return node.stateNode.text;
|
|
case Fragment:
|
|
case ContextProvider:
|
|
case ContextConsumer:
|
|
case Mode:
|
|
case Profiler:
|
|
case ForwardRef:
|
|
case MemoComponent:
|
|
case IncompleteClassComponent:
|
|
case ScopeComponent:
|
|
return childrenToTree(node.child);
|
|
default:
|
|
throw new Error(
|
|
`toTree() does not yet know how to handle nodes with tag=${node.tag}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
const validWrapperTypes = new Set([
|
|
FunctionComponent,
|
|
ClassComponent,
|
|
HostComponent,
|
|
ForwardRef,
|
|
MemoComponent,
|
|
SimpleMemoComponent,
|
|
// Normally skipped, but used when there's more than one root child.
|
|
HostRoot,
|
|
]);
|
|
|
|
function getChildren(parent: Fiber) {
|
|
const children = [];
|
|
const startingNode = parent;
|
|
let node: Fiber = startingNode;
|
|
if (node.child === null) {
|
|
return children;
|
|
}
|
|
node.child.return = node;
|
|
node = node.child;
|
|
outer: while (true) {
|
|
let descend = false;
|
|
if (validWrapperTypes.has(node.tag)) {
|
|
children.push(wrapFiber(node));
|
|
} else if (node.tag === HostText) {
|
|
if (__DEV__) {
|
|
checkPropStringCoercion(node.memoizedProps, 'memoizedProps');
|
|
}
|
|
children.push('' + node.memoizedProps);
|
|
} else {
|
|
descend = true;
|
|
}
|
|
if (descend && node.child !== null) {
|
|
node.child.return = node;
|
|
node = node.child;
|
|
continue;
|
|
}
|
|
while (node.sibling === null) {
|
|
if (node.return === startingNode) {
|
|
break outer;
|
|
}
|
|
node = (node.return: any);
|
|
}
|
|
(node.sibling: any).return = node.return;
|
|
node = (node.sibling: any);
|
|
}
|
|
return children;
|
|
}
|
|
|
|
class ReactTestInstance {
|
|
_fiber: Fiber;
|
|
|
|
_currentFiber(): Fiber {
|
|
// Throws if this component has been unmounted.
|
|
const fiber = findCurrentFiberUsingSlowPath(this._fiber);
|
|
|
|
if (fiber === null) {
|
|
throw new Error(
|
|
"Can't read from currently-mounting component. This error is likely " +
|
|
'caused by a bug in React. Please file an issue.',
|
|
);
|
|
}
|
|
|
|
return fiber;
|
|
}
|
|
|
|
constructor(fiber: Fiber) {
|
|
if (!validWrapperTypes.has(fiber.tag)) {
|
|
throw new Error(
|
|
`Unexpected object passed to ReactTestInstance constructor (tag: ${fiber.tag}). ` +
|
|
'This is probably a bug in React.',
|
|
);
|
|
}
|
|
|
|
this._fiber = fiber;
|
|
}
|
|
|
|
get instance(): $FlowFixMe {
|
|
const tag = this._fiber.tag;
|
|
if (
|
|
tag === HostComponent ||
|
|
tag === HostHoistable ||
|
|
tag === HostSingleton
|
|
) {
|
|
return getPublicInstance(this._fiber.stateNode);
|
|
} else {
|
|
return this._fiber.stateNode;
|
|
}
|
|
}
|
|
|
|
get type(): any {
|
|
return this._fiber.type;
|
|
}
|
|
|
|
get props(): Object {
|
|
return this._currentFiber().memoizedProps;
|
|
}
|
|
|
|
get parent(): ?ReactTestInstance {
|
|
let parent = this._fiber.return;
|
|
while (parent !== null) {
|
|
if (validWrapperTypes.has(parent.tag)) {
|
|
if (parent.tag === HostRoot) {
|
|
// Special case: we only "materialize" instances for roots
|
|
// if they have more than a single child. So we'll check that now.
|
|
if (getChildren(parent).length < 2) {
|
|
return null;
|
|
}
|
|
}
|
|
return wrapFiber(parent);
|
|
}
|
|
parent = parent.return;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
get children(): Array<ReactTestInstance | string> {
|
|
return getChildren(this._currentFiber());
|
|
}
|
|
|
|
// Custom search functions
|
|
find(predicate: Predicate): ReactTestInstance {
|
|
return expectOne(
|
|
this.findAll(predicate, {deep: false}),
|
|
`matching custom predicate: ${predicate.toString()}`,
|
|
);
|
|
}
|
|
|
|
findByType(type: any): ReactTestInstance {
|
|
return expectOne(
|
|
this.findAllByType(type, {deep: false}),
|
|
`with node type: "${getComponentNameFromType(type) || 'Unknown'}"`,
|
|
);
|
|
}
|
|
|
|
findByProps(props: Object): ReactTestInstance {
|
|
return expectOne(
|
|
this.findAllByProps(props, {deep: false}),
|
|
`with props: ${JSON.stringify(props)}`,
|
|
);
|
|
}
|
|
|
|
findAll(
|
|
predicate: Predicate,
|
|
options: ?FindOptions = null,
|
|
): Array<ReactTestInstance> {
|
|
return findAll(this, predicate, options);
|
|
}
|
|
|
|
findAllByType(
|
|
type: any,
|
|
options: ?FindOptions = null,
|
|
): Array<ReactTestInstance> {
|
|
return findAll(this, node => node.type === type, options);
|
|
}
|
|
|
|
findAllByProps(
|
|
props: Object,
|
|
options: ?FindOptions = null,
|
|
): Array<ReactTestInstance> {
|
|
return findAll(
|
|
this,
|
|
node => node.props && propsMatch(node.props, props),
|
|
options,
|
|
);
|
|
}
|
|
}
|
|
|
|
function findAll(
|
|
root: ReactTestInstance,
|
|
predicate: Predicate,
|
|
options: ?FindOptions,
|
|
): Array<ReactTestInstance> {
|
|
const deep = options ? options.deep : true;
|
|
const results = [];
|
|
|
|
if (predicate(root)) {
|
|
results.push(root);
|
|
if (!deep) {
|
|
return results;
|
|
}
|
|
}
|
|
|
|
root.children.forEach(child => {
|
|
if (typeof child === 'string') {
|
|
return;
|
|
}
|
|
results.push(...findAll(child, predicate, options));
|
|
});
|
|
|
|
return results;
|
|
}
|
|
|
|
function expectOne(
|
|
all: Array<ReactTestInstance>,
|
|
message: string,
|
|
): ReactTestInstance {
|
|
if (all.length === 1) {
|
|
return all[0];
|
|
}
|
|
|
|
const prefix =
|
|
all.length === 0
|
|
? 'No instances found '
|
|
: `Expected 1 but found ${all.length} instances `;
|
|
|
|
throw new Error(prefix + message);
|
|
}
|
|
|
|
function propsMatch(props: Object, filter: Object): boolean {
|
|
for (const key in filter) {
|
|
if (props[key] !== filter[key]) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function create(
|
|
element: React$Element<any>,
|
|
options: TestRendererOptions,
|
|
): {
|
|
_Scheduler: typeof Scheduler,
|
|
root: void,
|
|
toJSON(): Array<ReactTestRendererNode> | ReactTestRendererNode | null,
|
|
toTree(): mixed,
|
|
update(newElement: React$Element<any>): any,
|
|
unmount(): void,
|
|
getInstance(): React$Component<any, any> | PublicInstance | null,
|
|
unstable_flushSync: typeof flushSyncFromReconciler,
|
|
} {
|
|
if (__DEV__) {
|
|
if (
|
|
enableReactTestRendererWarning === true &&
|
|
global.IS_REACT_NATIVE_TEST_ENVIRONMENT !== true
|
|
) {
|
|
console.warn(
|
|
'react-test-renderer is deprecated. See https://react.dev/warnings/react-test-renderer',
|
|
);
|
|
}
|
|
}
|
|
|
|
let createNodeMock = defaultTestOptions.createNodeMock;
|
|
const isConcurrentOnly =
|
|
disableLegacyMode === true &&
|
|
global.IS_REACT_NATIVE_TEST_ENVIRONMENT !== true;
|
|
let isConcurrent = isConcurrentOnly;
|
|
let isStrictMode = false;
|
|
let concurrentUpdatesByDefault = null;
|
|
if (typeof options === 'object' && options !== null) {
|
|
if (typeof options.createNodeMock === 'function') {
|
|
// $FlowFixMe[incompatible-type] found when upgrading Flow
|
|
createNodeMock = options.createNodeMock;
|
|
}
|
|
if (isConcurrentOnly === false) {
|
|
isConcurrent = options.unstable_isConcurrent;
|
|
}
|
|
if (options.unstable_strictMode === true) {
|
|
isStrictMode = true;
|
|
}
|
|
if (allowConcurrentByDefault) {
|
|
if (options.unstable_concurrentUpdatesByDefault !== undefined) {
|
|
concurrentUpdatesByDefault =
|
|
options.unstable_concurrentUpdatesByDefault;
|
|
}
|
|
}
|
|
}
|
|
let container = {
|
|
children: ([]: Array<Instance | TextInstance>),
|
|
createNodeMock,
|
|
tag: 'CONTAINER',
|
|
};
|
|
let root: FiberRoot | null = createContainer(
|
|
container,
|
|
isConcurrent ? ConcurrentRoot : LegacyRoot,
|
|
null,
|
|
isStrictMode,
|
|
concurrentUpdatesByDefault,
|
|
'',
|
|
defaultOnUncaughtError,
|
|
defaultOnCaughtError,
|
|
defaultOnRecoverableError,
|
|
null,
|
|
);
|
|
|
|
if (root == null) {
|
|
throw new Error('something went wrong');
|
|
}
|
|
|
|
updateContainer(element, root, null, null);
|
|
|
|
const entry = {
|
|
_Scheduler: Scheduler,
|
|
|
|
root: undefined, // makes flow happy
|
|
// we define a 'getter' for 'root' below using 'Object.defineProperty'
|
|
toJSON(): Array<ReactTestRendererNode> | ReactTestRendererNode | null {
|
|
if (root == null || root.current == null || container == null) {
|
|
return null;
|
|
}
|
|
if (container.children.length === 0) {
|
|
return null;
|
|
}
|
|
if (container.children.length === 1) {
|
|
return toJSON(container.children[0]);
|
|
}
|
|
if (
|
|
container.children.length === 2 &&
|
|
container.children[0].isHidden === true &&
|
|
container.children[1].isHidden === false
|
|
) {
|
|
// Omit timed out children from output entirely, including the fact that we
|
|
// temporarily wrap fallback and timed out children in an array.
|
|
return toJSON(container.children[1]);
|
|
}
|
|
let renderedChildren = null;
|
|
if (container.children && container.children.length) {
|
|
for (let i = 0; i < container.children.length; i++) {
|
|
const renderedChild = toJSON(container.children[i]);
|
|
if (renderedChild !== null) {
|
|
if (renderedChildren === null) {
|
|
renderedChildren = [renderedChild];
|
|
} else {
|
|
renderedChildren.push(renderedChild);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return renderedChildren;
|
|
},
|
|
toTree() {
|
|
if (root == null || root.current == null) {
|
|
return null;
|
|
}
|
|
return toTree(root.current);
|
|
},
|
|
update(newElement: React$Element<any>): number | void {
|
|
if (root == null || root.current == null) {
|
|
return;
|
|
}
|
|
updateContainer(newElement, root, null, null);
|
|
},
|
|
unmount() {
|
|
if (root == null || root.current == null) {
|
|
return;
|
|
}
|
|
updateContainer(null, root, null, null);
|
|
// $FlowFixMe[incompatible-type] found when upgrading Flow
|
|
container = null;
|
|
root = null;
|
|
},
|
|
getInstance() {
|
|
if (root == null || root.current == null) {
|
|
return null;
|
|
}
|
|
return getPublicRootInstance(root);
|
|
},
|
|
|
|
unstable_flushSync: flushSyncFromReconciler,
|
|
};
|
|
|
|
Object.defineProperty(
|
|
entry,
|
|
'root',
|
|
({
|
|
configurable: true,
|
|
enumerable: true,
|
|
get: function () {
|
|
if (root === null) {
|
|
throw new Error("Can't access .root on unmounted test renderer");
|
|
}
|
|
const children = getChildren(root.current);
|
|
if (children.length === 0) {
|
|
throw new Error("Can't access .root on unmounted test renderer");
|
|
} else if (children.length === 1) {
|
|
// Normally, we skip the root and just give you the child.
|
|
return children[0];
|
|
} else {
|
|
// However, we give you the root if there's more than one root child.
|
|
// We could make this the behavior for all cases but it would be a breaking change.
|
|
// $FlowFixMe[incompatible-use] found when upgrading Flow
|
|
return wrapFiber(root.current);
|
|
}
|
|
},
|
|
}: Object),
|
|
);
|
|
|
|
return entry;
|
|
}
|
|
|
|
const fiberToWrapper = new WeakMap<Fiber, ReactTestInstance>();
|
|
function wrapFiber(fiber: Fiber): ReactTestInstance {
|
|
let wrapper = fiberToWrapper.get(fiber);
|
|
if (wrapper === undefined && fiber.alternate !== null) {
|
|
wrapper = fiberToWrapper.get(fiber.alternate);
|
|
}
|
|
if (wrapper === undefined) {
|
|
wrapper = new ReactTestInstance(fiber);
|
|
fiberToWrapper.set(fiber, wrapper);
|
|
}
|
|
return wrapper;
|
|
}
|
|
|
|
// Enable ReactTestRenderer to be used to test DevTools integration.
|
|
injectIntoDevTools({
|
|
findFiberByHostInstance: (() => {
|
|
throw new Error('TestRenderer does not support findFiberByHostInstance()');
|
|
}: any),
|
|
bundleType: __DEV__ ? 1 : 0,
|
|
version: ReactVersion,
|
|
rendererPackageName: 'react-test-renderer',
|
|
});
|
|
|
|
export {
|
|
Scheduler as _Scheduler,
|
|
create,
|
|
/* eslint-disable-next-line camelcase */
|
|
batchedUpdates as unstable_batchedUpdates,
|
|
act,
|
|
};
|