Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions scripts/fiber/tests-passing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1725,6 +1725,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');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you confirm that this works in the npm build? I.e. grunt build puts this file in the npm package. I've had trouble in RN when this isn't reachable from within the npm build?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry.... can you explain what you want me to try here? just run npm run build?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please verify this file actually ends up in build/packages/react-test-renderer after you've run npm run build. Whether it does or not depends on where ReactTypeOfWork is located. Explanation for our folder structure: https://facebook.github.io/react/contributing/codebase-overview.html#shared-code.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In other words, after npm run build try cd-ing into build/packages/react-test-renderer and verify you can require() it without crashing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've confirmed that build/packages/react-test-renderer/lib/ReactTypeOfWork.js exists

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