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:
parent
3f48caab40
commit
869c779861
|
@ -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
|
||||
|
||||
|
|
|
@ -702,7 +702,7 @@ var ReactCompositeComponent = {
|
|||
} else {
|
||||
if (__DEV__) {
|
||||
const componentName = this.getName();
|
||||
|
||||
|
||||
if (!warningAboutMissingGetChildContext[componentName]) {
|
||||
warningAboutMissingGetChildContext[componentName] = true;
|
||||
warning(
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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']);
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue