Skip to content

Commit caec8d5

Browse files
authored
Test renderer improvements (#7258)
Adds `.update(newElement)` and `.unmount()` and makes children reorders and composite type swapping work. Part of #7148.
1 parent e5513ec commit caec8d5

File tree

3 files changed

+143
-15
lines changed

3 files changed

+143
-15
lines changed

src/renderers/testing/ReactTestMount.js

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ var ReactUpdates = require('ReactUpdates');
1919
var emptyObject = require('emptyObject');
2020
var getHostComponentFromComposite = require('getHostComponentFromComposite');
2121
var instantiateReactComponent = require('instantiateReactComponent');
22+
var invariant = require('invariant');
2223

2324
/**
2425
* Temporary (?) hack so that we can store all top-level pending updates on
@@ -83,6 +84,48 @@ var ReactTestInstance = function(component) {
8384
ReactTestInstance.prototype.getInstance = function() {
8485
return this._component._renderedComponent.getPublicInstance();
8586
};
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+
};
86129
ReactTestInstance.prototype.toJSON = function() {
87130
var inst = getHostComponentFromComposite(this._component);
88131
return inst.toJSON();
@@ -92,7 +135,7 @@ ReactTestInstance.prototype.toJSON = function() {
92135
* As soon as `ReactMount` is refactored to not rely on the DOM, we can share
93136
* code between the two. For now, we'll hard code the ID logic.
94137
*/
95-
var ReactHostMount = {
138+
var ReactTestMount = {
96139

97140
render: function(
98141
nextElement: ReactElement
@@ -107,19 +150,6 @@ var ReactHostMount = {
107150
nextElement
108151
);
109152

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-
123153
var instance = instantiateReactComponent(nextWrappedElement, false);
124154

125155
// The initial render is synchronous but any updates that happen during
@@ -141,4 +171,4 @@ var ReactHostMount = {
141171

142172
};
143173

144-
module.exports = ReactHostMount;
174+
module.exports = ReactTestMount;

src/renderers/testing/ReactTestRenderer.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
'use strict';
1313

14+
var ReactComponentEnvironment = require('ReactComponentEnvironment');
1415
var ReactDefaultBatchingStrategy = require('ReactDefaultBatchingStrategy');
1516
var ReactEmptyComponent = require('ReactEmptyComponent');
1617
var ReactMultiChild = require('ReactMultiChild');
@@ -62,6 +63,11 @@ ReactTestComponent.prototype.receiveComponent = function(
6263
this.updateChildren(nextElement.props.children, transaction, context);
6364
};
6465
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+
};
6571
ReactTestComponent.prototype.unmountComponent = function() {};
6672
ReactTestComponent.prototype.toJSON = function() {
6773
var {children, ...props} = this._currentElement.props;
@@ -125,6 +131,11 @@ ReactEmptyComponent.injection.injectEmptyComponentFactory(function() {
125131
return new ReactTestEmptyComponent();
126132
});
127133

134+
ReactComponentEnvironment.injection.injectEnvironment({
135+
processChildrenUpdates: function() {},
136+
replaceNodeWithMarkup: function() {},
137+
});
138+
128139
var ReactTestRenderer = {
129140
create: ReactTestMount.render,
130141

src/renderers/testing/__tests__/ReactTestRenderer-test.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,4 +110,91 @@ describe('ReactTestRenderer', function() {
110110
});
111111
});
112112

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

0 commit comments

Comments
 (0)