From e35416ff50f6ae9d0c22c1462ac009d6b99d7dbc Mon Sep 17 00:00:00 2001 From: Leland Richardson Date: Sat, 25 Mar 2017 00:46:35 -0700 Subject: [PATCH] Add createComponentMock option to test renderer --- scripts/fiber/tests-passing.txt | 2 + src/renderers/shared/fiber/ReactFiber.js | 2 + .../shared/fiber/ReactFiberBeginWork.js | 17 ++ .../shared/fiber/ReactFiberReconciler.js | 2 + .../testing/ReactTestRendererFiber.js | 86 ++++++- .../__tests__/ReactTestRenderer-test.js | 227 ++++++++++++++++++ 6 files changed, 331 insertions(+), 5 deletions(-) diff --git a/scripts/fiber/tests-passing.txt b/scripts/fiber/tests-passing.txt index 16aab7d56eaaf..351f48625aeca 100644 --- a/scripts/fiber/tests-passing.txt +++ b/scripts/fiber/tests-passing.txt @@ -1791,6 +1791,8 @@ src/renderers/testing/__tests__/ReactTestRenderer-test.js * toTree() renders complicated trees of composites and hosts * can update text nodes when rendered as root * can render and update root fragments +* allows createComponentMock option to be passed in, simulating shallow +* createComponentMock can mock out specific components src/shared/utils/__tests__/KeyEscapeUtils-test.js * should properly escape and wrap user defined keys diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js index faf8f95d37047..fcd04e3df644a 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -61,6 +61,7 @@ export type Fiber = { _debugSource?: Source | null, _debugOwner?: Fiber | ReactInstance | null, // Stack compatible _debugIsCurrentlyTiming?: boolean, + _unmockedType?: any, // These first fields are conceptually members of an Instance. This used to // be split into a separate type and intersected with the other Fiber fields, @@ -222,6 +223,7 @@ var createFiber = function(tag: TypeOfWork, key: null | string): Fiber { fiber._debugSource = null; fiber._debugOwner = null; fiber._debugIsCurrentlyTiming = false; + fiber._unmockedType = null; if (typeof Object.preventExtensions === 'function') { Object.preventExtensions(fiber); } diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index ad58d044613f2..b600ed4136a32 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -102,6 +102,11 @@ module.exports = function( memoizeState, ); + if (__DEV__) { + var hasMockingBehavior = typeof config.mockComponent === 'function'; + var mockComponent = config.mockComponent || (() => {}); + } + function markChildAsProgressed(current, workInProgress, priorityLevel) { // We now have clones. Let's store them as the currently progressed work. workInProgress.progressedChild = workInProgress.child; @@ -794,6 +799,18 @@ module.exports = function( workInProgress.child = workInProgress.progressedChild; } + if (__DEV__) { + if (hasMockingBehavior) { + switch (workInProgress.tag) { + case IndeterminateComponent: + case FunctionalComponent: + case ClassComponent: + mockComponent(workInProgress, hostContext.getRootHostContainer()); + break; + } + } + } + switch (workInProgress.tag) { case IndeterminateComponent: return mountIndeterminateComponent( diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index 75e96aa274dfe..517c922f1f813 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -115,6 +115,8 @@ export type HostConfig = { resetAfterCommit(): void, useSyncScheduling?: boolean, + + mockComponent?: Function, }; export type Reconciler = { diff --git a/src/renderers/testing/ReactTestRendererFiber.js b/src/renderers/testing/ReactTestRendererFiber.js index 19c2fee0b2d2f..1d62eb54a42b7 100644 --- a/src/renderers/testing/ReactTestRendererFiber.js +++ b/src/renderers/testing/ReactTestRendererFiber.js @@ -19,9 +19,11 @@ var emptyObject = require('fbjs/lib/emptyObject'); var ReactTypeOfWork = require('ReactTypeOfWork'); var invariant = require('fbjs/lib/invariant'); var { + IndeterminateComponent, FunctionalComponent, ClassComponent, HostComponent, + Fragment, HostText, HostRoot, } = ReactTypeOfWork; @@ -41,6 +43,7 @@ type ReactTestRendererNode = ReactTestRendererJSON | string; type Container = {| children: Array, createNodeMock: Function, + createComponentMock: Function, tag: 'CONTAINER', |}; @@ -217,6 +220,29 @@ var TestRenderer = ReactFiberReconciler({ setTimeout(fn, 0, {timeRemaining: Infinity}); }, + mockComponent(component: Fiber, rootContainer: Container) { + invariant( + component._unmockedType === null, + 'Trying to mock an already mocked component', + ); + const mockedFn = rootContainer.createComponentMock({ + type: component.type, + props: component.pendingProps, + }); + invariant( + typeof mockedFn === 'function', + 'createComponentMock() must return a function. Found %s instead.', + typeof mockedFn, + ); + if (mockedFn !== component.type) { + component._unmockedType = component.type; + component.type = mockedFn; + // force the fiber to be indeterminate so that users can mock a class component + // into a functional component and vice versa + component.tag = IndeterminateComponent; + } + }, + useSyncScheduling: true, getPublicInstance(inst) { @@ -237,6 +263,9 @@ var defaultTestOptions = { createNodeMock: function() { return null; }, + createComponentMock: function(component: {type: Function, props: any}) { + return component.type; + }, }; function toJSON(inst: Instance | TextInstance): ReactTestRendererNode { @@ -277,6 +306,46 @@ function nodeAndSiblingsArray(nodeWithSibling: ?Fiber) { return array; } +function childrenToTree(node) { + if (!node) { + return null; + } + const children = nodeAndSiblingsArray(node); + if (children.length === 0) { + return null; + } else if (children.length === 1) { + return toTree(children[0]); + } else { + return flatten(children.map(toTree)); + } +} + +function flatten(arr) { + const result = []; + const stack = [{i: 0, array: arr}]; + while (stack.length) { + let n = stack.pop(); + while (n.i < n.array.length) { + const el = n.array[n.i]; + n.i += 1; + if (Array.isArray(el)) { + stack.push(n); + stack.push({i: 0, array: el}); + break; + } + result.push(el); + } + } + return result; +} + +function publicType(node: Fiber) { + if (node._unmockedType !== null) { + return node._unmockedType; + } + return node.type; +} + function toTree(node: ?Fiber) { if (node == null) { return null; @@ -287,18 +356,20 @@ function toTree(node: ?Fiber) { case ClassComponent: return { nodeType: 'component', - type: node.type, + type: publicType(node), props: {...node.memoizedProps}, instance: node.stateNode, - rendered: toTree(node.child), + rendered: childrenToTree(node.child), }; + case Fragment: // 10 + return childrenToTree(node.child); case FunctionalComponent: // 1 return { nodeType: 'component', - type: node.type, + type: publicType(node), props: {...node.memoizedProps}, instance: null, - rendered: toTree(node.child), + rendered: childrenToTree(node.child), }; case HostComponent: // 5 return { @@ -306,7 +377,7 @@ function toTree(node: ?Fiber) { type: node.type, props: {...node.memoizedProps}, instance: null, // TODO: use createNodeMock here somehow? - rendered: nodeAndSiblingsArray(node.child).map(toTree), + rendered: flatten(nodeAndSiblingsArray(node.child).map(toTree)), }; case HostText: // 6 return node.stateNode.text; @@ -325,9 +396,14 @@ var ReactTestFiberRenderer = { if (options && typeof options.createNodeMock === 'function') { createNodeMock = options.createNodeMock; } + var createComponentMock = defaultTestOptions.createComponentMock; + if (options && typeof options.createComponentMock === 'function') { + createComponentMock = options.createComponentMock; + } var container = { children: [], createNodeMock, + createComponentMock, tag: 'CONTAINER', }; var root: ?FiberRoot = TestRenderer.createContainer(container); diff --git a/src/renderers/testing/__tests__/ReactTestRenderer-test.js b/src/renderers/testing/__tests__/ReactTestRenderer-test.js index ce64455e4a4cb..c0167c3d476e6 100644 --- a/src/renderers/testing/__tests__/ReactTestRenderer-test.js +++ b/src/renderers/testing/__tests__/ReactTestRenderer-test.js @@ -40,6 +40,21 @@ function cleanNode(node) { } } +const MockComponent = ({children}) => { + return children ? React.Children.toArray(children) : []; +}; + +function makeShallowRendererMock() { + var isFirst = true; + return el => { + if (isFirst) { + isFirst = false; + return el.type; + } + return MockComponent; + }; +} + describe('ReactTestRenderer', () => { beforeEach(() => { ReactFeatureFlags = require('ReactFeatureFlags'); @@ -696,5 +711,217 @@ describe('ReactTestRenderer', () => { 'world', ]); }); + + it('allows createComponentMock option to be passed in, simulating shallow', () => { + class Bar extends React.Component { + constructor(props) { + super(props); + throw new Error('Bar constructor should not be called'); + } + render() { + throw new Error('Bar render method should not be called'); + } + } + + const Foo = () => { + throw new Error('Foo render method should not be called'); + }; + + // class composite return composite. no children props. + class Bam extends React.Component { + render() { + return ( + + + + + + ); + } + } + + var renderer = ReactTestRenderer.create(, { + createComponentMock: makeShallowRendererMock(), + }); + var tree = renderer.toTree(); + + cleanNode(tree); + + expect(prettyFormat(tree)).toEqual( + prettyFormat({ + type: Bam, + nodeType: 'component', + props: {}, + instance: null, + rendered: { + type: Bar, + nodeType: 'component', + props: {}, + instance: null, + rendered: [ + { + type: Foo, + nodeType: 'component', + props: {}, + instance: null, + rendered: null, + }, + { + type: Foo, + nodeType: 'component', + props: {}, + instance: null, + rendered: null, + }, + { + type: Foo, + nodeType: 'component', + props: {}, + instance: null, + rendered: null, + }, + ], + }, + }), + ); + }); + + it('createComponentMock can mock out specific components', () => { + class Bar extends React.Component { + render() { + return ( +
+ + {this.props.children} +
+ ); + } + } + class ErrorThrowingBar extends React.Component { + render() { + throw new Error('Bar render method should not be called'); + } + } + + const ErrorThrowingFoo = () => { + throw new Error('Foo render method should not be called'); + }; + + const SafeFoo = () =>
; + + // class composite return composite. no children props. + class Bam extends React.Component { + render() { + return ( + + + + + + + + + ); + } + } + + var unsafeComponents = [ErrorThrowingBar, ErrorThrowingFoo]; + + var renderer = ReactTestRenderer.create(, { + createComponentMock: ({type, props}) => { + if (unsafeComponents.indexOf(type) !== -1) { + return MockComponent; + } + return type; + }, + }); + var tree = renderer.toTree(); + + cleanNode(tree); + + expect(prettyFormat(tree)).toEqual( + prettyFormat({ + type: Bam, + nodeType: 'component', + props: {}, + instance: null, + rendered: { + type: ErrorThrowingBar, + nodeType: 'component', + props: {}, + instance: null, + rendered: [ + { + type: ErrorThrowingFoo, + nodeType: 'component', + props: {}, + instance: null, + rendered: null, + }, + { + type: SafeFoo, + nodeType: 'component', + props: {}, + instance: null, + rendered: { + type: 'div', + nodeType: 'host', + props: {}, + instance: null, + rendered: [], + }, + }, + { + type: Bar, + nodeType: 'component', + props: {}, + instance: null, + rendered: { + type: 'div', + nodeType: 'host', + props: {}, + instance: null, + rendered: [ + { + type: SafeFoo, + nodeType: 'component', + props: {}, + instance: null, + rendered: { + type: 'div', + nodeType: 'host', + props: {}, + instance: null, + rendered: [], + }, + }, + { + type: ErrorThrowingFoo, + nodeType: 'component', + props: {}, + instance: null, + rendered: null, + }, + { + type: SafeFoo, + nodeType: 'component', + props: {}, + instance: null, + rendered: { + type: 'div', + nodeType: 'host', + props: {}, + instance: null, + rendered: [], + }, + }, + ], + }, + }, + ], + }, + }), + ); + }); } });