@samitouri / QOS-React / commits / caec8d5ce7

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