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
+});