@samitouri / QOS-React / commits / e9f8d57c70

Add an initial implementation of ReactDebugTool events

Dan Abramov committed Apr 19, 2016 at 21:01 UTC e9f8d57c7045f01a6a0083f7d6c9663fc63c90e2
6 files changed +688
src/isomorphic/ReactDebugTool.js
+15
@@ -58,6 +58,21 @@ var ReactDebugTool = {
58 onSetState() {
59 emitEvent('onSetState');
60 },
61 + onSetIsComposite(debugID, isComposite) {
62 + emitEvent('onSetIsComposite', debugID, isComposite);
63 + },
64 + onSetDisplayName(debugID, displayName) {
65 + emitEvent('onSetDisplayName', debugID, displayName);
66 + },
67 + onSetChildren(debugID, childDebugIDs) {
68 + emitEvent('onSetChildren', debugID, childDebugIDs);
69 + },
70 + onSetOwner(debugID, ownerDebugID) {
71 + emitEvent('onSetOwner', debugID, ownerDebugID);
72 + },
73 + onSetText(debugID, text) {
74 + emitEvent('onSetText', debugID, text);
75 + },
76 onMountRootComponent(internalInstance) {
77 emitEvent('onMountRootComponent', internalInstance);
78 },
src/isomorphic/__tests__/ReactDebugTool-test.js new
+607
@@ -0,0 +1,607 @@
1 +/**
2 + * Copyright 2016-present, Facebook, Inc.
3 + * All rights reserved.
4 + *
5 + * This source code is licensed under the BSD-style license found in the
6 + * LICENSE file in the root directory of this source tree. An additional grant
7 + * of patent rights can be found in the PATENTS file in the same directory.
8 + *
9 + * @emails react-core
10 + */
11 +
12 +'use strict';
13 +
14 +describe('ReactDebugTool', () => {
15 +
16 +
17 +
18 +
19 +
20 +
21 +
22 +
23 +
24 +
25 +
26 +
27 + /*
28 + TODO:
29 + * Make sure update and unmount paths are tested
30 + * Make sure server rendering works the same way
31 + * Enable all existing tests
32 + * Remove ReactInstanceMap usage
33 + */
34 +
35 +
36 +
37 +
38 +
39 +
40 +
41 +
42 +
43 +
44 +
45 + var React;
46 + var ReactDebugTool;
47 + var ReactDOM;
48 + var ReactInstanceMap;
49 +
50 + var devtool;
51 +
52 + function createDevtool() {
53 + var tree = {};
54 +
55 + function updateTree(debugID, update) {
56 + if (!tree[debugID]) {
57 + tree[debugID] = {};
58 + }
59 + update(tree[debugID]);
60 + }
61 +
62 + var devtool = {
63 + onSetIsComposite(debugID, isComposite) {
64 + updateTree(debugID, item => item.isComposite = isComposite);
65 + },
66 + onSetDisplayName(debugID, displayName) {
67 + updateTree(debugID, item => item.displayName = displayName);
68 + },
69 + onSetChildren(debugID, childDebugIDs) {
70 + updateTree(debugID, item => item.childDebugIDs = childDebugIDs);
71 + },
72 + onSetOwner(debugID, ownerDebugID) {
73 + updateTree(debugID, item => item.ownerDebugID = ownerDebugID);
74 + },
75 + onSetText(debugID, text) {
76 + updateTree(debugID, item => item.text = text);
77 + },
78 + getTree(debugID, includeOwner) {
79 + var item = tree[debugID];
80 + var result = {
81 + isComposite: item.isComposite,
82 + displayName: item.displayName,
83 + };
84 + result.children = item.childDebugIDs.map(childDebugID =>
85 + devtool.getTree(childDebugID, includeOwner)
86 + );
87 + if (item.text != null) {
88 + result.text = item.text;
89 + }
90 + if (includeOwner && item.ownerDebugID) {
91 + result.ownerDisplayName = tree[item.ownerDebugID].displayName;
92 + }
93 + return result;
94 + }
95 + };
96 +
97 + return devtool;
98 + }
99 +
100 + beforeEach(() => {
101 + jest.resetModuleRegistry();
102 +
103 + React = require('React');
104 + ReactDebugTool = require('ReactDebugTool');
105 + ReactDOM = require('ReactDOM');
106 + ReactInstanceMap = require('ReactInstanceMap');
107 +
108 + devtool = createDevtool();
109 + ReactDebugTool.addDevtool(devtool);
110 + });
111 +
112 + afterEach(() => {
113 + ReactDebugTool.removeDevtool(devtool);
114 + });
115 +
116 + function assertTreeMatches(element, expectedTree, includeOwner) {
117 + class Wrapper extends React.Component {
118 + render() {
119 + return element;
120 + }
121 + }
122 +
123 + var node = document.createElement('div');
124 + var rootPublicInstance = ReactDOM.render(<Wrapper />, node);
125 + var rootInstance = ReactInstanceMap.get(rootPublicInstance);
126 + var actualTree = devtool.getTree(
127 + rootInstance._renderedComponent._debugID,
128 + includeOwner
129 + );
130 + expect(actualTree).toEqual(expectedTree);
131 + }
132 +
133 + it('uses displayName or Unknown for classic components', () => {
134 + var Foo = React.createClass({
135 + render() {
136 + return null;
137 + },
138 + });
139 + Foo.displayName = 'Bar';
140 + var Baz = React.createClass({
141 + render() {
142 + return null;
143 + },
144 + });
145 + var Qux = React.createClass({
146 + render() {
147 + return null;
148 + },
149 + });
150 + delete Qux.displayName;
151 +
152 + var element = <div><Foo /><Baz /><Qux /></div>;
153 + var expectedTree = {
154 + isComposite: false,
155 + displayName: 'div',
156 + children: [{
157 + isComposite: true,
158 + displayName: 'Bar',
159 + children: [],
160 + }, {
161 + isComposite: true,
162 + displayName: 'Baz',
163 + children: [],
164 + }, {
165 + isComposite: true,
166 + displayName: 'Unknown',
167 + children: [],
168 + }],
169 + };
170 + assertTreeMatches(element, expectedTree);
171 + });
172 +
173 + it('uses displayName, name, or ReactComponent for modern components', () => {
174 + class Foo extends React.Component {
175 + render() {
176 + return null;
177 + }
178 + }
179 + Foo.displayName = 'Bar';
180 + class Baz extends React.Component {
181 + render() {
182 + return null;
183 + }
184 + }
185 + class Qux extends React.Component {
186 + render() {
187 + return null;
188 + }
189 + }
190 + delete Qux.name;
191 +
192 + var element = <div><Foo /><Baz /><Qux /></div>;
193 + var expectedTree = {
194 + isComposite: false,
195 + displayName: 'div',
196 + children: [{
197 + isComposite: true,
198 + displayName: 'Bar',
199 + children: [],
200 + }, {
201 + isComposite: true,
202 + displayName: 'Baz',
203 + children: [],
204 + }, {
205 + isComposite: true,
206 + // Note: Ideally fallback name should be consistent (e.g. "Unknown")
207 + displayName: 'ReactComponent',
208 + children: [],
209 + }],
210 + };
211 + assertTreeMatches(element, expectedTree);
212 + });
213 +
214 + it('uses displayName, name, or Object for factory components', () => {
215 + function Foo() {
216 + return {
217 + render() {
218 + return null;
219 + },
220 + };
221 + }
222 + Foo.displayName = 'Bar';
223 + function Baz() {
224 + return {
225 + render() {
226 + return null;
227 + },
228 + };
229 + }
230 + function Qux() {
231 + return {
232 + render() {
233 + return null;
234 + },
235 + };
236 + }
237 + delete Qux.name;
238 +
239 + var element = <div><Foo /><Baz /><Qux /></div>;
240 + var expectedTree = {
241 + isComposite: false,
242 + displayName: 'div',
243 + children: [{
244 + isComposite: true,
245 + displayName: 'Bar',
246 + children: [],
247 + }, {
248 + isComposite: true,
249 + displayName: 'Baz',
250 + children: [],
251 + }, {
252 + isComposite: true,
253 + displayName: 'Unknown',
254 + children: [],
255 + }],
256 + };
257 + assertTreeMatches(element, expectedTree);
258 + });
259 +
260 + it('uses displayName, name, or StatelessComponent for functional components', () => {
261 + function Foo() {
262 + return null;
263 + }
264 + Foo.displayName = 'Bar';
265 + function Baz() {
266 + return null;
267 + }
268 + function Qux() {
269 + return null;
270 + }
271 + delete Qux.name;
272 +
273 + var element = <div><Foo /><Baz /><Qux /></div>;
274 + var expectedTree = {
275 + isComposite: false,
276 + displayName: 'div',
277 + children: [{
278 + isComposite: true,
279 + displayName: 'Bar',
280 + children: [],
281 + }, {
282 + isComposite: true,
283 + displayName: 'Baz',
284 + children: [],
285 + }, {
286 + isComposite: true,
287 + displayName: 'Unknown',
288 + children: [],
289 + }],
290 + };
291 + assertTreeMatches(element, expectedTree);
292 + });
293 +
294 + it('reports a native tree correctly', () => {
295 + var element = (
296 + <div>
297 + <p>
298 + <span>
299 + Hi!
300 + </span>
301 + Wow.
302 + </p>
303 + <hr />
304 + </div>
305 + );
306 + var expectedTree = {
307 + isComposite: false,
308 + displayName: 'div',
309 + children: [{
310 + isComposite: false,
311 + displayName: 'p',
312 + children: [{
313 + isComposite: false,
314 + displayName: 'span',
315 + children: [{
316 + isComposite: false,
317 + displayName: '#text',
318 + text: 'Hi!',
319 + children: [],
320 + }],
321 + }, {
322 + isComposite: false,
323 + displayName: '#text',
324 + text: 'Wow.',
325 + children: [],
326 + }],
327 + }, {
328 + isComposite: false,
329 + displayName: 'hr',
330 + children: [],
331 + }],
332 + };
333 + assertTreeMatches(element, expectedTree);
334 + });
335 +
336 + it('reports a simple tree with composites correctly', () => {
337 + class Foo extends React.Component {
338 + render() {
339 + return <div />;
340 + }
341 + }
342 +
343 + var element = <Foo />;
344 + var expectedTree = {
345 + isComposite: true,
346 + displayName: 'Foo',
347 + children: [{
348 + isComposite: false,
349 + displayName: 'div',
350 + children: [],
351 + }],
352 + };
353 + assertTreeMatches(element, expectedTree);
354 + });
355 +
356 + it('reports a tree with composites correctly', () => {
357 + var Qux = React.createClass({
358 + render() {
359 + return null;
360 + },
361 + });
362 + function Foo() {
363 + return {
364 + render() {
365 + return <Qux />;
366 + },
367 + };
368 + }
369 + function Bar({children}) {
370 + return <h1>{children}</h1>;
371 + }
372 + class Baz extends React.Component {
373 + render() {
374 + return (
375 + <div>
376 + <Foo />
377 + <Bar>
378 + <span>Hi,</span>
379 + Mom
380 + </Bar>
381 + <a href="#">Click me.</a>
382 + </div>
383 + );
384 + }
385 + }
386 +
387 + var element = <Baz />;
388 + var expectedTree = {
389 + isComposite: true,
390 + displayName: 'Baz',
391 + children: [{
392 + isComposite: false,
393 + displayName: 'div',
394 + children: [{
395 + isComposite: true,
396 + displayName: 'Foo',
397 + children: [{
398 + isComposite: true,
399 + displayName: 'Qux',
400 + children: [],
401 + }],
402 + }, {
403 + isComposite: true,
404 + displayName: 'Bar',
405 + children: [{
406 + isComposite: false,
407 + displayName: 'h1',
408 + children: [{
409 + isComposite: false,
410 + displayName: 'span',
411 + children: [{
412 + isComposite: false,
413 + displayName: '#text',
414 + text: 'Hi,',
415 + children: [],
416 + }],
417 + }, {
418 + isComposite: false,
419 + displayName: '#text',
420 + text: 'Mom',
421 + children: [],
422 + }],
423 + }],
424 + }, {
425 + isComposite: false,
426 + displayName: 'a',
427 + children: [{
428 + isComposite: false,
429 + displayName: '#text',
430 + text: 'Click me.',
431 + children: [],
432 + }],
433 + }],
434 + }],
435 + };
436 + assertTreeMatches(element, expectedTree);
437 + });
438 +
439 + it('ignores null children', () => {
440 + class Foo extends React.Component {
441 + render() {
442 + return null;
443 + }
444 + }
445 + var element = <Foo />;
446 + var expectedTree = {
447 + isComposite: true,
448 + displayName: 'Foo',
449 + children: [],
450 + };
451 + assertTreeMatches(element, expectedTree);
452 + });
453 +
454 + it('ignores false children', () => {
455 + class Foo extends React.Component {
456 + render() {
457 + return false;
458 + }
459 + }
460 + var element = <Foo />;
461 + var expectedTree = {
462 + isComposite: true,
463 + displayName: 'Foo',
464 + children: [],
465 + };
466 + assertTreeMatches(element, expectedTree);
467 + });
468 +
469 + it('reports text nodes as children', () => {
470 + var element = <div>{'1'}{2}</div>;
471 + var expectedTree = {
472 + isComposite: false,
473 + displayName: 'div',
474 + children: [{
475 + isComposite: false,
476 + displayName: '#text',
477 + text: '1',
478 + children: [],
479 + }, {
480 + isComposite: false,
481 + displayName: '#text',
482 + text: 2,
483 + children: [],
484 + }],
485 + };
486 + assertTreeMatches(element, expectedTree);
487 + });
488 +
489 + it('reports a single text node as a child', () => {
490 + var element = <div>{'1'}</div>;
491 + var expectedTree = {
492 + isComposite: false,
493 + displayName: 'div',
494 + children: [{
495 + isComposite: false,
496 + displayName: '#text',
497 + text: '1',
498 + children: [],
499 + }],
500 + };
501 + assertTreeMatches(element, expectedTree);
502 + });
503 +
504 + it('reports a single number node as a child', () => {
505 + var element = <div>{42}</div>;
506 + var expectedTree = {
507 + isComposite: false,
508 + displayName: 'div',
509 + children: [{
510 + isComposite: false,
511 + displayName: '#text',
512 + text: 42,
513 + children: [],
514 + }],
515 + };
516 + assertTreeMatches(element, expectedTree);
517 + });
518 +
519 + it('skips empty nodes for multiple children', () => {
520 + function Foo() {
521 + return <div />;
522 + }
523 + var element = (
524 + <div>
525 + {'hi'}
526 + {false}
527 + {42}
528 + {null}
529 + <Foo />
530 + </div>
531 + );
532 + var expectedTree = {
533 + isComposite: false,
534 + displayName: 'div',
535 + children: [{
536 + isComposite: false,
537 + displayName: '#text',
538 + text: 'hi',
539 + children: [],
540 + }, {
541 + isComposite: false,
542 + displayName: '#text',
543 + text: 42,
544 + children: [],
545 + }, {
546 + isComposite: true,
547 + displayName: 'Foo',
548 + children: [{
549 + isComposite: false,
550 + displayName: 'div',
551 + children: [],
552 + }],
553 + }],
554 + };
555 + assertTreeMatches(element, expectedTree);
556 + });
557 +
558 + it('tracks owner correctly', () => {
559 + class Foo extends React.Component {
560 + render() {
561 + return <Bar><h1>Hi.</h1></Bar>;
562 + }
563 + }
564 + function Bar({children}) {
565 + return <div>{children} Mom</div>;
566 + }
567 +
568 + // Note that owner is not calculated for text nodes
569 + // because they are not created from real elements.
570 + var element = <article><Foo /></article>;
571 + var expectedTree = {
572 + isComposite: false,
573 + displayName: 'article',
574 + children: [{
575 + isComposite: true,
576 + displayName: 'Foo',
577 + children: [{
578 + isComposite: true,
579 + displayName: 'Bar',
580 + ownerDisplayName: 'Foo',
581 + children: [{
582 + isComposite: false,
583 + displayName: 'div',
584 + ownerDisplayName: 'Bar',
585 + children: [{
586 + isComposite: false,
587 + displayName: 'h1',
588 + ownerDisplayName: 'Foo',
589 + children: [{
590 + isComposite: false,
591 + displayName: '#text',
592 + text: 'Hi.',
593 + children: [],
594 + }],
595 + }, {
596 + isComposite: false,
597 + displayName: '#text',
598 + text: ' Mom',
599 + children: [],
600 + }],
601 + }],
602 + }],
603 + }],
604 + };
605 + assertTreeMatches(element, expectedTree, true);
606 + });
607 +});
src/renderers/dom/shared/ReactDOMComponent.js
+9
@@ -32,6 +32,7 @@ var ReactDOMInput = require('ReactDOMInput');
32 var ReactDOMOption = require('ReactDOMOption');
33 var ReactDOMSelect = require('ReactDOMSelect');
34 var ReactDOMTextarea = require('ReactDOMTextarea');
35 +var ReactInstrumentation = require('ReactInstrumentation');
36 var ReactMultiChild = require('ReactMultiChild');
37 var ReactPerf = require('ReactPerf');
38
@@ -756,6 +757,14 @@ ReactDOMComponent.Mixin = {
757 var childrenToUse = contentToUse != null ? null : props.children;
758 if (contentToUse != null) {
759 // TODO: Validate that text is allowed as a child of this node
760 + if (__DEV__) {
761 + var inlinedTextDebugID = this._debugID + '#text';
762 + ReactInstrumentation.debugTool.onSetChildren(this._debugID, [inlinedTextDebugID]);
763 + ReactInstrumentation.debugTool.onSetIsComposite(inlinedTextDebugID, false);
764 + ReactInstrumentation.debugTool.onSetDisplayName(inlinedTextDebugID, '#text');
765 + ReactInstrumentation.debugTool.onSetChildren(inlinedTextDebugID, []);
766 + ReactInstrumentation.debugTool.onSetText(inlinedTextDebugID, contentToUse);
767 + }
768 DOMLazyTree.queueText(lazyTree, contentToUse);
769 } else if (childrenToUse != null) {
770 var mountImages = this.mountChildren(
src/renderers/shared/reconciler/ReactCompositeComponent.js
+8
@@ -377,6 +377,14 @@ var ReactCompositeComponentMixin = {
377 this._renderedComponent = this._instantiateReactComponent(
378 renderedElement
379 );
380 + if (__DEV__) {
381 + if (this._renderedComponent._debugID) {
382 + ReactInstrumentation.debugTool.onSetChildren(
383 + this._debugID,
384 + [this._renderedComponent._debugID]
385 + );
386 + }
387 + }
388
389 var markup = ReactReconciler.mountComponent(
390 this._renderedComponent,
src/renderers/shared/reconciler/ReactMultiChild.js
+9
@@ -12,6 +12,7 @@
12 'use strict';
13
14 var ReactComponentEnvironment = require('ReactComponentEnvironment');
15 +var ReactInstrumentation = require('ReactInstrumentation');
16 var ReactMultiChildUpdateTypes = require('ReactMultiChildUpdateTypes');
17
18 var ReactCurrentOwner = require('ReactCurrentOwner');
@@ -214,6 +215,14 @@ var ReactMultiChild = {
215 nestedChildren, transaction, context
216 );
217 this._renderedChildren = children;
218 +
219 + if (__DEV__) {
220 + ReactInstrumentation.debugTool.onSetChildren(
221 + this._debugID,
222 + Object.keys(children).map(key => children[key]._debugID)
223 + );
224 + }
225 +
226 var mountImages = [];
227 var index = 0;
228 for (var name in children) {
src/renderers/shared/reconciler/instantiateReactComponent.js
+40
@@ -14,6 +14,7 @@
14 var ReactCompositeComponent = require('ReactCompositeComponent');
15 var ReactEmptyComponent = require('ReactEmptyComponent');
16 var ReactNativeComponent = require('ReactNativeComponent');
17 +var ReactInstrumentation = require('ReactInstrumentation');
18
19 var invariant = require('invariant');
20 var warning = require('warning');
@@ -40,6 +41,21 @@ function getDeclarationErrorAddendum(owner) {
41 return '';
42 }
43
44 +function getDisplayName(instance) {
45 + var element = instance._currentElement;
46 + if (element == null) {
47 + return '#empty';
48 + } else if (typeof element === 'string' || typeof element === 'number') {
49 + return '#text';
50 + } else if (typeof element.type === 'string') {
51 + return element.type;
52 + } else if (instance.getName) {
53 + return instance.getName() || 'Unknown';
54 + } else {
55 + return element.type.displayName || element.type.name || 'Unknown';
56 + }
57 +}
58 +
59 /**
60 * Check if the type reference is a known internal type. I.e. not a user
61 * provided composite type.
@@ -56,6 +72,8 @@ function isInternalComponentType(type) {
72 );
73 }
74
75 +var nextDebugID = 1;
76 +
77 /**
78 * Given a ReactNode, create an instance that will actually be mounted.
79 *
@@ -66,7 +84,12 @@ function isInternalComponentType(type) {
84 function instantiateReactComponent(node) {
85 var instance;
86
87 + var isEmpty = false;
88 + var isText = false;
89 + var isComposite = false;
90 +
91 if (node === null || node === false) {
92 + isEmpty = true;
93 instance = ReactEmptyComponent.create(instantiateReactComponent);
94 } else if (typeof node === 'object') {
95 var element = node;
@@ -88,9 +111,11 @@ function instantiateReactComponent(node) {
111 // representation, we can drop this code path.
112 instance = new element.type(element);
113 } else {
114 + isComposite = true;
115 instance = new ReactCompositeComponentWrapper(element);
116 }
117 } else if (typeof node === 'string' || typeof node === 'number') {
118 + isText = true;
119 instance = ReactNativeComponent.createInstanceForText(node);
120 } else {
121 invariant(
@@ -121,6 +146,21 @@ function instantiateReactComponent(node) {
146 instance._warnedAboutRefsInRender = false;
147 }
148
149 + if (__DEV__) {
150 + instance._debugID = isEmpty ? 0 : nextDebugID++;
151 + var displayName = getDisplayName(instance);
152 + ReactInstrumentation.debugTool.onSetIsComposite(instance._debugID, isComposite);
153 + ReactInstrumentation.debugTool.onSetDisplayName(instance._debugID, displayName);
154 + ReactInstrumentation.debugTool.onSetChildren(instance._debugID, []);
155 + var owner = node && node._owner;
156 + if (owner) {
157 + ReactInstrumentation.debugTool.onSetOwner(instance._debugID, owner._debugID);
158 + }
159 + if (isText) {
160 + ReactInstrumentation.debugTool.onSetText(instance._debugID, node);
161 + }
162 + }
163 +
164 // Internal instances should fully constructed at this point, so they should
165 // not get any new fields added to them at this point.
166 if (__DEV__) {