@samitouri / QOS-React / commits / 50982cea99

Add rudimentary test renderer (#6944)

Ben Alpert committed Jun 1, 2016 at 11:17 UTC 50982cea99fcb12d9a2f4bda82ab6735b5fc0149
4 files changed +488
src/renderers/testing/ReactTestMount.js new
+155
@@ -0,0 +1,155 @@
1 +/**
2 + * Copyright (c) 2015-present, Facebook, Inc.
3 + * All rights reserved.
4 + *
5 + * This source code is licensed under the BSD-style license found in the
6 + * LICENSE file in the root directory of this source tree. An additional grant
7 + * of patent rights can be found in the PATENTS file in the same directory.
8 + *
9 + * @providesModule ReactTestMount
10 + * @flow
11 + */
12 +'use strict';
13 +
14 +var ReactElement = require('ReactElement');
15 +var ReactInstrumentation = require('ReactInstrumentation');
16 +var ReactReconciler = require('ReactReconciler');
17 +var ReactUpdates = require('ReactUpdates');
18 +
19 +var emptyObject = require('emptyObject');
20 +var getHostComponentFromComposite = require('getHostComponentFromComposite');
21 +var instantiateReactComponent = require('instantiateReactComponent');
22 +
23 +/**
24 + * Temporary (?) hack so that we can store all top-level pending updates on
25 + * composites instead of having to worry about different types of components
26 + * here.
27 + */
28 +var TopLevelWrapper = function() {};
29 +TopLevelWrapper.prototype.isReactComponent = {};
30 +if (__DEV__) {
31 + TopLevelWrapper.displayName = 'TopLevelWrapper';
32 +}
33 +TopLevelWrapper.prototype.render = function() {
34 + // this.props is actually a ReactElement
35 + return this.props;
36 +};
37 +
38 +/**
39 + * Mounts this component and inserts it into the DOM.
40 + *
41 + * @param {ReactComponent} componentInstance The instance to mount.
42 + * @param {number} rootID ID of the root node.
43 + * @param {number} containerTag container element to mount into.
44 + * @param {ReactReconcileTransaction} transaction
45 + */
46 +function mountComponentIntoNode(
47 + componentInstance,
48 + transaction) {
49 + var image = ReactReconciler.mountComponent(
50 + componentInstance,
51 + transaction,
52 + null,
53 + null,
54 + emptyObject
55 + );
56 + componentInstance._renderedComponent._topLevelWrapper = componentInstance;
57 + return image;
58 +}
59 +
60 +/**
61 + * Batched mount.
62 + *
63 + * @param {ReactComponent} componentInstance The instance to mount.
64 + * @param {number} rootID ID of the root node.
65 + * @param {number} containerTag container element to mount into.
66 + */
67 +function batchedMountComponentIntoNode(
68 + componentInstance) {
69 + var transaction = ReactUpdates.ReactReconcileTransaction.getPooled();
70 + var image = transaction.perform(
71 + mountComponentIntoNode,
72 + null,
73 + componentInstance,
74 + transaction
75 + );
76 + ReactUpdates.ReactReconcileTransaction.release(transaction);
77 + return image;
78 +}
79 +
80 +var ReactTestInstance = function(component) {
81 + this._component = component;
82 +};
83 +ReactTestInstance.prototype.getInstance = function() {
84 + return this._component._renderedComponent.getPublicInstance();
85 +};
86 +ReactTestInstance.prototype.toJSON = function() {
87 + var inst = getHostComponentFromComposite(this._component);
88 + return inst.toJSON();
89 +};
90 +
91 +/**
92 + * As soon as `ReactMount` is refactored to not rely on the DOM, we can share
93 + * code between the two. For now, we'll hard code the ID logic.
94 + */
95 +var ReactHostMount = {
96 +
97 + render: function(
98 + nextElement: ReactElement
99 + ): ?ReactComponent {
100 + var nextWrappedElement = new ReactElement(
101 + TopLevelWrapper,
102 + null,
103 + null,
104 + null,
105 + null,
106 + null,
107 + nextElement
108 + );
109 +
110 + // var prevComponent = ReactHostMount._instancesByContainerID[containerTag];
111 + // if (prevComponent) {
112 + // var prevWrappedElement = prevComponent._currentElement;
113 + // var prevElement = prevWrappedElement.props;
114 + // if (shouldUpdateReactComponent(prevElement, nextElement)) {
115 + // ReactUpdateQueue.enqueueElementInternal(prevComponent, nextWrappedElement);
116 + // if (callback) {
117 + // ReactUpdateQueue.enqueueCallbackInternal(prevComponent, callback);
118 + // }
119 + // return prevComponent;
120 + // }
121 + // }
122 +
123 + var instance = instantiateReactComponent(nextWrappedElement);
124 +
125 + if (__DEV__) {
126 + // Mute future events from the top level wrapper.
127 + // It is an implementation detail that devtools should not know about.
128 + instance._debugID = 0;
129 +
130 + if (__DEV__) {
131 + ReactInstrumentation.debugTool.onBeginFlush();
132 + }
133 + }
134 +
135 + // The initial render is synchronous but any updates that happen during
136 + // rendering, in componentWillMount or componentDidMount, will be batched
137 + // according to the current batching strategy.
138 +
139 + ReactUpdates.batchedUpdates(
140 + batchedMountComponentIntoNode,
141 + instance
142 + );
143 + if (__DEV__) {
144 + // The instance here is TopLevelWrapper so we report mount for its child.
145 + ReactInstrumentation.debugTool.onMountRootComponent(
146 + instance._renderedComponent._debugID
147 + );
148 + ReactInstrumentation.debugTool.onEndFlush();
149 + }
150 + return new ReactTestInstance(instance);
151 + },
152 +
153 +};
154 +
155 +module.exports = ReactHostMount;
src/renderers/testing/ReactTestReconcileTransaction.js new
+103
@@ -0,0 +1,103 @@
1 +/**
2 + * Copyright (c) 2015-present, Facebook, Inc.
3 + * All rights reserved.
4 + *
5 + * This source code is licensed under the BSD-style license found in the
6 + * LICENSE file in the root directory of this source tree. An additional grant
7 + * of patent rights can be found in the PATENTS file in the same directory.
8 + *
9 + * @providesModule ReactTestReconcileTransaction
10 + * @flow
11 + */
12 +'use strict';
13 +
14 +var CallbackQueue = require('CallbackQueue');
15 +var PooledClass = require('PooledClass');
16 +var Transaction = require('Transaction');
17 +
18 +/**
19 + * Provides a `CallbackQueue` queue for collecting `onDOMReady` callbacks during
20 + * the performing of the transaction.
21 + */
22 +var ON_DOM_READY_QUEUEING = {
23 + /**
24 + * Initializes the internal `onDOMReady` queue.
25 + */
26 + initialize: function() {
27 + this.reactMountReady.reset();
28 + },
29 +
30 + /**
31 + * After DOM is flushed, invoke all registered `onDOMReady` callbacks.
32 + */
33 + close: function() {
34 + this.reactMountReady.notifyAll();
35 + },
36 +};
37 +
38 +/**
39 + * Executed within the scope of the `Transaction` instance. Consider these as
40 + * being member methods, but with an implied ordering while being isolated from
41 + * each other.
42 + */
43 +var TRANSACTION_WRAPPERS = [ON_DOM_READY_QUEUEING];
44 +
45 +/**
46 + * Currently:
47 + * - The order that these are listed in the transaction is critical:
48 + * - Suppresses events.
49 + * - Restores selection range.
50 + *
51 + * Future:
52 + * - Restore document/overflow scroll positions that were unintentionally
53 + * modified via DOM insertions above the top viewport boundary.
54 + * - Implement/integrate with customized constraint based layout system and keep
55 + * track of which dimensions must be remeasured.
56 + *
57 + * @class ReactTestReconcileTransaction
58 + */
59 +function ReactTestReconcileTransaction() {
60 + this.reinitializeTransaction();
61 + this.reactMountReady = CallbackQueue.getPooled(null);
62 +}
63 +
64 +var Mixin = {
65 + /**
66 + * @see Transaction
67 + * @abstract
68 + * @final
69 + * @return {array<object>} List of operation wrap procedures.
70 + * TODO: convert to array<TransactionWrapper>
71 + */
72 + getTransactionWrappers: function() {
73 + return TRANSACTION_WRAPPERS;
74 + },
75 +
76 + /**
77 + * @return {object} The queue to collect `onDOMReady` callbacks with.
78 + * TODO: convert to ReactMountReady
79 + */
80 + getReactMountReady: function() {
81 + return this.reactMountReady;
82 + },
83 +
84 + /**
85 + * `PooledClass` looks for this, and will invoke this before allowing this
86 + * instance to be reused.
87 + */
88 + destructor: function() {
89 + CallbackQueue.release(this.reactMountReady);
90 + this.reactMountReady = null;
91 + },
92 +};
93 +
94 +Object.assign(
95 + ReactTestReconcileTransaction.prototype,
96 + Transaction.Mixin,
97 + ReactTestReconcileTransaction,
98 + Mixin
99 +);
100 +
101 +PooledClass.addPoolingTo(ReactTestReconcileTransaction);
102 +
103 +module.exports = ReactTestReconcileTransaction;
src/renderers/testing/ReactTestRenderer.js new
+133
@@ -0,0 +1,133 @@
1 +/**
2 + * Copyright 2013-present, Facebook, Inc.
3 + * All rights reserved.
4 + *
5 + * This source code is licensed under the BSD-style license found in the
6 + * LICENSE file in the root directory of this source tree. An additional grant
7 + * of patent rights can be found in the PATENTS file in the same directory.
8 + *
9 + * @providesModule ReactTestRenderer
10 + */
11 +
12 +'use strict';
13 +
14 +var ReactDefaultBatchingStrategy = require('ReactDefaultBatchingStrategy');
15 +var ReactEmptyComponent = require('ReactEmptyComponent');
16 +var ReactMultiChild = require('ReactMultiChild');
17 +var ReactHostComponent = require('ReactHostComponent');
18 +var ReactTestMount = require('ReactTestMount');
19 +var ReactTestReconcileTransaction = require('ReactTestReconcileTransaction');
20 +var ReactUpdates = require('ReactUpdates');
21 +
22 +var renderSubtreeIntoContainer = require('renderSubtreeIntoContainer');
23 +
24 +/**
25 + * Drill down (through composites and empty components) until we get a native or
26 + * native text component.
27 + *
28 + * This is pretty polymorphic but unavoidable with the current structure we have
29 + * for `_renderedChildren`.
30 + */
31 +function getRenderedHostOrTextFromComponent(component) {
32 + var rendered;
33 + while ((rendered = component._renderedComponent)) {
34 + component = rendered;
35 + }
36 + return component;
37 +}
38 +
39 +
40 +// =============================================================================
41 +
42 +var ReactTestComponent = function(element) {
43 + this._currentElement = element;
44 + this._renderedChildren = null;
45 + this._topLevelWrapper = null;
46 +};
47 +ReactTestComponent.prototype.mountComponent = function(
48 + transaction,
49 + nativeParent,
50 + nativeContainerInfo,
51 + context
52 +) {
53 + var element = this._currentElement;
54 + this.mountChildren(element.props.children, transaction, context);
55 +};
56 +ReactTestComponent.prototype.receiveComponent = function(
57 + nextElement,
58 + transaction,
59 + context
60 +) {
61 + this._currentElement = nextElement;
62 + this.updateChildren(nextElement.props.children, transaction, context);
63 +};
64 +ReactTestComponent.prototype.getHostNode = function() {};
65 +ReactTestComponent.prototype.unmountComponent = function() {};
66 +ReactTestComponent.prototype.toJSON = function() {
67 + var {children, ...props} = this._currentElement.props;
68 + var childrenJSON = [];
69 + for (var key in this._renderedChildren) {
70 + var inst = this._renderedChildren[key];
71 + inst = getRenderedHostOrTextFromComponent(inst);
72 + var json = inst.toJSON();
73 + if (json !== undefined) {
74 + childrenJSON.push(json);
75 + }
76 + }
77 + return {
78 + type: this._currentElement.type,
79 + props: props,
80 + children: childrenJSON.length ? childrenJSON : null,
81 + };
82 +};
83 +Object.assign(ReactTestComponent.prototype, ReactMultiChild.Mixin);
84 +
85 +// =============================================================================
86 +
87 +var ReactTestTextComponent = function(element) {
88 + this._currentElement = element;
89 +};
90 +ReactTestTextComponent.prototype.mountComponent = function() {};
91 +ReactTestTextComponent.prototype.receiveComponent = function(nextElement) {
92 + this._currentElement = nextElement;
93 +};
94 +ReactTestTextComponent.prototype.getHostNode = function() {};
95 +ReactTestTextComponent.prototype.unmountComponent = function() {};
96 +ReactTestTextComponent.prototype.toJSON = function() {
97 + return this._currentElement;
98 +};
99 +
100 +// =============================================================================
101 +
102 +var ReactTestEmptyComponent = function(element) {
103 + this._currentElement = null;
104 +};
105 +ReactTestEmptyComponent.prototype.mountComponent = function() {};
106 +ReactTestEmptyComponent.prototype.receiveComponent = function() {};
107 +ReactTestEmptyComponent.prototype.getHostNode = function() {};
108 +ReactTestEmptyComponent.prototype.unmountComponent = function() {};
109 +ReactTestEmptyComponent.prototype.toJSON = function() {};
110 +
111 +// =============================================================================
112 +
113 +ReactUpdates.injection.injectReconcileTransaction(
114 + ReactTestReconcileTransaction
115 +);
116 +ReactUpdates.injection.injectBatchingStrategy(ReactDefaultBatchingStrategy);
117 +
118 +ReactHostComponent.injection.injectGenericComponentClass(ReactTestComponent);
119 +ReactHostComponent.injection.injectTextComponentClass(ReactTestTextComponent);
120 +ReactEmptyComponent.injection.injectEmptyComponentFactory(function() {
121 + return new ReactTestEmptyComponent();
122 +});
123 +
124 +var ReactTestRenderer = {
125 + create: ReactTestMount.render,
126 +
127 + /* eslint-disable camelcase */
128 + unstable_batchedUpdates: ReactUpdates.batchedUpdates,
129 + unstable_renderSubtreeIntoContainer: renderSubtreeIntoContainer,
130 + /* eslint-enable camelcase */
131 +};
132 +
133 +module.exports = ReactTestRenderer;
src/renderers/testing/__tests__/ReactTestRenderer-test.js new
+97
@@ -0,0 +1,97 @@
1 +/**
2 + * Copyright 2013-present, Facebook, Inc.
3 + * All rights reserved.
4 + *
5 + * This source code is licensed under the BSD-style license found in the
6 + * LICENSE file in the root directory of this source tree. An additional grant
7 + * of patent rights can be found in the PATENTS file in the same directory.
8 + *
9 + * @emails react-core
10 + */
11 +
12 +'use strict';
13 +
14 +var React = require('React');
15 +var ReactTestRenderer = require('ReactTestRenderer');
16 +
17 +describe('ReactTestRenderer', function() {
18 +
19 + it('renders a simple component', function() {
20 + function Link() {
21 + return <a role="link" />;
22 + }
23 + var renderer = ReactTestRenderer.create(<Link />);
24 + expect(renderer.toJSON()).toEqual({
25 + type: 'a',
26 + props: { role: 'link' },
27 + children: null,
28 + });
29 + });
30 +
31 + it('renders some basics with an update', function() {
32 + var renders = 0;
33 + var Component = React.createClass({
34 + getInitialState: function() {
35 + return {x: 3};
36 + },
37 + render: function() {
38 + renders++;
39 + return (
40 + <div className="purple">
41 + {this.state.x}
42 + <Child />
43 + <Null />
44 + </div>
45 + );
46 + },
47 + componentDidMount: function() {
48 + this.setState({x: 7});
49 + },
50 + });
51 +
52 + var Child = () => (renders++, <moo />);
53 + var Null = () => (renders++, null);
54 +
55 + var renderer = ReactTestRenderer.create(<Component />);
56 + expect(renderer.toJSON()).toEqual({
57 + type: 'div',
58 + props: { className: 'purple' },
59 + children: [
60 + 7,
61 + { type: 'moo', props: {}, children: null },
62 + ],
63 + });
64 + expect(renders).toBe(6);
65 + });
66 +
67 + it('exposes the instance', function() {
68 + class Mouse extends React.Component {
69 + constructor() {
70 + super();
71 + this.state = {mouse: 'mouse'};
72 + }
73 + handleMoose() {
74 + this.setState({mouse: 'moose'});
75 + }
76 + render() {
77 + return <div>{this.state.mouse}</div>;
78 + }
79 + }
80 + var renderer = ReactTestRenderer.create(<Mouse />);
81 +
82 + expect(renderer.toJSON()).toEqual({
83 + type: 'div',
84 + props: {},
85 + children: ['mouse'],
86 + });
87 +
88 + var mouse = renderer.getInstance();
89 + mouse.handleMoose();
90 + expect(renderer.toJSON()).toEqual({
91 + type: 'div',
92 + props: {},
93 + children: ['moose'],
94 + });
95 + });
96 +
97 +});