Skip to content

Commit 869c779

Browse files
lelandrichardsongaearon
authored andcommitted
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
1 parent 3f48caa commit 869c779

File tree

5 files changed

+285
-6
lines changed

5 files changed

+285
-6
lines changed

scripts/fiber/tests-passing.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1686,6 +1686,9 @@ src/renderers/testing/__tests__/ReactTestRenderer-test.js
16861686
* supports updates when using refs
16871687
* supports error boundaries
16881688
* can update text nodes
1689+
* toTree() renders simple components returning host components
1690+
* toTree() handles null rendering components
1691+
* toTree() renders complicated trees of composites and hosts
16891692
* can update text nodes when rendered as root
16901693
* can render and update root fragments
16911694

src/renderers/shared/stack/reconciler/ReactCompositeComponent.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -702,7 +702,7 @@ var ReactCompositeComponent = {
702702
} else {
703703
if (__DEV__) {
704704
const componentName = this.getName();
705-
705+
706706
if (!warningAboutMissingGetChildContext[componentName]) {
707707
warningAboutMissingGetChildContext[componentName] = true;
708708
warning(

src/renderers/testing/ReactTestRendererFiber.js

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,19 @@
1616
var ReactFiberReconciler = require('ReactFiberReconciler');
1717
var ReactGenericBatching = require('ReactGenericBatching');
1818
var emptyObject = require('emptyObject');
19+
var ReactTypeOfWork = require('ReactTypeOfWork');
20+
var invariant = require('invariant');
21+
var {
22+
FunctionalComponent,
23+
ClassComponent,
24+
HostComponent,
25+
HostText,
26+
HostRoot,
27+
} = ReactTypeOfWork;
1928

2029
import type { TestRendererOptions } from 'ReactTestMount';
30+
import type { Fiber } from 'ReactFiber';
31+
import type { FiberRoot } from 'ReactFiberRoot';
2132

2233
type ReactTestRendererJSON = {|
2334
type : string,
@@ -237,6 +248,58 @@ function toJSON(inst : Instance | TextInstance) : ReactTestRendererNode {
237248
}
238249
}
239250

251+
function nodeAndSiblingsArray(nodeWithSibling: ?Fiber) {
252+
var array = [];
253+
var node = nodeWithSibling;
254+
while (node != null) {
255+
array.push(node);
256+
node = node.sibling;
257+
}
258+
return array;
259+
}
260+
261+
function toTree(node: ?Fiber) {
262+
if (node == null) {
263+
return null;
264+
}
265+
switch (node.tag) {
266+
case HostRoot: // 3
267+
return toTree(node.child);
268+
case ClassComponent:
269+
return {
270+
nodeType: 'component',
271+
type: node.type,
272+
props: { ...node.memoizedProps },
273+
instance: node.stateNode,
274+
rendered: toTree(node.child),
275+
};
276+
case FunctionalComponent: // 1
277+
return {
278+
nodeType: 'component',
279+
type: node.type,
280+
props: { ...node.memoizedProps },
281+
instance: null,
282+
rendered: toTree(node.child),
283+
};
284+
case HostComponent: // 5
285+
return {
286+
nodeType: 'host',
287+
type: node.type,
288+
props: { ...node.memoizedProps },
289+
instance: null, // TODO: use createNodeMock here somehow?
290+
rendered: nodeAndSiblingsArray(node.child).map(toTree),
291+
};
292+
case HostText: // 6
293+
return node.stateNode.text;
294+
default:
295+
invariant(
296+
false,
297+
'toTree() does not yet know how to handle nodes with tag=%s',
298+
node.tag
299+
);
300+
}
301+
}
302+
240303
var ReactTestFiberRenderer = {
241304
create(element : ReactElement<any>, options : TestRendererOptions) {
242305
var createNodeMock = defaultTestOptions.createNodeMock;
@@ -248,12 +311,13 @@ var ReactTestFiberRenderer = {
248311
createNodeMock,
249312
tag: 'CONTAINER',
250313
};
251-
var root = TestRenderer.createContainer(container);
314+
var root: ?FiberRoot = TestRenderer.createContainer(container);
315+
invariant(root != null, 'something went wrong');
252316
TestRenderer.updateContainer(element, root, null, null);
253317

254318
return {
255319
toJSON() {
256-
if (root == null || container == null) {
320+
if (root == null || root.current == null || container == null) {
257321
return null;
258322
}
259323
if (container.children.length === 0) {
@@ -264,22 +328,28 @@ var ReactTestFiberRenderer = {
264328
}
265329
return container.children.map(toJSON);
266330
},
331+
toTree() {
332+
if (root == null || root.current == null) {
333+
return null;
334+
}
335+
return toTree(root.current);
336+
},
267337
update(newElement : ReactElement<any>) {
268-
if (root == null) {
338+
if (root == null || root.current == null) {
269339
return;
270340
}
271341
TestRenderer.updateContainer(newElement, root, null, null);
272342
},
273343
unmount() {
274-
if (root == null) {
344+
if (root == null || root.current == null) {
275345
return;
276346
}
277347
TestRenderer.updateContainer(null, root, null);
278348
container = null;
279349
root = null;
280350
},
281351
getInstance() {
282-
if (root == null) {
352+
if (root == null || root.current == null) {
283353
return null;
284354
}
285355
return TestRenderer.getPublicRootInstance(root);

src/renderers/testing/__tests__/ReactTestRenderer-test.js

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,32 @@
1414
var React = require('React');
1515
var ReactTestRenderer = require('ReactTestRenderer');
1616
var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
17+
var prettyFormat = require('pretty-format');
1718
var ReactFeatureFlags;
1819

20+
// Kind of hacky, but we nullify all the instances to test the tree structure
21+
// with jasmine's deep equality function, and test the instances separate. We
22+
// also delete children props because testing them is more annoying and not
23+
// really important to verify.
24+
function cleanNode(node) {
25+
if (!node) {
26+
return;
27+
}
28+
if (node && node.instance) {
29+
node.instance = null;
30+
}
31+
if (node && node.props && node.props.children) {
32+
// eslint-disable-next-line no-unused-vars
33+
var { children, ...props } = node.props;
34+
node.props = props;
35+
}
36+
if (Array.isArray(node.rendered)) {
37+
node.rendered.forEach(cleanNode);
38+
} else if (typeof node.rendered === 'object') {
39+
cleanNode(node.rendered);
40+
}
41+
}
42+
1943
describe('ReactTestRenderer', () => {
2044
beforeEach(() => {
2145
ReactFeatureFlags = require('ReactFeatureFlags');
@@ -517,6 +541,152 @@ describe('ReactTestRenderer', () => {
517541
});
518542
});
519543

544+
it('toTree() renders simple components returning host components', () => {
545+
546+
var Qoo = () => (
547+
<span className="Qoo">Hello World!</span>
548+
);
549+
550+
var renderer = ReactTestRenderer.create(<Qoo />);
551+
var tree = renderer.toTree();
552+
553+
cleanNode(tree);
554+
555+
expect(prettyFormat(tree)).toEqual(prettyFormat({
556+
nodeType: 'component',
557+
type: Qoo,
558+
props: {},
559+
instance: null,
560+
rendered: {
561+
nodeType: 'host',
562+
type: 'span',
563+
props: { className: 'Qoo' },
564+
instance: null,
565+
rendered: ['Hello World!'],
566+
},
567+
}));
568+
569+
});
570+
571+
it('toTree() handles null rendering components', () => {
572+
class Foo extends React.Component {
573+
render() {
574+
return null;
575+
}
576+
}
577+
578+
var renderer = ReactTestRenderer.create(<Foo />);
579+
var tree = renderer.toTree();
580+
581+
expect(tree.instance).toBeInstanceOf(Foo);
582+
583+
cleanNode(tree);
584+
585+
expect(tree).toEqual({
586+
type: Foo,
587+
nodeType: 'component',
588+
props: { },
589+
instance: null,
590+
rendered: null,
591+
});
592+
593+
});
594+
595+
it('toTree() renders complicated trees of composites and hosts', () => {
596+
// SFC returning host. no children props.
597+
var Qoo = () => (
598+
<span className="Qoo">Hello World!</span>
599+
);
600+
601+
// SFC returning host. passes through children.
602+
var Foo = ({ className, children }) => (
603+
<div className={'Foo ' + className}>
604+
<span className="Foo2">Literal</span>
605+
{children}
606+
</div>
607+
);
608+
609+
// class composite returning composite. passes through children.
610+
class Bar extends React.Component {
611+
render() {
612+
const { special, children } = this.props;
613+
return (
614+
<Foo className={special ? 'special' : 'normal'}>
615+
{children}
616+
</Foo>
617+
);
618+
}
619+
}
620+
621+
// class composite return composite. no children props.
622+
class Bam extends React.Component {
623+
render() {
624+
return (
625+
<Bar special={true}>
626+
<Qoo />
627+
</Bar>
628+
);
629+
}
630+
}
631+
632+
var renderer = ReactTestRenderer.create(<Bam />);
633+
var tree = renderer.toTree();
634+
635+
// we test for the presence of instances before nulling them out
636+
expect(tree.instance).toBeInstanceOf(Bam);
637+
expect(tree.rendered.instance).toBeInstanceOf(Bar);
638+
639+
cleanNode(tree);
640+
641+
expect(prettyFormat(tree)).toEqual(prettyFormat({
642+
type: Bam,
643+
nodeType: 'component',
644+
props: {},
645+
instance: null,
646+
rendered: {
647+
type: Bar,
648+
nodeType: 'component',
649+
props: { special: true },
650+
instance: null,
651+
rendered: {
652+
type: Foo,
653+
nodeType: 'component',
654+
props: { className: 'special' },
655+
instance: null,
656+
rendered: {
657+
type: 'div',
658+
nodeType: 'host',
659+
props: { className: 'Foo special' },
660+
instance: null,
661+
rendered: [
662+
{
663+
type: 'span',
664+
nodeType: 'host',
665+
props: { className: 'Foo2' },
666+
instance: null,
667+
rendered: ['Literal'],
668+
},
669+
{
670+
type: Qoo,
671+
nodeType: 'component',
672+
props: {},
673+
instance: null,
674+
rendered: {
675+
type: 'span',
676+
nodeType: 'host',
677+
props: { className: 'Qoo' },
678+
instance: null,
679+
rendered: ['Hello World!'],
680+
},
681+
},
682+
],
683+
},
684+
},
685+
},
686+
}));
687+
688+
});
689+
520690
if (ReactDOMFeatureFlags.useFiber) {
521691
it('can update text nodes when rendered as root', () => {
522692
var renderer = ReactTestRenderer.create(['Hello', 'world']);

src/renderers/testing/stack/ReactTestMount.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,9 @@ ReactTestInstance.prototype.unmount = function(nextElement) {
138138
});
139139
this._component = null;
140140
};
141+
ReactTestInstance.prototype.toTree = function() {
142+
return toTree(this._component._renderedComponent);
143+
};
141144
ReactTestInstance.prototype.toJSON = function() {
142145
var inst = getHostComponentFromComposite(this._component);
143146
if (inst === null) {
@@ -146,6 +149,39 @@ ReactTestInstance.prototype.toJSON = function() {
146149
return inst.toJSON();
147150
};
148151

152+
function toTree(component) {
153+
var element = component._currentElement;
154+
if (!React.isValidElement(element)) {
155+
return element;
156+
}
157+
if (!component._renderedComponent) {
158+
var rendered = [];
159+
for (var key in component._renderedChildren) {
160+
var inst = component._renderedChildren[key];
161+
var json = toTree(inst);
162+
if (json !== undefined) {
163+
rendered.push(json);
164+
}
165+
}
166+
167+
return {
168+
nodeType: 'host',
169+
type: element.type,
170+
props: { ...element.props },
171+
instance: component._nodeMock,
172+
rendered: rendered,
173+
};
174+
} else {
175+
return {
176+
nodeType: 'component',
177+
type: element.type,
178+
props: { ...element.props },
179+
instance: component._instance,
180+
rendered: toTree(component._renderedComponent),
181+
};
182+
}
183+
}
184+
149185
/**
150186
* As soon as `ReactMount` is refactored to not rely on the DOM, we can share
151187
* code between the two. For now, we'll hard code the ID logic.

0 commit comments

Comments
 (0)