mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
[Fiber] Implement test renderer (#8628)
* ReactTestRenderer move current impl to stack dir * ReactTestRenderer on fiber: commence! * ReactTestRenderer: most non-ref/non-public-instance tests are passing * Move ReactTestFiberComponent functions from Renderer to Component file * test renderer: get rid of private root containers and root Maps * TestRenderer: switch impl based on ReactDOMFeatureFlag.useFiber * ReactTestRenderer: inline component creation * ReactTestRenderer: return to pristine original glory (+ Fiber for error order difference) * TestRendererFiber: use a simple class as TestComponentInstances * Add `getPublicInstance` to support TestRenderer `createNodeMock` * Rename files to end. Update for `mountContainer->createContainer` change * test renderer return same object to prevent unnecessary context pushing/popping * Fiber HostConfig add getPublicInstance. This should be the identity fn everywhere except the test renderer * appease flow * Initial cleanup from sleepy work * unstable_batchedUpdates * Stack test renderer: cache nodeMock to not call on unmount * add public instance type parameter to the reconciler * test renderer: set _nodeMock when mounted * More cleanup * Add test cases for root fragments and (maybe?) root text nodes * Fix the npm package build Explicitly require the Stack version by default. Add a separate entry point for Fiber. We don't add fiber.js to the package yet since it's considered internal until React 16. * Relax the ref type from Object to mixed This seems like the most straightforward way to support getPublicInstance for test renderer. * Remove accidental newline * test renderer: unify TestComponent and TestContainer, handle root updates * Remove string/number serialization attempts since Fiber ensures all textInstances are strings * Return full fragments in toJSON * Test Renderer remove TestComponent instances for simple objects * Update babylon for exact object type syntax * Use $$typeof because clarity > punching ducks. * Minor Flow annotation tweaks * Tweak style, types, and naming * Fix typo
This commit is contained in:
committed by
Dan Abramov
parent
837835d7f3
commit
2da35fcae8
+1
-1
@@ -33,7 +33,7 @@
|
||||
"babel-plugin-transform-react-jsx-source": "^6.8.0",
|
||||
"babel-preset-react": "^6.5.0",
|
||||
"babel-traverse": "^6.9.0",
|
||||
"babylon": "6.8.0",
|
||||
"babylon": "6.15.0",
|
||||
"browserify": "^13.0.0",
|
||||
"bundle-collapser": "^1.1.1",
|
||||
"coffee-script": "^1.8.0",
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = require('./lib/ReactTestRendererFiber');
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = require('./lib/ReactTestRenderer');
|
||||
module.exports = require('./lib/ReactTestRendererStack');
|
||||
|
||||
@@ -1675,6 +1675,7 @@ src/renderers/testing/__tests__/ReactTestRenderer-test.js
|
||||
* renders a simple component
|
||||
* renders a top-level empty component
|
||||
* exposes a type flag
|
||||
* can render a composite component
|
||||
* renders some basics with an update
|
||||
* exposes the instance
|
||||
* updates types
|
||||
@@ -1687,6 +1688,9 @@ src/renderers/testing/__tests__/ReactTestRenderer-test.js
|
||||
* supports unmounting inner instances
|
||||
* supports updates when using refs
|
||||
* supports error boundaries
|
||||
* can update text nodes
|
||||
* can update text nodes when rendered as root
|
||||
* can render and update root fragments
|
||||
|
||||
src/shared/utils/__tests__/KeyEscapeUtils-test.js
|
||||
* should properly escape and wrap user defined keys
|
||||
|
||||
@@ -464,6 +464,10 @@ const ARTRenderer = ReactFiberReconciler({
|
||||
return false;
|
||||
},
|
||||
|
||||
getPublicInstance(instance) {
|
||||
return instance;
|
||||
},
|
||||
|
||||
insertBefore(parentInstance, child, beforeChild) {
|
||||
invariant(
|
||||
child !== beforeChild,
|
||||
|
||||
@@ -137,6 +137,10 @@ var DOMRenderer = ReactFiberReconciler({
|
||||
return getChildNamespace(parentNamespace, type);
|
||||
},
|
||||
|
||||
getPublicInstance(instance) {
|
||||
return instance;
|
||||
},
|
||||
|
||||
prepareForCommit() : void {
|
||||
eventsEnabled = ReactBrowserEventEmitter.isEnabled();
|
||||
selectionInformation = ReactInputSelection.getSelectionInformation();
|
||||
|
||||
@@ -232,6 +232,10 @@ const NativeRenderer = ReactFiberReconciler({
|
||||
return emptyObject;
|
||||
},
|
||||
|
||||
getPublicInstance(instance) {
|
||||
return instance;
|
||||
},
|
||||
|
||||
insertBefore(
|
||||
parentInstance : Instance | Container,
|
||||
child : Instance | TextInstance,
|
||||
|
||||
@@ -54,6 +54,10 @@ var NoopRenderer = ReactFiberReconciler({
|
||||
return emptyObject;
|
||||
},
|
||||
|
||||
getPublicInstance(instance) {
|
||||
return instance;
|
||||
},
|
||||
|
||||
createInstance(type : string, props : Props) : Instance {
|
||||
const inst = {
|
||||
id: instanceCounter++,
|
||||
|
||||
@@ -102,7 +102,7 @@ export type Fiber = {
|
||||
|
||||
// The ref last used to attach this node.
|
||||
// I'll avoid adding an owner field for prod and model that as functions.
|
||||
ref: null | (((handle : ?Object) => void) & { _stringRef: ?string }),
|
||||
ref: null | (((handle : mixed) => void) & { _stringRef: ?string }),
|
||||
|
||||
// Input is the data coming into process this fiber. Arguments. Props.
|
||||
pendingProps: any, // This type will be more specific once we overload the tag.
|
||||
|
||||
@@ -71,8 +71,8 @@ if (__DEV__) {
|
||||
var warnedAboutStatelessRefs = {};
|
||||
}
|
||||
|
||||
module.exports = function<T, P, I, TI, C, CX>(
|
||||
config : HostConfig<T, P, I, TI, C, CX>,
|
||||
module.exports = function<T, P, I, TI, PI, C, CX>(
|
||||
config : HostConfig<T, P, I, TI, PI, C, CX>,
|
||||
hostContext : HostContext<C, CX>,
|
||||
scheduleUpdate : (fiber : Fiber, priorityLevel : PriorityLevel) => void,
|
||||
getPriorityContext : () => PriorityLevel,
|
||||
|
||||
@@ -34,8 +34,8 @@ var {
|
||||
ContentReset,
|
||||
} = require('ReactTypeOfSideEffect');
|
||||
|
||||
module.exports = function<T, P, I, TI, C, CX>(
|
||||
config : HostConfig<T, P, I, TI, C, CX>,
|
||||
module.exports = function<T, P, I, TI, PI, C, CX>(
|
||||
config : HostConfig<T, P, I, TI, PI, C, CX>,
|
||||
hostContext : HostContext<C, CX>,
|
||||
captureError : (failedFiber : Fiber, error: Error) => ?Fiber
|
||||
) {
|
||||
@@ -48,6 +48,7 @@ module.exports = function<T, P, I, TI, C, CX>(
|
||||
appendChild,
|
||||
insertBefore,
|
||||
removeChild,
|
||||
getPublicInstance,
|
||||
} = config;
|
||||
|
||||
const {
|
||||
@@ -462,7 +463,7 @@ module.exports = function<T, P, I, TI, C, CX>(
|
||||
}
|
||||
const ref = finishedWork.ref;
|
||||
if (ref) {
|
||||
const instance = finishedWork.stateNode;
|
||||
const instance = getPublicInstance(finishedWork.stateNode);
|
||||
ref(instance);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,8 +46,8 @@ if (__DEV__) {
|
||||
var ReactDebugCurrentFiber = require('ReactDebugCurrentFiber');
|
||||
}
|
||||
|
||||
module.exports = function<T, P, I, TI, C, CX>(
|
||||
config : HostConfig<T, P, I, TI, C, CX>,
|
||||
module.exports = function<T, P, I, TI, PI, C, CX>(
|
||||
config : HostConfig<T, P, I, TI, PI, C, CX>,
|
||||
hostContext : HostContext<C, CX>,
|
||||
) {
|
||||
const {
|
||||
|
||||
@@ -34,8 +34,8 @@ export type HostContext<C, CX> = {
|
||||
resetHostContainer() : void,
|
||||
};
|
||||
|
||||
module.exports = function<T, P, I, TI, C, CX>(
|
||||
config : HostConfig<T, P, I, TI, C, CX>
|
||||
module.exports = function<T, P, I, TI, PI, C, CX>(
|
||||
config : HostConfig<T, P, I, TI, PI, C, CX>
|
||||
) : HostContext<C, CX> {
|
||||
const {
|
||||
getChildHostContext,
|
||||
|
||||
@@ -43,10 +43,11 @@ export type Deadline = {
|
||||
|
||||
type OpaqueNode = Fiber;
|
||||
|
||||
export type HostConfig<T, P, I, TI, C, CX> = {
|
||||
export type HostConfig<T, P, I, TI, PI, C, CX> = {
|
||||
|
||||
getRootHostContext(rootContainerInstance : C) : CX,
|
||||
getChildHostContext(parentHostContext : CX, type : T) : CX,
|
||||
getPublicInstance(instance : I | TI) : PI,
|
||||
|
||||
createInstance(type : T, props : P, rootContainerInstance : C, hostContext : CX, internalInstanceHandle : OpaqueNode) : I,
|
||||
appendInitialChild(parentInstance : I, child : I | TI) : void,
|
||||
@@ -101,7 +102,7 @@ getContextForSubtree._injectFiber(function(fiber : Fiber) {
|
||||
parentContext;
|
||||
});
|
||||
|
||||
module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C, CX>) : Reconciler<C, I, TI> {
|
||||
module.exports = function<T, P, I, TI, PI, C, CX>(config : HostConfig<T, P, I, TI, PI, C, CX>) : Reconciler<C, I, TI> {
|
||||
|
||||
var {
|
||||
scheduleUpdate,
|
||||
|
||||
@@ -74,7 +74,7 @@ if (__DEV__) {
|
||||
|
||||
var timeHeuristicForUnitOfWork = 1;
|
||||
|
||||
module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C, CX>) {
|
||||
module.exports = function<T, P, I, TI, PI, C, CX>(config : HostConfig<T, P, I, TI, PI, C, CX>) {
|
||||
const hostContext = ReactFiberHostContext(config);
|
||||
const { popHostContainer, popHostContext, resetHostContainer } = hostContext;
|
||||
const { beginWork, beginFailedWork } = ReactFiberBeginWork(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright 2013-present, Facebook, Inc.
|
||||
* Copyright (c) 2013-present Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
@@ -7,151 +7,12 @@
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @providesModule ReactTestRenderer
|
||||
* @preventMunge
|
||||
* @flow
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var ReactComponentEnvironment = require('ReactComponentEnvironment');
|
||||
var ReactDefaultBatchingStrategy = require('ReactDefaultBatchingStrategy');
|
||||
var ReactEmptyComponent = require('ReactEmptyComponent');
|
||||
var ReactMultiChild = require('ReactMultiChild');
|
||||
var ReactHostComponent = require('ReactHostComponent');
|
||||
var ReactTestMount = require('ReactTestMount');
|
||||
var ReactTestReconcileTransaction = require('ReactTestReconcileTransaction');
|
||||
var ReactUpdates = require('ReactUpdates');
|
||||
var ReactTestTextComponent = require('ReactTestTextComponent');
|
||||
var ReactTestEmptyComponent = require('ReactTestEmptyComponent');
|
||||
var invariant = require('invariant');
|
||||
const ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
|
||||
|
||||
import type { ReactElement } from 'ReactElementType';
|
||||
import type { ReactInstance } from 'ReactInstanceType';
|
||||
|
||||
type ReactTestRendererJSON = {
|
||||
type: string,
|
||||
props: { [propName: string]: string },
|
||||
children: null | Array<string | ReactTestRendererJSON>,
|
||||
$$typeof?: any
|
||||
}
|
||||
|
||||
/**
|
||||
* Drill down (through composites and empty components) until we get a native or
|
||||
* native text component.
|
||||
*
|
||||
* This is pretty polymorphic but unavoidable with the current structure we have
|
||||
* for `_renderedChildren`.
|
||||
*/
|
||||
function getRenderedHostOrTextFromComponent(component) {
|
||||
var rendered;
|
||||
while ((rendered = component._renderedComponent)) {
|
||||
component = rendered;
|
||||
}
|
||||
return component;
|
||||
}
|
||||
|
||||
class ReactTestComponent {
|
||||
_currentElement: ReactElement;
|
||||
_renderedChildren: null | Object;
|
||||
_topLevelWrapper: null | ReactInstance;
|
||||
_hostContainerInfo: null | Object;
|
||||
|
||||
constructor(element: ReactElement) {
|
||||
this._currentElement = element;
|
||||
this._renderedChildren = null;
|
||||
this._topLevelWrapper = null;
|
||||
this._hostContainerInfo = null;
|
||||
}
|
||||
|
||||
mountComponent(
|
||||
transaction: ReactTestReconcileTransaction,
|
||||
nativeParent: null | ReactTestComponent,
|
||||
hostContainerInfo: Object,
|
||||
context: Object,
|
||||
) {
|
||||
var element = this._currentElement;
|
||||
this._hostContainerInfo = hostContainerInfo;
|
||||
// $FlowFixMe https://github.com/facebook/flow/issues/1805
|
||||
this.mountChildren(element.props.children, transaction, context);
|
||||
}
|
||||
|
||||
receiveComponent(
|
||||
nextElement: ReactElement,
|
||||
transaction: ReactTestReconcileTransaction,
|
||||
context: Object,
|
||||
) {
|
||||
this._currentElement = nextElement;
|
||||
// $FlowFixMe https://github.com/facebook/flow/issues/1805
|
||||
this.updateChildren(nextElement.props.children, transaction, context);
|
||||
}
|
||||
|
||||
getPublicInstance(): Object {
|
||||
var element = this._currentElement;
|
||||
var hostContainerInfo = this._hostContainerInfo;
|
||||
invariant(
|
||||
hostContainerInfo,
|
||||
'hostContainerInfo should be populated before ' +
|
||||
'getPublicInstance is called.'
|
||||
);
|
||||
return hostContainerInfo.createNodeMock(element);
|
||||
}
|
||||
|
||||
toJSON(): ReactTestRendererJSON {
|
||||
// not using `children`, but I don't want to rewrite without destructuring
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
var {children, ...props} = this._currentElement.props;
|
||||
var childrenJSON = [];
|
||||
for (var key in this._renderedChildren) {
|
||||
var inst = this._renderedChildren[key];
|
||||
inst = getRenderedHostOrTextFromComponent(inst);
|
||||
var json = inst.toJSON();
|
||||
if (json !== undefined) {
|
||||
childrenJSON.push(json);
|
||||
}
|
||||
}
|
||||
var object: ReactTestRendererJSON = {
|
||||
type: this._currentElement.type,
|
||||
props: props,
|
||||
children: childrenJSON.length ? childrenJSON : null,
|
||||
};
|
||||
Object.defineProperty(object, '$$typeof', {
|
||||
value: Symbol.for('react.test.json'),
|
||||
});
|
||||
return object;
|
||||
}
|
||||
|
||||
getHostNode(): void {}
|
||||
unmountComponent(safely, skipLifecycle): void {
|
||||
// $FlowFixMe https://github.com/facebook/flow/issues/1805
|
||||
this.unmountChildren(safely, skipLifecycle);
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(ReactTestComponent.prototype, ReactMultiChild);
|
||||
|
||||
// =============================================================================
|
||||
|
||||
ReactUpdates.injection.injectReconcileTransaction(
|
||||
ReactTestReconcileTransaction
|
||||
);
|
||||
ReactUpdates.injection.injectBatchingStrategy(ReactDefaultBatchingStrategy);
|
||||
|
||||
ReactHostComponent.injection.injectGenericComponentClass(ReactTestComponent);
|
||||
ReactHostComponent.injection.injectTextComponentClass(ReactTestTextComponent);
|
||||
ReactEmptyComponent.injection.injectEmptyComponentFactory(function() {
|
||||
return new ReactTestEmptyComponent();
|
||||
});
|
||||
|
||||
ReactComponentEnvironment.injection.injectEnvironment({
|
||||
processChildrenUpdates: function() {},
|
||||
replaceNodeWithMarkup: function() {},
|
||||
});
|
||||
|
||||
var ReactTestRenderer = {
|
||||
create: ReactTestMount.render,
|
||||
/* eslint-disable camelcase */
|
||||
unstable_batchedUpdates: ReactUpdates.batchedUpdates,
|
||||
/* eslint-enable camelcase */
|
||||
};
|
||||
|
||||
module.exports = ReactTestRenderer;
|
||||
module.exports = ReactDOMFeatureFlags.useFiber
|
||||
? require('ReactTestRendererFiber')
|
||||
: require('ReactTestRendererStack');
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
/**
|
||||
* Copyright 2013-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @providesModule ReactTestRendererFiber
|
||||
* @preventMunge
|
||||
* @flow
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var ReactFiberReconciler = require('ReactFiberReconciler');
|
||||
var ReactGenericBatching = require('ReactGenericBatching');
|
||||
var emptyObject = require('emptyObject');
|
||||
|
||||
import type { TestRendererOptions } from 'ReactTestMount';
|
||||
|
||||
type ReactTestRendererJSON = {|
|
||||
type : string,
|
||||
props : {[propName: string] : string },
|
||||
children : null | Array<ReactTestRendererNode>,
|
||||
$$typeof ?: Symbol, // Optional because we add it with defineProperty().
|
||||
|};
|
||||
type ReactTestRendererNode = ReactTestRendererJSON | string;
|
||||
|
||||
type Container = {|
|
||||
children : Array<Instance | TextInstance>,
|
||||
createNodeMock : Function,
|
||||
tag : 'CONTAINER',
|
||||
|};
|
||||
|
||||
type Props = Object;
|
||||
type Instance = {|
|
||||
type : string,
|
||||
props : Object,
|
||||
children : Array<Instance | TextInstance>,
|
||||
rootContainerInstance : Container,
|
||||
tag : 'INSTANCE',
|
||||
|};
|
||||
|
||||
type TextInstance = {|
|
||||
text : string,
|
||||
tag : 'TEXT',
|
||||
|};
|
||||
|
||||
var TestRenderer = ReactFiberReconciler({
|
||||
getRootHostContext() {
|
||||
return emptyObject;
|
||||
},
|
||||
|
||||
getChildHostContext() {
|
||||
return emptyObject;
|
||||
},
|
||||
|
||||
prepareForCommit() : void {
|
||||
// noop
|
||||
},
|
||||
|
||||
resetAfterCommit() : void {
|
||||
// noop
|
||||
},
|
||||
|
||||
createInstance(
|
||||
type : string,
|
||||
props : Props,
|
||||
rootContainerInstance : Container,
|
||||
hostContext : Object,
|
||||
internalInstanceHandle : Object,
|
||||
) : Instance {
|
||||
return {
|
||||
type,
|
||||
props,
|
||||
children: [],
|
||||
rootContainerInstance,
|
||||
tag: 'INSTANCE',
|
||||
};
|
||||
},
|
||||
|
||||
appendInitialChild(parentInstance : Instance, child : Instance | TextInstance) : void {
|
||||
const index = parentInstance.children.indexOf(child);
|
||||
if (index !== -1) {
|
||||
parentInstance.children.splice(index, 1);
|
||||
}
|
||||
parentInstance.children.push(child);
|
||||
},
|
||||
|
||||
finalizeInitialChildren(
|
||||
testElement : Instance,
|
||||
type : string,
|
||||
props : Props,
|
||||
rootContainerInstance : Container,
|
||||
) : boolean {
|
||||
return false;
|
||||
},
|
||||
|
||||
prepareUpdate(
|
||||
testElement : Instance,
|
||||
type : string,
|
||||
oldProps : Props,
|
||||
newProps : Props,
|
||||
hostContext : Object,
|
||||
) : boolean {
|
||||
return true;
|
||||
},
|
||||
|
||||
commitUpdate(
|
||||
instance : Instance,
|
||||
type : string,
|
||||
oldProps : Props,
|
||||
newProps : Props,
|
||||
rootContainerInstance : Container,
|
||||
internalInstanceHandle : Object,
|
||||
) : void {
|
||||
instance.type = type;
|
||||
instance.props = newProps;
|
||||
},
|
||||
|
||||
commitMount(
|
||||
instance : Instance,
|
||||
type : string,
|
||||
newProps : Props,
|
||||
rootContainerInstance : Object,
|
||||
internalInstanceHandle : Object
|
||||
) : void {
|
||||
// noop
|
||||
},
|
||||
|
||||
shouldSetTextContent(props : Props) : boolean {
|
||||
return false;
|
||||
},
|
||||
|
||||
resetTextContent(testElement : Instance) : void {
|
||||
// noop
|
||||
},
|
||||
|
||||
createTextInstance(
|
||||
text : string,
|
||||
rootContainerInstance : Container,
|
||||
hostContext : Object,
|
||||
internalInstanceHandle : Object
|
||||
) : TextInstance {
|
||||
return {
|
||||
text,
|
||||
tag: 'TEXT',
|
||||
};
|
||||
},
|
||||
|
||||
commitTextUpdate(textInstance : TextInstance, oldText : string, newText : string) : void {
|
||||
textInstance.text = newText;
|
||||
},
|
||||
|
||||
appendChild(parentInstance : Instance | Container, child : Instance | TextInstance) : void {
|
||||
const index = parentInstance.children.indexOf(child);
|
||||
if (index !== -1) {
|
||||
parentInstance.children.splice(index, 1);
|
||||
}
|
||||
parentInstance.children.push(child);
|
||||
},
|
||||
|
||||
insertBefore(
|
||||
parentInstance : Instance | Container,
|
||||
child : Instance | TextInstance,
|
||||
beforeChild : Instance | TextInstance
|
||||
) : void {
|
||||
const index = parentInstance.children.indexOf(child);
|
||||
if (index !== -1) {
|
||||
parentInstance.children.splice(index, 1);
|
||||
}
|
||||
const beforeIndex = parentInstance.children.indexOf(beforeChild);
|
||||
parentInstance.children.splice(beforeIndex, 0, child);
|
||||
},
|
||||
|
||||
removeChild(parentInstance : Instance | Container, child : Instance | TextInstance) : void {
|
||||
const index = parentInstance.children.indexOf(child);
|
||||
parentInstance.children.splice(index, 1);
|
||||
},
|
||||
|
||||
scheduleAnimationCallback(fn : Function) : void {
|
||||
setTimeout(fn);
|
||||
},
|
||||
|
||||
scheduleDeferredCallback(fn : Function) : void {
|
||||
setTimeout(fn, 0, {timeRemaining: Infinity});
|
||||
},
|
||||
|
||||
useSyncScheduling: true,
|
||||
|
||||
getPublicInstance(inst) {
|
||||
switch (inst.tag) {
|
||||
case 'INSTANCE':
|
||||
const createNodeMock = inst.rootContainerInstance.createNodeMock;
|
||||
return createNodeMock({
|
||||
type: inst.type,
|
||||
props: inst.props,
|
||||
});
|
||||
default:
|
||||
return inst;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
var defaultTestOptions = {
|
||||
createNodeMock: function() {
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
function toJSON(inst : Instance | TextInstance) : ReactTestRendererNode {
|
||||
switch (inst.tag) {
|
||||
case 'TEXT':
|
||||
return inst.text;
|
||||
case 'INSTANCE':
|
||||
/* eslint-disable no-unused-vars */
|
||||
// We don't include the `children` prop in JSON.
|
||||
// Instead, we will include the actual rendered children.
|
||||
const {children, ...props} = inst.props;
|
||||
/* eslint-enable */
|
||||
let renderedChildren = null;
|
||||
if (inst.children && inst.children.length) {
|
||||
renderedChildren = inst.children.map(toJSON);
|
||||
}
|
||||
const json : ReactTestRendererJSON = {
|
||||
type: inst.type,
|
||||
props: props,
|
||||
children: renderedChildren,
|
||||
};
|
||||
Object.defineProperty(json, '$$typeof', {value: Symbol.for('react.test.json')});
|
||||
return json;
|
||||
default:
|
||||
throw new Error(`Unexpected node type in toJSON: ${inst.tag}`);
|
||||
}
|
||||
}
|
||||
|
||||
var ReactTestFiberRenderer = {
|
||||
create(element : ReactElement<any>, options : TestRendererOptions) {
|
||||
var createNodeMock = defaultTestOptions.createNodeMock;
|
||||
if (options && typeof options.createNodeMock === 'function') {
|
||||
createNodeMock = options.createNodeMock;
|
||||
}
|
||||
var container = {
|
||||
children: [],
|
||||
createNodeMock,
|
||||
tag: 'CONTAINER',
|
||||
};
|
||||
var root = TestRenderer.createContainer(container);
|
||||
TestRenderer.updateContainer(element, root, null, null);
|
||||
|
||||
return {
|
||||
toJSON() {
|
||||
if (root == null || container == null) {
|
||||
return null;
|
||||
}
|
||||
if (container.children.length === 0) {
|
||||
return null;
|
||||
}
|
||||
if (container.children.length === 1) {
|
||||
return toJSON(container.children[0]);
|
||||
}
|
||||
return container.children.map(toJSON);
|
||||
},
|
||||
update(newElement : ReactElement<any>) {
|
||||
if (root == null) {
|
||||
return;
|
||||
}
|
||||
TestRenderer.updateContainer(newElement, root, null, null);
|
||||
},
|
||||
unmount() {
|
||||
if (root == null) {
|
||||
return;
|
||||
}
|
||||
TestRenderer.updateContainer(null, root, null);
|
||||
container = null;
|
||||
root = null;
|
||||
},
|
||||
getInstance() {
|
||||
if (root == null) {
|
||||
return null;
|
||||
}
|
||||
return TestRenderer.getPublicRootInstance(root);
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
unstable_batchedUpdates: ReactGenericBatching.batchedUpdates,
|
||||
/* eslint-enable camelcase */
|
||||
};
|
||||
|
||||
module.exports = ReactTestFiberRenderer;
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
var React = require('React');
|
||||
var ReactTestRenderer = require('ReactTestRenderer');
|
||||
var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
|
||||
|
||||
describe('ReactTestRenderer', () => {
|
||||
function normalizeCodeLocInfo(str) {
|
||||
@@ -55,6 +56,31 @@ describe('ReactTestRenderer', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('can render a composite component', () => {
|
||||
class Component extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="purple">
|
||||
<Child />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
var Child = () => {
|
||||
return <moo />;
|
||||
};
|
||||
|
||||
var renderer = ReactTestRenderer.create(<Component />);
|
||||
expect(renderer.toJSON()).toEqual({
|
||||
type: 'div',
|
||||
props: { className: 'purple' },
|
||||
children: [
|
||||
{ type: 'moo', props: {}, children: null },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('renders some basics with an update', () => {
|
||||
var renders = 0;
|
||||
|
||||
@@ -92,7 +118,7 @@ describe('ReactTestRenderer', () => {
|
||||
type: 'div',
|
||||
props: { className: 'purple' },
|
||||
children: [
|
||||
7,
|
||||
ReactDOMFeatureFlags.useFiber ? '7' : 7,
|
||||
{ type: 'moo', props: {}, children: null },
|
||||
],
|
||||
});
|
||||
@@ -124,8 +150,8 @@ describe('ReactTestRenderer', () => {
|
||||
mouse.handleMoose();
|
||||
expect(renderer.toJSON()).toEqual({
|
||||
type: 'div',
|
||||
props: {},
|
||||
children: ['moose'],
|
||||
props: {},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -366,8 +392,7 @@ describe('ReactTestRenderer', () => {
|
||||
{createNodeMock}
|
||||
);
|
||||
inst.update(<Foo useDiv={false} />);
|
||||
// It's called with 'div' twice (mounting and unmounting)
|
||||
expect(log).toEqual(['div', 'div', 'span']);
|
||||
expect(log).toEqual(['div', 'span']);
|
||||
});
|
||||
|
||||
it('supports error boundaries', () => {
|
||||
@@ -420,12 +445,107 @@ describe('ReactTestRenderer', () => {
|
||||
props: {},
|
||||
children: ['Happy Birthday!'],
|
||||
});
|
||||
expect(log).toEqual([
|
||||
'Boundary render',
|
||||
'Angry render',
|
||||
'Boundary render',
|
||||
'Boundary componentDidMount',
|
||||
]);
|
||||
if (ReactDOMFeatureFlags.useFiber) {
|
||||
expect(log).toEqual([
|
||||
'Boundary render',
|
||||
'Angry render',
|
||||
'Boundary componentDidMount',
|
||||
'Boundary render',
|
||||
]);
|
||||
} else {
|
||||
expect(log).toEqual([
|
||||
'Boundary render',
|
||||
'Angry render',
|
||||
'Boundary render',
|
||||
'Boundary componentDidMount',
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
it('can update text nodes', () => {
|
||||
class Component extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
var renderer = ReactTestRenderer.create(<Component>Hi</Component>);
|
||||
expect(renderer.toJSON()).toEqual({
|
||||
type: 'div',
|
||||
children: ['Hi'],
|
||||
props: {},
|
||||
});
|
||||
renderer.update(<Component>{['Hi', 'Bye']}</Component>);
|
||||
expect(renderer.toJSON()).toEqual({
|
||||
type: 'div',
|
||||
children: ['Hi', 'Bye'],
|
||||
props: {},
|
||||
});
|
||||
renderer.update(<Component>Bye</Component>);
|
||||
expect(renderer.toJSON()).toEqual({
|
||||
type: 'div',
|
||||
children: ['Bye'],
|
||||
props: {},
|
||||
});
|
||||
renderer.update(<Component>{42}</Component>);
|
||||
expect(renderer.toJSON()).toEqual({
|
||||
type: 'div',
|
||||
children: [
|
||||
ReactDOMFeatureFlags.useFiber ? '42' : 42,
|
||||
],
|
||||
props: {},
|
||||
});
|
||||
renderer.update(<Component><div /></Component>);
|
||||
expect(renderer.toJSON()).toEqual({
|
||||
type: 'div',
|
||||
children: [{
|
||||
type: 'div',
|
||||
children: null,
|
||||
props: {},
|
||||
}],
|
||||
props: {},
|
||||
});
|
||||
});
|
||||
|
||||
if (ReactDOMFeatureFlags.useFiber) {
|
||||
it('can update text nodes when rendered as root', () => {
|
||||
var renderer = ReactTestRenderer.create(['Hello', 'world']);
|
||||
expect(renderer.toJSON()).toEqual(['Hello', 'world']);
|
||||
renderer.update(42);
|
||||
expect(renderer.toJSON()).toEqual('42');
|
||||
renderer.update([42, 'world']);
|
||||
expect(renderer.toJSON()).toEqual(['42', 'world']);
|
||||
});
|
||||
|
||||
it('can render and update root fragments', () => {
|
||||
var Component = (props) => props.children;
|
||||
|
||||
var renderer = ReactTestRenderer.create([
|
||||
<Component>Hi</Component>,
|
||||
<Component>Bye</Component>,
|
||||
]);
|
||||
expect(renderer.toJSON()).toEqual(['Hi', 'Bye']);
|
||||
renderer.update(<div />);
|
||||
expect(renderer.toJSON()).toEqual({
|
||||
type: 'div',
|
||||
children: null,
|
||||
props: {},
|
||||
});
|
||||
renderer.update([<div>goodbye</div>, 'world']);
|
||||
expect(renderer.toJSON()).toEqual([
|
||||
{
|
||||
type: 'div',
|
||||
children: [
|
||||
'goodbye',
|
||||
],
|
||||
props: {},
|
||||
},
|
||||
'world',
|
||||
]);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Copyright 2013-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @providesModule ReactTestRendererStack
|
||||
* @preventMunge
|
||||
* @flow
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var ReactComponentEnvironment = require('ReactComponentEnvironment');
|
||||
var ReactDefaultBatchingStrategy = require('ReactDefaultBatchingStrategy');
|
||||
var ReactEmptyComponent = require('ReactEmptyComponent');
|
||||
var ReactMultiChild = require('ReactMultiChild');
|
||||
var ReactHostComponent = require('ReactHostComponent');
|
||||
var ReactTestMount = require('ReactTestMount');
|
||||
var ReactTestReconcileTransaction = require('ReactTestReconcileTransaction');
|
||||
var ReactUpdates = require('ReactUpdates');
|
||||
var ReactTestTextComponent = require('ReactTestTextComponent');
|
||||
var ReactTestEmptyComponent = require('ReactTestEmptyComponent');
|
||||
var invariant = require('invariant');
|
||||
|
||||
import type { ReactElement } from 'ReactElementType';
|
||||
import type { ReactInstance } from 'ReactInstanceType';
|
||||
|
||||
type ReactTestRendererJSON = {
|
||||
type: string,
|
||||
props: { [propName: string]: string },
|
||||
children: null | Array<string | ReactTestRendererJSON>,
|
||||
$$typeof?: any
|
||||
}
|
||||
|
||||
/**
|
||||
* Drill down (through composites and empty components) until we get a native or
|
||||
* native text component.
|
||||
*
|
||||
* This is pretty polymorphic but unavoidable with the current structure we have
|
||||
* for `_renderedChildren`.
|
||||
*/
|
||||
function getRenderedHostOrTextFromComponent(component) {
|
||||
var rendered;
|
||||
while ((rendered = component._renderedComponent)) {
|
||||
component = rendered;
|
||||
}
|
||||
return component;
|
||||
}
|
||||
|
||||
var UNSET = {};
|
||||
|
||||
class ReactTestComponent {
|
||||
_currentElement: ReactElement;
|
||||
_renderedChildren: null | Object;
|
||||
_topLevelWrapper: null | ReactInstance;
|
||||
_hostContainerInfo: null | Object;
|
||||
_nodeMock: Object;
|
||||
|
||||
constructor(element: ReactElement) {
|
||||
this._currentElement = element;
|
||||
this._renderedChildren = null;
|
||||
this._topLevelWrapper = null;
|
||||
this._hostContainerInfo = null;
|
||||
this._nodeMock = UNSET;
|
||||
}
|
||||
|
||||
mountComponent(
|
||||
transaction: ReactTestReconcileTransaction,
|
||||
nativeParent: null | ReactTestComponent,
|
||||
hostContainerInfo: Object,
|
||||
context: Object,
|
||||
) {
|
||||
var element = this._currentElement;
|
||||
this._hostContainerInfo = hostContainerInfo;
|
||||
this._nodeMock = hostContainerInfo.createNodeMock(element);
|
||||
// $FlowFixMe https://github.com/facebook/flow/issues/1805
|
||||
this.mountChildren(element.props.children, transaction, context);
|
||||
}
|
||||
|
||||
receiveComponent(
|
||||
nextElement: ReactElement,
|
||||
transaction: ReactTestReconcileTransaction,
|
||||
context: Object,
|
||||
) {
|
||||
this._currentElement = nextElement;
|
||||
// $FlowFixMe https://github.com/facebook/flow/issues/1805
|
||||
this.updateChildren(nextElement.props.children, transaction, context);
|
||||
}
|
||||
|
||||
getPublicInstance(): Object {
|
||||
invariant(
|
||||
this._nodeMock !== UNSET,
|
||||
'getPublicInstance should not be called before component is mounted.'
|
||||
);
|
||||
return this._nodeMock;
|
||||
}
|
||||
|
||||
toJSON(): ReactTestRendererJSON {
|
||||
// not using `children`, but I don't want to rewrite without destructuring
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
var {children, ...props} = this._currentElement.props;
|
||||
var childrenJSON = [];
|
||||
for (var key in this._renderedChildren) {
|
||||
var inst = this._renderedChildren[key];
|
||||
inst = getRenderedHostOrTextFromComponent(inst);
|
||||
var json = inst.toJSON();
|
||||
if (json !== undefined) {
|
||||
childrenJSON.push(json);
|
||||
}
|
||||
}
|
||||
var object: ReactTestRendererJSON = {
|
||||
type: this._currentElement.type,
|
||||
props: props,
|
||||
children: childrenJSON.length ? childrenJSON : null,
|
||||
};
|
||||
Object.defineProperty(object, '$$typeof', {
|
||||
value: Symbol.for('react.test.json'),
|
||||
});
|
||||
return object;
|
||||
}
|
||||
|
||||
getHostNode(): void {}
|
||||
unmountComponent(safely, skipLifecycle): void {
|
||||
// $FlowFixMe https://github.com/facebook/flow/issues/1805
|
||||
this.unmountChildren(safely, skipLifecycle);
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(ReactTestComponent.prototype, ReactMultiChild);
|
||||
|
||||
// =============================================================================
|
||||
|
||||
ReactUpdates.injection.injectReconcileTransaction(
|
||||
ReactTestReconcileTransaction
|
||||
);
|
||||
ReactUpdates.injection.injectBatchingStrategy(ReactDefaultBatchingStrategy);
|
||||
|
||||
ReactHostComponent.injection.injectGenericComponentClass(ReactTestComponent);
|
||||
ReactHostComponent.injection.injectTextComponentClass(ReactTestTextComponent);
|
||||
ReactEmptyComponent.injection.injectEmptyComponentFactory(function() {
|
||||
return new ReactTestEmptyComponent();
|
||||
});
|
||||
|
||||
ReactComponentEnvironment.injection.injectEnvironment({
|
||||
processChildrenUpdates: function() {},
|
||||
replaceNodeWithMarkup: function() {},
|
||||
});
|
||||
|
||||
var ReactTestRenderer = {
|
||||
create: ReactTestMount.render,
|
||||
/* eslint-disable camelcase */
|
||||
unstable_batchedUpdates: ReactUpdates.batchedUpdates,
|
||||
/* eslint-enable camelcase */
|
||||
};
|
||||
|
||||
module.exports = ReactTestRenderer;
|
||||
@@ -859,11 +859,9 @@ babel@^5.4.7:
|
||||
slash "^1.0.0"
|
||||
source-map "^0.5.0"
|
||||
|
||||
babylon@6.8.0:
|
||||
version "6.8.0"
|
||||
resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.8.0.tgz#c1057f7bf703620dc04ddb69cd59ade961b87cb0"
|
||||
dependencies:
|
||||
babel-runtime "^6.0.0"
|
||||
babylon@6.15.0:
|
||||
version "6.15.0"
|
||||
resolved "http://registry.yarnpkg.com/babylon/-/babylon-6.15.0.tgz#ba65cfa1a80e1759b0e89fb562e27dccae70348e"
|
||||
|
||||
babylon@^5.8.38:
|
||||
version "5.8.38"
|
||||
|
||||
Reference in New Issue
Block a user