Add toTree() method to stack and fiber TestRenderer (#8931)

* Add toTree() method to stack and fiber TestRenderer

* Address PR feedback

* Refactor TestRenderer to use correct root

* Rebase off master and fix root node references

* Add flow types

* Add test for null rendering components

* Remove last remaining lint error

* Add missing test
This commit is contained in:
Leland Richardson 2017-02-09 12:55:00 -08:00 committed by Dan Abramov
parent 3f48caab40
commit 869c779861
5 changed files with 285 additions and 6 deletions

View File

@ -1686,6 +1686,9 @@ src/renderers/testing/__tests__/ReactTestRenderer-test.js
* supports updates when using refs
* supports error boundaries
* can update text nodes
* toTree() renders simple components returning host components
* toTree() handles null rendering components
* toTree() renders complicated trees of composites and hosts
* can update text nodes when rendered as root
* can render and update root fragments

View File

@ -702,7 +702,7 @@ var ReactCompositeComponent = {
} else {
if (__DEV__) {
const componentName = this.getName();
if (!warningAboutMissingGetChildContext[componentName]) {
warningAboutMissingGetChildContext[componentName] = true;
warning(

View File

@ -16,8 +16,19 @@
var ReactFiberReconciler = require('ReactFiberReconciler');
var ReactGenericBatching = require('ReactGenericBatching');
var emptyObject = require('emptyObject');
var ReactTypeOfWork = require('ReactTypeOfWork');
var invariant = require('invariant');
var {
FunctionalComponent,
ClassComponent,
HostComponent,
HostText,
HostRoot,
} = ReactTypeOfWork;
import type { TestRendererOptions } from 'ReactTestMount';
import type { Fiber } from 'ReactFiber';
import type { FiberRoot } from 'ReactFiberRoot';
type ReactTestRendererJSON = {|
type : string,
@ -237,6 +248,58 @@ function toJSON(inst : Instance | TextInstance) : ReactTestRendererNode {
}
}
function nodeAndSiblingsArray(nodeWithSibling: ?Fiber) {
var array = [];
var node = nodeWithSibling;
while (node != null) {
array.push(node);
node = node.sibling;
}
return array;
}
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: toTree(node.child),
};
case FunctionalComponent: // 1
return {
nodeType: 'component',
type: node.type,
props: { ...node.memoizedProps },
instance: null,
rendered: toTree(node.child),
};
case HostComponent: // 5
return {
nodeType: 'host',
type: node.type,
props: { ...node.memoizedProps },
instance: null, // TODO: use createNodeMock here somehow?
rendered: nodeAndSiblingsArray(node.child).map(toTree),
};
case HostText: // 6
return node.stateNode.text;
default:
invariant(
false,
'toTree() does not yet know how to handle nodes with tag=%s',
node.tag
);
}
}
var ReactTestFiberRenderer = {
create(element : ReactElement<any>, options : TestRendererOptions) {
var createNodeMock = defaultTestOptions.createNodeMock;
@ -248,12 +311,13 @@ var ReactTestFiberRenderer = {
createNodeMock,
tag: 'CONTAINER',
};
var root = TestRenderer.createContainer(container);
var root: ?FiberRoot = TestRenderer.createContainer(container);
invariant(root != null, 'something went wrong');
TestRenderer.updateContainer(element, root, null, null);
return {
toJSON() {
if (root == null || container == null) {
if (root == null || root.current == null || container == null) {
return null;
}
if (container.children.length === 0) {
@ -264,14 +328,20 @@ var ReactTestFiberRenderer = {
}
return container.children.map(toJSON);
},
toTree() {
if (root == null || root.current == null) {
return null;
}
return toTree(root.current);
},
update(newElement : ReactElement<any>) {
if (root == null) {
if (root == null || root.current == null) {
return;
}
TestRenderer.updateContainer(newElement, root, null, null);
},
unmount() {
if (root == null) {
if (root == null || root.current == null) {
return;
}
TestRenderer.updateContainer(null, root, null);
@ -279,7 +349,7 @@ var ReactTestFiberRenderer = {
root = null;
},
getInstance() {
if (root == null) {
if (root == null || root.current == null) {
return null;
}
return TestRenderer.getPublicRootInstance(root);

View File

@ -14,8 +14,32 @@
var React = require('React');
var ReactTestRenderer = require('ReactTestRenderer');
var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
var prettyFormat = require('pretty-format');
var ReactFeatureFlags;
// Kind of hacky, but we nullify all the instances to test the tree structure
// with jasmine's deep equality function, and test the instances separate. We
// also delete children props because testing them is more annoying and not
// really important to verify.
function cleanNode(node) {
if (!node) {
return;
}
if (node && node.instance) {
node.instance = null;
}
if (node && node.props && node.props.children) {
// eslint-disable-next-line no-unused-vars
var { children, ...props } = node.props;
node.props = props;
}
if (Array.isArray(node.rendered)) {
node.rendered.forEach(cleanNode);
} else if (typeof node.rendered === 'object') {
cleanNode(node.rendered);
}
}
describe('ReactTestRenderer', () => {
beforeEach(() => {
ReactFeatureFlags = require('ReactFeatureFlags');
@ -517,6 +541,152 @@ describe('ReactTestRenderer', () => {
});
});
it('toTree() renders simple components returning host components', () => {
var Qoo = () => (
<span className="Qoo">Hello World!</span>
);
var renderer = ReactTestRenderer.create(<Qoo />);
var tree = renderer.toTree();
cleanNode(tree);
expect(prettyFormat(tree)).toEqual(prettyFormat({
nodeType: 'component',
type: Qoo,
props: {},
instance: null,
rendered: {
nodeType: 'host',
type: 'span',
props: { className: 'Qoo' },
instance: null,
rendered: ['Hello World!'],
},
}));
});
it('toTree() handles null rendering components', () => {
class Foo extends React.Component {
render() {
return null;
}
}
var renderer = ReactTestRenderer.create(<Foo />);
var tree = renderer.toTree();
expect(tree.instance).toBeInstanceOf(Foo);
cleanNode(tree);
expect(tree).toEqual({
type: Foo,
nodeType: 'component',
props: { },
instance: null,
rendered: null,
});
});
it('toTree() renders complicated trees of composites and hosts', () => {
// SFC returning host. no children props.
var Qoo = () => (
<span className="Qoo">Hello World!</span>
);
// SFC returning host. passes through children.
var Foo = ({ className, children }) => (
<div className={'Foo ' + className}>
<span className="Foo2">Literal</span>
{children}
</div>
);
// class composite returning composite. passes through children.
class Bar extends React.Component {
render() {
const { special, children } = this.props;
return (
<Foo className={special ? 'special' : 'normal'}>
{children}
</Foo>
);
}
}
// class composite return composite. no children props.
class Bam extends React.Component {
render() {
return (
<Bar special={true}>
<Qoo />
</Bar>
);
}
}
var renderer = ReactTestRenderer.create(<Bam />);
var tree = renderer.toTree();
// we test for the presence of instances before nulling them out
expect(tree.instance).toBeInstanceOf(Bam);
expect(tree.rendered.instance).toBeInstanceOf(Bar);
cleanNode(tree);
expect(prettyFormat(tree)).toEqual(prettyFormat({
type: Bam,
nodeType: 'component',
props: {},
instance: null,
rendered: {
type: Bar,
nodeType: 'component',
props: { special: true },
instance: null,
rendered: {
type: Foo,
nodeType: 'component',
props: { className: 'special' },
instance: null,
rendered: {
type: 'div',
nodeType: 'host',
props: { className: 'Foo special' },
instance: null,
rendered: [
{
type: 'span',
nodeType: 'host',
props: { className: 'Foo2' },
instance: null,
rendered: ['Literal'],
},
{
type: Qoo,
nodeType: 'component',
props: {},
instance: null,
rendered: {
type: 'span',
nodeType: 'host',
props: { className: 'Qoo' },
instance: null,
rendered: ['Hello World!'],
},
},
],
},
},
},
}));
});
if (ReactDOMFeatureFlags.useFiber) {
it('can update text nodes when rendered as root', () => {
var renderer = ReactTestRenderer.create(['Hello', 'world']);

View File

@ -138,6 +138,9 @@ ReactTestInstance.prototype.unmount = function(nextElement) {
});
this._component = null;
};
ReactTestInstance.prototype.toTree = function() {
return toTree(this._component._renderedComponent);
};
ReactTestInstance.prototype.toJSON = function() {
var inst = getHostComponentFromComposite(this._component);
if (inst === null) {
@ -146,6 +149,39 @@ ReactTestInstance.prototype.toJSON = function() {
return inst.toJSON();
};
function toTree(component) {
var element = component._currentElement;
if (!React.isValidElement(element)) {
return element;
}
if (!component._renderedComponent) {
var rendered = [];
for (var key in component._renderedChildren) {
var inst = component._renderedChildren[key];
var json = toTree(inst);
if (json !== undefined) {
rendered.push(json);
}
}
return {
nodeType: 'host',
type: element.type,
props: { ...element.props },
instance: component._nodeMock,
rendered: rendered,
};
} else {
return {
nodeType: 'component',
type: element.type,
props: { ...element.props },
instance: component._instance,
rendered: toTree(component._renderedComponent),
};
}
}
/**
* As soon as `ReactMount` is refactored to not rely on the DOM, we can share
* code between the two. For now, we'll hard code the ID logic.