Files
react/packages/react-test-renderer/src/ReactTestRenderer.js
T
2017-12-01 00:01:45 +00:00

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;