@samitouri / QOS-React-2 / commits / f3569a2c31

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 {