diff --git a/scripts/fiber/tests-passing.txt b/scripts/fiber/tests-passing.txt index 001cd67294261..8452fb3f79396 100644 --- a/scripts/fiber/tests-passing.txt +++ b/scripts/fiber/tests-passing.txt @@ -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 diff --git a/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js b/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js index e7aa14ef93a15..f534cfd5c71fe 100644 --- a/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js +++ b/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js @@ -702,7 +702,7 @@ var ReactCompositeComponent = { } else { if (__DEV__) { const componentName = this.getName(); - + if (!warningAboutMissingGetChildContext[componentName]) { warningAboutMissingGetChildContext[componentName] = true; warning( diff --git a/src/renderers/testing/ReactTestRendererFiber.js b/src/renderers/testing/ReactTestRendererFiber.js index 2ce0d7978814c..7aba7553ba475 100644 --- a/src/renderers/testing/ReactTestRendererFiber.js +++ b/src/renderers/testing/ReactTestRendererFiber.js @@ -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, 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) { - 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); diff --git a/src/renderers/testing/__tests__/ReactTestRenderer-test.js b/src/renderers/testing/__tests__/ReactTestRenderer-test.js index 7510095463502..ef2b6124f6bc5 100644 --- a/src/renderers/testing/__tests__/ReactTestRenderer-test.js +++ b/src/renderers/testing/__tests__/ReactTestRenderer-test.js @@ -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 = () => ( + Hello World! + ); + + var renderer = ReactTestRenderer.create(); + 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(); + 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 = () => ( + Hello World! + ); + + // SFC returning host. passes through children. + var Foo = ({ className, children }) => ( +
+ Literal + {children} +
+ ); + + // class composite returning composite. passes through children. + class Bar extends React.Component { + render() { + const { special, children } = this.props; + return ( + + {children} + + ); + } + } + + // class composite return composite. no children props. + class Bam extends React.Component { + render() { + return ( + + + + ); + } + } + + var renderer = ReactTestRenderer.create(); + 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']); diff --git a/src/renderers/testing/stack/ReactTestMount.js b/src/renderers/testing/stack/ReactTestMount.js index 65dd8c4d0a209..f2af00340648e 100644 --- a/src/renderers/testing/stack/ReactTestMount.js +++ b/src/renderers/testing/stack/ReactTestMount.js @@ -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.