mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
646 lines
15 KiB
JavaScript
646 lines
15 KiB
JavaScript
/**
|
|
* Copyright (c) 2013-present, Facebook, Inc.
|
|
*
|
|
* 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} from 'react-reconciler/src/ReactFiber';
|
|
import type {FiberRoot} from 'react-reconciler/src/ReactFiberRoot';
|
|
|
|
import ReactFiberReconciler from 'react-reconciler';
|
|
import {batchedUpdates} from 'events/ReactGenericBatching';
|
|
import {findCurrentFiberUsingSlowPath} from 'react-reconciler/reflection';
|
|
import emptyObject from 'fbjs/lib/emptyObject';
|
|
import {
|
|
Fragment,
|
|
FunctionalComponent,
|
|
ClassComponent,
|
|
HostComponent,
|
|
HostText,
|
|
HostRoot,
|
|
} from 'shared/ReactTypeOfWork';
|
|
import invariant from 'fbjs/lib/invariant';
|
|
|
|
type TestRendererOptions = {
|
|
createNodeMock: (element: React$Element<any>) => any,
|
|
};
|
|
|
|
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 Container = {|
|
|
children: Array<Instance | TextInstance>,
|
|
createNodeMock: Function,
|
|
tag: 'CONTAINER',
|
|
|};
|
|
|
|
type Props = Object;
|
|
type Instance = {|
|
|
type: string,
|
|
props: Object,
|
|
children: Array<Instance | TextInstance>,
|
|
rootContainerInstance: Container,
|
|
tag: 'INSTANCE',
|
|
|};
|
|
|
|
type TextInstance = {|
|
|
text: string,
|
|
tag: 'TEXT',
|
|
|};
|
|
|
|
type FindOptions = $Shape<{
|
|
// 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 UPDATE_SIGNAL = {};
|
|
|
|
function getPublicInstance(inst: Instance | TextInstance): * {
|
|
switch (inst.tag) {
|
|
case 'INSTANCE':
|
|
const createNodeMock = inst.rootContainerInstance.createNodeMock;
|
|
return createNodeMock({
|
|
type: inst.type,
|
|
props: inst.props,
|
|
});
|
|
default:
|
|
return inst;
|
|
}
|
|
}
|
|
|
|
function appendChild(
|
|
parentInstance: Instance | Container,
|
|
child: Instance | TextInstance,
|
|
): void {
|
|
const index = parentInstance.children.indexOf(child);
|
|
if (index !== -1) {
|
|
parentInstance.children.splice(index, 1);
|
|
}
|
|
parentInstance.children.push(child);
|
|
}
|
|
|
|
function insertBefore(
|
|
parentInstance: Instance | Container,
|
|
child: Instance | TextInstance,
|
|
beforeChild: Instance | TextInstance,
|
|
): void {
|
|
const index = parentInstance.children.indexOf(child);
|
|
if (index !== -1) {
|
|
parentInstance.children.splice(index, 1);
|
|
}
|
|
const beforeIndex = parentInstance.children.indexOf(beforeChild);
|
|
parentInstance.children.splice(beforeIndex, 0, child);
|
|
}
|
|
|
|
function removeChild(
|
|
parentInstance: Instance | Container,
|
|
child: Instance | TextInstance,
|
|
): void {
|
|
const index = parentInstance.children.indexOf(child);
|
|
parentInstance.children.splice(index, 1);
|
|
}
|
|
|
|
const TestRenderer = ReactFiberReconciler({
|
|
getRootHostContext() {
|
|
return emptyObject;
|
|
},
|
|
|
|
getChildHostContext() {
|
|
return emptyObject;
|
|
},
|
|
|
|
prepareForCommit(): void {
|
|
// noop
|
|
},
|
|
|
|
resetAfterCommit(): void {
|
|
// noop
|
|
},
|
|
|
|
createInstance(
|
|
type: string,
|
|
props: Props,
|
|
rootContainerInstance: Container,
|
|
hostContext: Object,
|
|
internalInstanceHandle: Object,
|
|
): Instance {
|
|
return {
|
|
type,
|
|
props,
|
|
children: [],
|
|
rootContainerInstance,
|
|
tag: 'INSTANCE',
|
|
};
|
|
},
|
|
|
|
appendInitialChild(
|
|
parentInstance: Instance,
|
|
child: Instance | TextInstance,
|
|
): void {
|
|
const index = parentInstance.children.indexOf(child);
|
|
if (index !== -1) {
|
|
parentInstance.children.splice(index, 1);
|
|
}
|
|
parentInstance.children.push(child);
|
|
},
|
|
|
|
finalizeInitialChildren(
|
|
testElement: Instance,
|
|
type: string,
|
|
props: Props,
|
|
rootContainerInstance: Container,
|
|
): boolean {
|
|
return false;
|
|
},
|
|
|
|
prepareUpdate(
|
|
testElement: Instance,
|
|
type: string,
|
|
oldProps: Props,
|
|
newProps: Props,
|
|
rootContainerInstance: Container,
|
|
hostContext: Object,
|
|
): null | {} {
|
|
return UPDATE_SIGNAL;
|
|
},
|
|
|
|
shouldSetTextContent(type: string, props: Props): boolean {
|
|
return false;
|
|
},
|
|
|
|
shouldDeprioritizeSubtree(type: string, props: Props): boolean {
|
|
return false;
|
|
},
|
|
|
|
createTextInstance(
|
|
text: string,
|
|
rootContainerInstance: Container,
|
|
hostContext: Object,
|
|
internalInstanceHandle: Object,
|
|
): TextInstance {
|
|
return {
|
|
text,
|
|
tag: 'TEXT',
|
|
};
|
|
},
|
|
|
|
scheduleDeferredCallback(fn: Function): number {
|
|
return setTimeout(fn, 0, {timeRemaining: Infinity});
|
|
},
|
|
|
|
cancelDeferredCallback(timeoutID: number): void {
|
|
clearTimeout(timeoutID);
|
|
},
|
|
|
|
useSyncScheduling: true,
|
|
|
|
getPublicInstance,
|
|
|
|
now(): number {
|
|
// Test renderer does not use expiration
|
|
return 0;
|
|
},
|
|
|
|
mutation: {
|
|
commitUpdate(
|
|
instance: Instance,
|
|
updatePayload: {},
|
|
type: string,
|
|
oldProps: Props,
|
|
newProps: Props,
|
|
internalInstanceHandle: Object,
|
|
): void {
|
|
instance.type = type;
|
|
instance.props = newProps;
|
|
},
|
|
|
|
commitMount(
|
|
instance: Instance,
|
|
type: string,
|
|
newProps: Props,
|
|
internalInstanceHandle: Object,
|
|
): void {
|
|
// noop
|
|
},
|
|
|
|
commitTextUpdate(
|
|
textInstance: TextInstance,
|
|
oldText: string,
|
|
newText: string,
|
|
): void {
|
|
textInstance.text = newText;
|
|
},
|
|
resetTextContent(testElement: Instance): void {
|
|
// noop
|
|
},
|
|
|
|
appendChild: appendChild,
|
|
appendChildToContainer: appendChild,
|
|
insertBefore: insertBefore,
|
|
insertInContainerBefore: insertBefore,
|
|
removeChild: removeChild,
|
|
removeChildFromContainer: removeChild,
|
|
},
|
|
});
|
|
|
|
const defaultTestOptions = {
|
|
createNodeMock: function() {
|
|
return null;
|
|
},
|
|
};
|
|
|
|
function toJSON(inst: Instance | TextInstance): ReactTestRendererNode {
|
|
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) {
|
|
renderedChildren = inst.children.map(toJSON);
|
|
}
|
|
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 nodeAndSiblingsTrees(nodeWithSibling: ?Fiber) {
|
|
const array = [];
|
|
let node = nodeWithSibling;
|
|
while (node != null) {
|
|
array.push(node);
|
|
node = node.sibling;
|
|
}
|
|
const trees = array.map(toTree);
|
|
return trees.length ? trees : null;
|
|
}
|
|
|
|
function hasSiblings(node: ?Fiber) {
|
|
return node && node.sibling;
|
|
}
|
|
|
|
function toTree(node: ?Fiber) {
|
|
if (node == null) {
|
|
return null;
|
|
}
|
|
switch (node.tag) {
|
|
case HostRoot: // 3
|
|
return toTree(node.child);
|
|
case ClassComponent:
|
|
return {
|
|
nodeType: 'component',
|
|
type: node.type,
|
|
props: {...node.memoizedProps},
|
|
instance: node.stateNode,
|
|
rendered: hasSiblings(node.child)
|
|
? nodeAndSiblingsTrees(node.child)
|
|
: toTree(node.child),
|
|
};
|
|
case FunctionalComponent: // 1
|
|
return {
|
|
nodeType: 'component',
|
|
type: node.type,
|
|
props: {...node.memoizedProps},
|
|
instance: null,
|
|
rendered: hasSiblings(node.child)
|
|
? nodeAndSiblingsTrees(node.child)
|
|
: toTree(node.child),
|
|
};
|
|
case HostComponent: // 5
|
|
return {
|
|
nodeType: 'host',
|
|
type: node.type,
|
|
props: {...node.memoizedProps},
|
|
instance: null, // TODO: use createNodeMock here somehow?
|
|
rendered: nodeAndSiblingsTrees(node.child),
|
|
};
|
|
case HostText: // 6
|
|
return node.stateNode.text;
|
|
default:
|
|
invariant(
|
|
false,
|
|
'toTree() does not yet know how to handle nodes with tag=%s',
|
|
node.tag,
|
|
);
|
|
}
|
|
}
|
|
|
|
const fiberToWrapper = new WeakMap();
|
|
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;
|
|
}
|
|
|
|
const validWrapperTypes = new Set([
|
|
FunctionalComponent,
|
|
ClassComponent,
|
|
HostComponent,
|
|
]);
|
|
|
|
class ReactTestInstance {
|
|
_fiber: Fiber;
|
|
|
|
_currentFiber(): Fiber {
|
|
// Throws if this component has been unmounted.
|
|
const fiber = findCurrentFiberUsingSlowPath(this._fiber);
|
|
invariant(
|
|
fiber !== null,
|
|
"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) {
|
|
invariant(
|
|
validWrapperTypes.has(fiber.tag),
|
|
'Unexpected object passed to ReactTestInstance constructor (tag: %s). ' +
|
|
'This is probably a bug in React.',
|
|
fiber.tag,
|
|
);
|
|
this._fiber = fiber;
|
|
}
|
|
|
|
get instance() {
|
|
if (this._fiber.tag === HostComponent) {
|
|
return getPublicInstance(this._fiber.stateNode);
|
|
} else {
|
|
return this._fiber.stateNode;
|
|
}
|
|
}
|
|
|
|
get type() {
|
|
return this._fiber.type;
|
|
}
|
|
|
|
get props(): Object {
|
|
return this._currentFiber().memoizedProps;
|
|
}
|
|
|
|
get parent(): ?ReactTestInstance {
|
|
const parent = this._fiber.return;
|
|
return parent === null || parent.tag === HostRoot
|
|
? null
|
|
: wrapFiber(parent);
|
|
}
|
|
|
|
get children(): Array<ReactTestInstance | string> {
|
|
const children = [];
|
|
const startingNode = this._currentFiber();
|
|
let node: Fiber = startingNode;
|
|
if (node.child === null) {
|
|
return children;
|
|
}
|
|
node.child.return = node;
|
|
node = node.child;
|
|
outer: while (true) {
|
|
let descend = false;
|
|
switch (node.tag) {
|
|
case FunctionalComponent:
|
|
case ClassComponent:
|
|
case HostComponent:
|
|
children.push(wrapFiber(node));
|
|
break;
|
|
case HostText:
|
|
children.push('' + node.memoizedProps);
|
|
break;
|
|
case Fragment:
|
|
descend = true;
|
|
break;
|
|
default:
|
|
invariant(
|
|
false,
|
|
'Unsupported component type %s in test renderer. ' +
|
|
'This is probably a bug in React.',
|
|
node.tag,
|
|
);
|
|
}
|
|
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;
|
|
}
|
|
|
|
// 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: "${type.displayName || type.name}"`,
|
|
);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
for (const child of root.children) {
|
|
if (typeof child === 'string') {
|
|
continue;
|
|
}
|
|
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;
|
|
}
|
|
|
|
const ReactTestRendererFiber = {
|
|
create(element: React$Element<any>, options: TestRendererOptions) {
|
|
let createNodeMock = defaultTestOptions.createNodeMock;
|
|
if (options && typeof options.createNodeMock === 'function') {
|
|
createNodeMock = options.createNodeMock;
|
|
}
|
|
let container = {
|
|
children: [],
|
|
createNodeMock,
|
|
tag: 'CONTAINER',
|
|
};
|
|
let root: FiberRoot | null = TestRenderer.createContainer(container, false);
|
|
invariant(root != null, 'something went wrong');
|
|
TestRenderer.updateContainer(element, root, null, null);
|
|
|
|
const entry = {
|
|
root: undefined, // makes flow happy
|
|
// we define a 'getter' for 'root' below using 'Object.defineProperty'
|
|
toJSON() {
|
|
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]);
|
|
}
|
|
return container.children.map(toJSON);
|
|
},
|
|
toTree() {
|
|
if (root == null || root.current == null) {
|
|
return null;
|
|
}
|
|
return toTree(root.current);
|
|
},
|
|
update(newElement: React$Element<any>) {
|
|
if (root == null || root.current == null) {
|
|
return;
|
|
}
|
|
TestRenderer.updateContainer(newElement, root, null, null);
|
|
},
|
|
unmount() {
|
|
if (root == null || root.current == null) {
|
|
return;
|
|
}
|
|
TestRenderer.updateContainer(null, root, null, null);
|
|
container = null;
|
|
root = null;
|
|
},
|
|
getInstance() {
|
|
if (root == null || root.current == null) {
|
|
return null;
|
|
}
|
|
return TestRenderer.getPublicRootInstance(root);
|
|
},
|
|
};
|
|
|
|
Object.defineProperty(
|
|
entry,
|
|
'root',
|
|
({
|
|
configurable: true,
|
|
enumerable: true,
|
|
get: function() {
|
|
if (root === null || root.current.child === null) {
|
|
throw new Error("Can't access .root on unmounted test renderer");
|
|
}
|
|
return wrapFiber(root.current.child);
|
|
},
|
|
}: Object),
|
|
);
|
|
|
|
return entry;
|
|
},
|
|
|
|
/* eslint-disable camelcase */
|
|
unstable_batchedUpdates: batchedUpdates,
|
|
/* eslint-enable camelcase */
|
|
};
|
|
|
|
export default ReactTestRendererFiber;
|