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__) {