Skip to content

Commit

Permalink
Add toTree() method to stack and fiber TestRenderer (#8931)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
lelandrichardson authored and gaearon committed Feb 9, 2017
1 parent 3f48caa commit 869c779
Show file tree
Hide file tree
Showing 5 changed files with 285 additions and 6 deletions.
3 changes: 3 additions & 0 deletions scripts/fiber/tests-passing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -702,7 +702,7 @@ var ReactCompositeComponent = {
} else {
if (__DEV__) {
const componentName = this.getName();

if (!warningAboutMissingGetChildContext[componentName]) {
warningAboutMissingGetChildContext[componentName] = true;
warning(
Expand Down
80 changes: 75 additions & 5 deletions src/renderers/testing/ReactTestRendererFiber.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand All @@ -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) {
Expand All @@ -264,22 +328,28 @@ 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);
container = null;
root = null;
},
getInstance() {
if (root == null) {
if (root == null || root.current == null) {
return null;
}
return TestRenderer.getPublicRootInstance(root);
Expand Down
170 changes: 170 additions & 0 deletions src/renderers/testing/__tests__/ReactTestRenderer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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']);
Expand Down
36 changes: 36 additions & 0 deletions src/renderers/testing/stack/ReactTestMount.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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.
Expand Down

0 comments on commit 869c779

Please sign in to comment.