@samitouri / QOS-React / commits / 869c779861

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.