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
Leland Richardson committed
Feb 9, 2017 at 12:55 UTC
869c7798611ac17634e0a4cde71b4322dce20b87
5 files changed
+285
-6
scripts/fiber/tests-passing.txt
+3
@@ -1686,6 +1686,9 @@ src/renderers/testing/__tests__/ReactTestRenderer-test.js
1686
* supports updates when using refs
1687
* supports error boundaries
1688
* 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
1692
* can update text nodes when rendered as root
1693
* can render and update root fragments
1694
src/renderers/shared/stack/reconciler/ReactCompositeComponent.js
+1
-1
@@ -702,7 +702,7 @@ var ReactCompositeComponent = {
702
} else {
703
if (__DEV__) {
704
const componentName = this.getName();
705
-
705
+
706
if (!warningAboutMissingGetChildContext[componentName]) {
707
warningAboutMissingGetChildContext[componentName] = true;
708
warning(
src/renderers/testing/ReactTestRendererFiber.js
+75
-5
@@ -16,8 +16,19 @@
16
var ReactFiberReconciler = require('ReactFiberReconciler');
17
var ReactGenericBatching = require('ReactGenericBatching');
18
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;
28
29
import type { TestRendererOptions } from 'ReactTestMount';
30
+import type { Fiber } from 'ReactFiber';
31
+import type { FiberRoot } from 'ReactFiberRoot';
32
33
type ReactTestRendererJSON = {|
34
type : string,
@@ -237,6 +248,58 @@ function toJSON(inst : Instance | TextInstance) : ReactTestRendererNode {
248
}
249
}
250
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
+
303
var ReactTestFiberRenderer = {
304
create(element : ReactElement<any>, options : TestRendererOptions) {
305
var createNodeMock = defaultTestOptions.createNodeMock;
@@ -248,12 +311,13 @@ var ReactTestFiberRenderer = {
311
createNodeMock,
312
tag: 'CONTAINER',
313
};
251
- var root = TestRenderer.createContainer(container);
314
+ var root: ?FiberRoot = TestRenderer.createContainer(container);
315
+ invariant(root != null, 'something went wrong');
316
TestRenderer.updateContainer(element, root, null, null);
317
318
return {
319
toJSON() {
256
- if (root == null || container == null) {
320
+ if (root == null || root.current == null || container == null) {
321
return null;
322
}
323
if (container.children.length === 0) {
@@ -264,14 +328,20 @@ var ReactTestFiberRenderer = {
328
}
329
return container.children.map(toJSON);
330
},
331
+ toTree() {
332
+ if (root == null || root.current == null) {
333
+ return null;
334
+ }
335
+ return toTree(root.current);
336
+ },
337
update(newElement : ReactElement<any>) {
268
- if (root == null) {
338
+ if (root == null || root.current == null) {
339
return;
340
}
341
TestRenderer.updateContainer(newElement, root, null, null);
342
},
343
unmount() {
274
- if (root == null) {
344
+ if (root == null || root.current == null) {
345
return;
346
}
347
TestRenderer.updateContainer(null, root, null);
@@ -279,7 +349,7 @@ var ReactTestFiberRenderer = {
349
root = null;
350
},
351
getInstance() {
282
- if (root == null) {
352
+ if (root == null || root.current == null) {
353
return null;
354
}
355
return TestRenderer.getPublicRootInstance(root);
src/renderers/testing/__tests__/ReactTestRenderer-test.js
+170
@@ -14,8 +14,32 @@
14
var React = require('React');
15
var ReactTestRenderer = require('ReactTestRenderer');
16
var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
17
+var prettyFormat = require('pretty-format');
18
var ReactFeatureFlags;
19
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
+
43
describe('ReactTestRenderer', () => {
44
beforeEach(() => {
45
ReactFeatureFlags = require('ReactFeatureFlags');
@@ -517,6 +541,152 @@ describe('ReactTestRenderer', () => {
541
});
542
});
543
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
+
690
if (ReactDOMFeatureFlags.useFiber) {
691
it('can update text nodes when rendered as root', () => {
692
var renderer = ReactTestRenderer.create(['Hello', 'world']);
src/renderers/testing/stack/ReactTestMount.js
+36
@@ -138,6 +138,9 @@ ReactTestInstance.prototype.unmount = function(nextElement) {
138
});
139
this._component = null;
140
};
141
+ReactTestInstance.prototype.toTree = function() {
142
+ return toTree(this._component._renderedComponent);
143
+};
144
ReactTestInstance.prototype.toJSON = function() {
145
var inst = getHostComponentFromComposite(this._component);
146
if (inst === null) {
@@ -146,6 +149,39 @@ ReactTestInstance.prototype.toJSON = function() {
149
return inst.toJSON();
150
};
151
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
+
185
/**
186
* As soon as `ReactMount` is refactored to not rely on the DOM, we can share
187
* code between the two. For now, we'll hard code the ID logic.