Test renderer improvements (#7258)
Adds `.update(newElement)` and `.unmount()` and makes children reorders and composite type swapping work. Part of #7148.
Ben Alpert committed
Jul 12, 2016 at 22:35 UTC
caec8d5ce719997c15a4d586ceb771fcfbb5ccf7
3 files changed
+143
-15
src/renderers/testing/ReactTestMount.js
+45
-15
@@ -19,6 +19,7 @@ var ReactUpdates = require('ReactUpdates');
19
var emptyObject = require('emptyObject');
20
var getHostComponentFromComposite = require('getHostComponentFromComposite');
21
var instantiateReactComponent = require('instantiateReactComponent');
22
+var invariant = require('invariant');
23
24
/**
25
* Temporary (?) hack so that we can store all top-level pending updates on
@@ -83,6 +84,48 @@ var ReactTestInstance = function(component) {
84
ReactTestInstance.prototype.getInstance = function() {
85
return this._component._renderedComponent.getPublicInstance();
86
};
87
+ReactTestInstance.prototype.update = function(nextElement) {
88
+ invariant(
89
+ this._component,
90
+ "ReactTestRenderer: .update() can't be called after unmount."
91
+ );
92
+ var nextWrappedElement = new ReactElement(
93
+ TopLevelWrapper,
94
+ null,
95
+ null,
96
+ null,
97
+ null,
98
+ null,
99
+ nextElement
100
+ );
101
+ var component = this._component;
102
+ ReactUpdates.batchedUpdates(function() {
103
+ var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(true);
104
+ transaction.perform(function() {
105
+ ReactReconciler.receiveComponent(
106
+ component,
107
+ nextWrappedElement,
108
+ transaction,
109
+ emptyObject
110
+ );
111
+ });
112
+ ReactUpdates.ReactReconcileTransaction.release(transaction);
113
+ });
114
+};
115
+ReactTestInstance.prototype.unmount = function(nextElement) {
116
+ var component = this._component;
117
+ ReactUpdates.batchedUpdates(function() {
118
+ var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(true);
119
+ transaction.perform(function() {
120
+ ReactReconciler.unmountComponent(
121
+ component,
122
+ false
123
+ );
124
+ });
125
+ ReactUpdates.ReactReconcileTransaction.release(transaction);
126
+ });
127
+ this._component = null;
128
+};
129
ReactTestInstance.prototype.toJSON = function() {
130
var inst = getHostComponentFromComposite(this._component);
131
return inst.toJSON();
@@ -92,7 +135,7 @@ ReactTestInstance.prototype.toJSON = function() {
135
* As soon as `ReactMount` is refactored to not rely on the DOM, we can share
136
* code between the two. For now, we'll hard code the ID logic.
137
*/
95
-var ReactHostMount = {
138
+var ReactTestMount = {
139
140
render: function(
141
nextElement: ReactElement
@@ -107,19 +150,6 @@ var ReactHostMount = {
150
nextElement
151
);
152
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
-
153
var instance = instantiateReactComponent(nextWrappedElement, false);
154
155
// The initial render is synchronous but any updates that happen during
@@ -141,4 +171,4 @@ var ReactHostMount = {
171
172
};
173
144
-module.exports = ReactHostMount;
174
+module.exports = ReactTestMount;
src/renderers/testing/ReactTestRenderer.js
+11
@@ -11,6 +11,7 @@
11
12
'use strict';
13
14
+var ReactComponentEnvironment = require('ReactComponentEnvironment');
15
var ReactDefaultBatchingStrategy = require('ReactDefaultBatchingStrategy');
16
var ReactEmptyComponent = require('ReactEmptyComponent');
17
var ReactMultiChild = require('ReactMultiChild');
@@ -62,6 +63,11 @@ ReactTestComponent.prototype.receiveComponent = function(
63
this.updateChildren(nextElement.props.children, transaction, context);
64
};
65
ReactTestComponent.prototype.getHostNode = function() {};
66
+ReactTestComponent.prototype.getPublicInstance = function() {
67
+ // I can't say this makes a ton of sense but it seems better than throwing.
68
+ // Maybe we'll revise later if someone has a good use case.
69
+ return null;
70
+};
71
ReactTestComponent.prototype.unmountComponent = function() {};
72
ReactTestComponent.prototype.toJSON = function() {
73
var {children, ...props} = this._currentElement.props;
@@ -125,6 +131,11 @@ ReactEmptyComponent.injection.injectEmptyComponentFactory(function() {
131
return new ReactTestEmptyComponent();
132
});
133
134
+ReactComponentEnvironment.injection.injectEnvironment({
135
+ processChildrenUpdates: function() {},
136
+ replaceNodeWithMarkup: function() {},
137
+});
138
+
139
var ReactTestRenderer = {
140
create: ReactTestMount.render,
141
src/renderers/testing/__tests__/ReactTestRenderer-test.js
+87
@@ -110,4 +110,91 @@ describe('ReactTestRenderer', function() {
110
});
111
});
112
113
+ it('updates types', function() {
114
+ var renderer = ReactTestRenderer.create(<div>mouse</div>);
115
+ expect(renderer.toJSON()).toEqual({
116
+ type: 'div',
117
+ props: {},
118
+ children: ['mouse'],
119
+ });
120
+
121
+ renderer.update(<span>mice</span>);
122
+ expect(renderer.toJSON()).toEqual({
123
+ type: 'span',
124
+ props: {},
125
+ children: ['mice'],
126
+ });
127
+ });
128
+
129
+ it('updates children', function() {
130
+ var renderer = ReactTestRenderer.create(
131
+ <div>
132
+ <span key="a">A</span>
133
+ <span key="b">B</span>
134
+ <span key="c">C</span>
135
+ </div>
136
+ );
137
+ expect(renderer.toJSON()).toEqual({
138
+ type: 'div',
139
+ props: {},
140
+ children: [
141
+ {type: 'span', props: {}, children: ['A']},
142
+ {type: 'span', props: {}, children: ['B']},
143
+ {type: 'span', props: {}, children: ['C']},
144
+ ],
145
+ });
146
+
147
+ renderer.update(
148
+ <div>
149
+ <span key="d">D</span>
150
+ <span key="c">C</span>
151
+ <span key="b">B</span>
152
+ </div>
153
+ );
154
+ expect(renderer.toJSON()).toEqual({
155
+ type: 'div',
156
+ props: {},
157
+ children: [
158
+ {type: 'span', props: {}, children: ['D']},
159
+ {type: 'span', props: {}, children: ['C']},
160
+ {type: 'span', props: {}, children: ['B']},
161
+ ],
162
+ });
163
+ });
164
+
165
+ it('does the full lifecycle', function() {
166
+ var log = [];
167
+ class Log extends React.Component {
168
+ render() {
169
+ log.push('render ' + this.props.name);
170
+ return <div />;
171
+ }
172
+ componentDidMount() {
173
+ log.push('mount ' + this.props.name);
174
+ }
175
+ componentWillUnmount() {
176
+ log.push('unmount ' + this.props.name);
177
+ }
178
+ }
179
+
180
+ var renderer = ReactTestRenderer.create(<Log key="foo" name="Foo" />);
181
+ renderer.update(<Log key="bar" name="Bar" />);
182
+ renderer.unmount();
183
+
184
+ expect(log).toEqual([
185
+ 'render Foo',
186
+ 'mount Foo',
187
+ 'unmount Foo',
188
+ 'render Bar',
189
+ 'mount Bar',
190
+ 'unmount Bar',
191
+ ]);
192
+ });
193
+
194
+ it('gives a ref to native components', function() {
195
+ var log = [];
196
+ ReactTestRenderer.create(<div ref={(r) => log.push(r)} />);
197
+ expect(log).toEqual([null]);
198
+ });
199
+
200
});