Implement createNodeMock for ReactTestRenderer (#7649)
* Implement optional mockConfig and getMockRef * default mockConfig, walk render tree * Pass mockConfig to transaction * Attach mockConfig to transaction * type mockConfig in ReactRef * Expect object in native component ref test * Fix argument name for attachRefs * Add mockConfig support to legacy refs * Pass transaction to getPublicInstance * Implement getMockConfig on ReactTestReconcileTransaction * Merge defaultMockConfig and mockConfig options * Rename mockConfig to testOptions * Break getPublicInstnce into three lines * createMockRef -> createNodeMock
Brandon Dail committed
Sep 13, 2016 at 09:09 UTC
f3569a2c311808d4d6d2b9dcf7ec6df064bce76e
9 files changed
+143
-26
src/renderers/shared/stack/reconciler/ReactCompositeComponent.js
+2
-2
@@ -1100,10 +1100,10 @@ var ReactCompositeComponent = {
1100
* @final
1101
* @private
1102
*/
1103
- attachRef: function(ref, component) {
1103
+ attachRef: function(ref, component, transaction) {
1104
var inst = this.getPublicInstance();
1105
invariant(inst != null, 'Stateless function components cannot have refs.');
1106
- var publicComponentInstance = component.getPublicInstance();
1106
+ var publicComponentInstance = component.getPublicInstance(transaction);
1107
if (__DEV__) {
1108
var componentName = component && component.getName ?
1109
component.getName() : 'a component';
src/renderers/shared/stack/reconciler/ReactOwner.js
+2
-1
@@ -73,6 +73,7 @@ var ReactOwner = {
73
component: ReactInstance,
74
ref: string,
75
owner: ReactInstance,
76
+ transaction,
77
): void {
78
invariant(
79
isValidOwner(owner),
@@ -81,7 +82,7 @@ var ReactOwner = {
82
'`render` method, or you have multiple copies of React loaded ' +
83
'(details: https://fb.me/react-refs-must-have-owner).'
84
);
84
- owner.attachRef(ref, component);
85
+ owner.attachRef(ref, component, transaction);
86
},
87
88
/**
src/renderers/shared/stack/reconciler/ReactReconciler.js
+6
-2
@@ -20,8 +20,12 @@ var warning = require('warning');
20
* Helper to call ReactRef.attachRefs with this composite component, split out
21
* to avoid allocations in the transaction mount-ready queue.
22
*/
23
-function attachRefs() {
24
- ReactRef.attachRefs(this, this._currentElement);
23
+function attachRefs(transaction) {
24
+ ReactRef.attachRefs(
25
+ this,
26
+ this._currentElement,
27
+ transaction,
28
+ );
29
}
30
31
var ReactReconciler = {
src/renderers/shared/stack/reconciler/ReactRef.js
+11
-4
@@ -16,15 +16,21 @@ var ReactOwner = require('ReactOwner');
16
17
import type { ReactInstance } from 'ReactInstanceType';
18
import type { ReactElement } from 'ReactElementType';
19
+import type { Transaction } from 'Transaction';
20
21
var ReactRef = {};
22
22
-function attachRef(ref, component, owner) {
23
+function attachRef(ref, component, owner, transaction) {
24
if (typeof ref === 'function') {
24
- ref(component.getPublicInstance());
25
+ ref(component.getPublicInstance(transaction));
26
} else {
27
// Legacy ref
27
- ReactOwner.addComponentAsRefTo(component, ref, owner);
28
+ ReactOwner.addComponentAsRefTo(
29
+ component,
30
+ ref,
31
+ owner,
32
+ transaction,
33
+ );
34
}
35
}
36
@@ -40,13 +46,14 @@ function detachRef(ref, component, owner) {
46
ReactRef.attachRefs = function(
47
instance: ReactInstance,
48
element: ReactElement | string | number | null | false,
49
+ transaction: Transaction,
50
): void {
51
if (element === null || typeof element !== 'object') {
52
return;
53
}
54
var ref = element.ref;
55
if (ref != null) {
49
- attachRef(ref, instance, element._owner);
56
+ attachRef(ref, instance, element._owner, transaction);
57
}
58
};
59
src/renderers/shared/utils/CallbackQueue.js
+5
-2
@@ -30,10 +30,12 @@ var invariant = require('invariant');
30
class CallbackQueue<T> {
31
_callbacks: ?Array<() => void>;
32
_contexts: ?Array<T>;
33
+ _arg: ?mixed;
34
34
- constructor() {
35
+ constructor(arg) {
36
this._callbacks = null;
37
this._contexts = null;
38
+ this._arg = arg;
39
}
40
41
/**
@@ -59,6 +61,7 @@ class CallbackQueue<T> {
61
notifyAll() {
62
var callbacks = this._callbacks;
63
var contexts = this._contexts;
64
+ var arg = this._arg;
65
if (callbacks && contexts) {
66
invariant(
67
callbacks.length === contexts.length,
@@ -67,7 +70,7 @@ class CallbackQueue<T> {
70
this._callbacks = null;
71
this._contexts = null;
72
for (var i = 0; i < callbacks.length; i++) {
70
- callbacks[i].call(contexts[i]);
73
+ callbacks[i].call(contexts[i], arg);
74
}
75
callbacks.length = 0;
76
contexts.length = 0;
src/renderers/testing/ReactTestMount.js
+22
-8
@@ -20,6 +20,16 @@ var getHostComponentFromComposite = require('getHostComponentFromComposite');
20
var instantiateReactComponent = require('instantiateReactComponent');
21
var invariant = require('invariant');
22
23
+type TestRendererOptions = {
24
+ createNodeMock: (element: ReactElement) => Object,
25
+};
26
+
27
+var defaultTestOptions = {
28
+ createNodeMock: function() {
29
+ return null;
30
+ },
31
+};
32
+
33
/**
34
* Temporary (?) hack so that we can store all top-level pending updates on
35
* composites instead of having to worry about different types of components
@@ -45,7 +55,8 @@ TopLevelWrapper.isReactTopLevelWrapper = true;
55
*/
56
function mountComponentIntoNode(
57
componentInstance,
48
- transaction) {
58
+ transaction,
59
+ ) {
60
var image = ReactReconciler.mountComponent(
61
componentInstance,
62
transaction,
@@ -65,13 +76,15 @@ function mountComponentIntoNode(
76
* @param {number} containerTag container element to mount into.
77
*/
78
function batchedMountComponentIntoNode(
68
- componentInstance) {
69
- var transaction = ReactUpdates.ReactReconcileTransaction.getPooled();
79
+ componentInstance,
80
+ options,
81
+ ) {
82
+ var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(options);
83
var image = transaction.perform(
84
mountComponentIntoNode,
85
null,
86
componentInstance,
74
- transaction
87
+ transaction,
88
);
89
ReactUpdates.ReactReconcileTransaction.release(transaction);
90
return image;
@@ -135,11 +148,12 @@ ReactTestInstance.prototype.toJSON = function() {
148
var ReactTestMount = {
149
150
render: function(
138
- nextElement: ReactElement<any>
151
+ nextElement: ReactElement<any>,
152
+ options?: TestRendererOptions,
153
): ReactTestInstance {
154
var nextWrappedElement = React.createElement(
155
TopLevelWrapper,
142
- { child: nextElement }
156
+ {child: nextElement},
157
);
158
159
var instance = instantiateReactComponent(nextWrappedElement, false);
@@ -147,10 +161,10 @@ var ReactTestMount = {
161
// The initial render is synchronous but any updates that happen during
162
// rendering, in componentWillMount or componentDidMount, will be batched
163
// according to the current batching strategy.
150
-
164
ReactUpdates.batchedUpdates(
165
batchedMountComponentIntoNode,
153
- instance
166
+ instance,
167
+ Object.assign({}, defaultTestOptions, options),
168
);
169
return new ReactTestInstance(instance);
170
},
src/renderers/testing/ReactTestReconcileTransaction.js
+10
-2
@@ -57,9 +57,10 @@ var TRANSACTION_WRAPPERS = [ON_DOM_READY_QUEUEING];
57
*
58
* @class ReactTestReconcileTransaction
59
*/
60
-function ReactTestReconcileTransaction() {
60
+function ReactTestReconcileTransaction(testOptions) {
61
this.reinitializeTransaction();
62
- this.reactMountReady = CallbackQueue.getPooled(null);
62
+ this.testOptions = testOptions;
63
+ this.reactMountReady = CallbackQueue.getPooled(this);
64
}
65
66
var Mixin = {
@@ -82,6 +83,13 @@ var Mixin = {
83
return this.reactMountReady;
84
},
85
86
+ /**
87
+ * @return {object} the options passed to ReactTestRenderer
88
+ */
89
+ getTestOptions: function() {
90
+ return this.testOptions;
91
+ },
92
+
93
/**
94
* @return {object} The queue to collect React async events.
95
*/
src/renderers/testing/ReactTestRenderer.js
+10
-5
@@ -43,6 +43,7 @@ var ReactTestComponent = function(element) {
43
this._renderedChildren = null;
44
this._topLevelWrapper = null;
45
};
46
+
47
ReactTestComponent.prototype.mountComponent = function(
48
transaction,
49
nativeParent,
@@ -52,6 +53,7 @@ ReactTestComponent.prototype.mountComponent = function(
53
var element = this._currentElement;
54
this.mountChildren(element.props.children, transaction, context);
55
};
56
+
57
ReactTestComponent.prototype.receiveComponent = function(
58
nextElement,
59
transaction,
@@ -60,13 +62,17 @@ ReactTestComponent.prototype.receiveComponent = function(
62
this._currentElement = nextElement;
63
this.updateChildren(nextElement.props.children, transaction, context);
64
};
65
+
66
ReactTestComponent.prototype.getHostNode = function() {};
64
-ReactTestComponent.prototype.getPublicInstance = function() {
65
- // I can't say this makes a ton of sense but it seems better than throwing.
66
- // Maybe we'll revise later if someone has a good use case.
67
- return null;
67
+
68
+ReactTestComponent.prototype.getPublicInstance = function(transaction) {
69
+ var element = this._currentElement;
70
+ var options = transaction.getTestOptions();
71
+ return options.createNodeMock(element);
72
};
73
+
74
ReactTestComponent.prototype.unmountComponent = function() {};
75
+
76
ReactTestComponent.prototype.toJSON = function() {
77
var {children, ...props} = this._currentElement.props;
78
var childrenJSON = [];
@@ -136,7 +142,6 @@ ReactComponentEnvironment.injection.injectEnvironment({
142
143
var ReactTestRenderer = {
144
create: ReactTestMount.render,
139
-
145
/* eslint-disable camelcase */
146
unstable_batchedUpdates: ReactUpdates.batchedUpdates,
147
/* eslint-enable camelcase */
src/renderers/testing/__tests__/ReactTestRenderer-test.js
+75
@@ -231,6 +231,81 @@ describe('ReactTestRenderer', () => {
231
);
232
});
233
234
+ it('allows an optional createNodeMock function', () => {
235
+ var mockDivInstance = { appendChild: () => {} };
236
+ var mockInputInstance = { focus: () => {} };
237
+ var mockListItemInstance = { click: () => {} };
238
+ var mockAnchorInstance = { hover: () => {} };
239
+ var log = [];
240
+ class Foo extends React.Component {
241
+ componentDidMount() {
242
+ log.push(this.refs.bar);
243
+ }
244
+ render() {
245
+ return (
246
+ <a ref="bar">Hello, world</a>
247
+ );
248
+ }
249
+ }
250
+ function createNodeMock(element) {
251
+ switch (element.type) {
252
+ case 'div':
253
+ return mockDivInstance;
254
+ case 'input':
255
+ return mockInputInstance;
256
+ case 'li':
257
+ return mockListItemInstance;
258
+ case 'a':
259
+ return mockAnchorInstance;
260
+ default:
261
+ return {};
262
+ }
263
+ }
264
+ ReactTestRenderer.create(
265
+ <div ref={(r) => log.push(r)} />,
266
+ {createNodeMock}
267
+ );
268
+ ReactTestRenderer.create(
269
+ <input ref={(r) => log.push(r)} />,
270
+ {createNodeMock},
271
+ );
272
+ ReactTestRenderer.create(
273
+ <div>
274
+ <span>
275
+ <ul>
276
+ <li ref={(r) => log.push(r)} />
277
+ </ul>
278
+ <ul>
279
+ <li ref={(r) => log.push(r)} />
280
+ <li ref={(r) => log.push(r)} />
281
+ </ul>
282
+ </span>
283
+ </div>,
284
+ {createNodeMock, foobar: true},
285
+ );
286
+ ReactTestRenderer.create(
287
+ <Foo />,
288
+ {createNodeMock},
289
+ );
290
+ ReactTestRenderer.create(
291
+ <div ref={(r) => log.push(r)} />,
292
+ );
293
+ ReactTestRenderer.create(
294
+ <div ref={(r) => log.push(r)} />,
295
+ {}
296
+ );
297
+ expect(log).toEqual([
298
+ mockDivInstance,
299
+ mockInputInstance,
300
+ mockListItemInstance,
301
+ mockListItemInstance,
302
+ mockListItemInstance,
303
+ mockAnchorInstance,
304
+ null,
305
+ null,
306
+ ]);
307
+ });
308
+
309
it('supports error boundaries', () => {
310
var log = [];
311
class Angry extends React.Component {