Add ReactNativeOperationHistoryDevtool to track native operations
Dan Abramov committed
Apr 26, 2016 at 01:28 UTC
aaadb31827621470fb9be92b67ffdd75ade7775b
11 files changed
+833
-12
src/isomorphic/ReactDebugTool.js
+3
@@ -55,6 +55,9 @@ var ReactDebugTool = {
55
onEndProcessingChildContext() {
56
emitEvent('onEndProcessingChildContext');
57
},
58
+ onNativeOperation(debugID, type, payload) {
59
+ emitEvent('onNativeOperation', debugID, type, payload);
60
+ },
61
onSetState() {
62
emitEvent('onSetState');
63
},
src/isomorphic/devtools/ReactNativeOperationHistoryDevtool.js
new
+34
@@ -0,0 +1,34 @@
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
+ * @providesModule ReactNativeOperationHistoryDevtool
10
+ */
11
+
12
+'use strict';
13
+
14
+var history = [];
15
+
16
+var ReactNativeOperationHistoryDevtool = {
17
+ onNativeOperation(debugID, type, payload) {
18
+ history.push({
19
+ instanceID: debugID,
20
+ type,
21
+ payload,
22
+ });
23
+ },
24
+
25
+ clearHistory() {
26
+ history = [];
27
+ },
28
+
29
+ getHistory() {
30
+ return history;
31
+ },
32
+};
33
+
34
+module.exports = ReactNativeOperationHistoryDevtool;
src/isomorphic/devtools/__tests__/ReactNativeOperationHistoryDevtool-test.js
new
+654
@@ -0,0 +1,654 @@
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('ReactNativeOperationHistoryDevtool', () => {
15
+ var React;
16
+ var ReactDebugTool;
17
+ var ReactDOM;
18
+ var ReactDOMComponentTree;
19
+ var ReactDOMFeatureFlags;
20
+ var ReactNativeOperationHistoryDevtool;
21
+
22
+ beforeEach(() => {
23
+ jest.resetModuleRegistry();
24
+
25
+ React = require('React');
26
+ ReactDebugTool = require('ReactDebugTool');
27
+ ReactDOM = require('ReactDOM');
28
+ ReactDOMComponentTree = require('ReactDOMComponentTree');
29
+ ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
30
+ ReactNativeOperationHistoryDevtool = require('ReactNativeOperationHistoryDevtool');
31
+
32
+ ReactDebugTool.addDevtool(ReactNativeOperationHistoryDevtool);
33
+ });
34
+
35
+ afterEach(() => {
36
+ ReactDebugTool.removeDevtool(ReactNativeOperationHistoryDevtool);
37
+ });
38
+
39
+ function assertHistoryMatches(expectedHistory) {
40
+ var actualHistory = ReactNativeOperationHistoryDevtool.getHistory();
41
+ expect(actualHistory).toEqual(expectedHistory);
42
+ }
43
+
44
+ describe('mount', () => {
45
+ it('gets recorded for native roots', () => {
46
+ var node = document.createElement('div');
47
+ ReactDOM.render(<div><p>Hi.</p></div>, node);
48
+ var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
49
+
50
+ assertHistoryMatches([{
51
+ instanceID: inst._debugID,
52
+ type: 'mount',
53
+ payload: ReactDOMFeatureFlags.useCreateElement ?
54
+ 'DIV' :
55
+ '<div data-reactroot="" data-reactid="1"><p data-reactid="2">Hi.</p></div>',
56
+ }]);
57
+ });
58
+
59
+ it('gets recorded for composite roots', () => {
60
+ function Foo() {
61
+ return <div><p>Hi.</p></div>;
62
+ }
63
+ var node = document.createElement('div');
64
+ ReactDOM.render(<Foo />, node);
65
+ var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
66
+ assertHistoryMatches([{
67
+ instanceID: inst._debugID,
68
+ type: 'mount',
69
+ payload: ReactDOMFeatureFlags.useCreateElement ?
70
+ 'DIV' :
71
+ '<div data-reactroot="" data-reactid="1">' +
72
+ '<p data-reactid="2">Hi.</p></div>',
73
+ }]);
74
+ });
75
+
76
+ it('gets recorded for composite roots that return null', () => {
77
+ function Foo() {
78
+ return null;
79
+ }
80
+ var node = document.createElement('div');
81
+ ReactDOM.render(<Foo />, node);
82
+ var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
83
+ assertHistoryMatches([{
84
+ instanceID: inst._debugID,
85
+ type: 'mount',
86
+ payload: ReactDOMFeatureFlags.useCreateElement ?
87
+ '#comment' :
88
+ '<!-- react-empty: 1 -->',
89
+ }]);
90
+ });
91
+ });
92
+
93
+ describe('update styles', () => {
94
+ it('gets recorded during mount', () => {
95
+ var node = document.createElement('div');
96
+ ReactDOM.render(<div style={{
97
+ color: 'red',
98
+ backgroundColor: 'yellow',
99
+ }} />, node);
100
+ var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
101
+
102
+ if (ReactDOMFeatureFlags.useCreateElement) {
103
+ assertHistoryMatches([{
104
+ instanceID: inst._debugID,
105
+ type: 'update styles',
106
+ payload: {
107
+ color: 'red',
108
+ backgroundColor: 'yellow',
109
+ },
110
+ }, {
111
+ instanceID: inst._debugID,
112
+ type: 'mount',
113
+ payload: 'DIV',
114
+ }]);
115
+ } else {
116
+ assertHistoryMatches([{
117
+ instanceID: inst._debugID,
118
+ type: 'mount',
119
+ payload: '<div style="color:red;background-color:yellow;" ' +
120
+ 'data-reactroot="" data-reactid="1"></div>',
121
+ }]);
122
+ }
123
+ });
124
+
125
+ it('gets recorded during an update', () => {
126
+ var node = document.createElement('div');
127
+ ReactDOM.render(<div />, node);
128
+ var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
129
+
130
+ ReactNativeOperationHistoryDevtool.clearHistory();
131
+ ReactDOM.render(<div style={{ color: 'red' }} />, node);
132
+ ReactDOM.render(<div style={{
133
+ color: 'blue',
134
+ backgroundColor: 'yellow',
135
+ }} />, node);
136
+ ReactDOM.render(<div style={{ backgroundColor: 'green' }} />, node);
137
+ ReactDOM.render(<div />, node);
138
+
139
+ assertHistoryMatches([{
140
+ instanceID: inst._debugID,
141
+ type: 'update styles',
142
+ payload: { color: 'red' },
143
+ }, {
144
+ instanceID: inst._debugID,
145
+ type: 'update styles',
146
+ payload: { color: 'blue', backgroundColor: 'yellow' },
147
+ }, {
148
+ instanceID: inst._debugID,
149
+ type: 'update styles',
150
+ payload: { color: '', backgroundColor: 'green' },
151
+ }, {
152
+ instanceID: inst._debugID,
153
+ type: 'update styles',
154
+ payload: { backgroundColor: '' },
155
+ }]);
156
+ });
157
+
158
+ it('gets ignored if the styles are shallowly equal', () => {
159
+ var node = document.createElement('div');
160
+ ReactDOM.render(<div />, node);
161
+ var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
162
+
163
+ ReactNativeOperationHistoryDevtool.clearHistory();
164
+ ReactDOM.render(<div style={{
165
+ color: 'red',
166
+ backgroundColor: 'yellow',
167
+ }} />, node);
168
+ ReactDOM.render(<div style={{
169
+ color: 'red',
170
+ backgroundColor: 'yellow',
171
+ }} />, node);
172
+
173
+ assertHistoryMatches([{
174
+ instanceID: inst._debugID,
175
+ type: 'update styles',
176
+ payload: {
177
+ color: 'red',
178
+ backgroundColor: 'yellow',
179
+ },
180
+ }]);
181
+ });
182
+ });
183
+
184
+ describe('update attribute', () => {
185
+ describe('simple attribute', () => {
186
+ it('gets recorded during mount', () => {
187
+ var node = document.createElement('div');
188
+ ReactDOM.render(<div className="rad" tabIndex={42} />, node);
189
+ var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
190
+
191
+ if (ReactDOMFeatureFlags.useCreateElement) {
192
+ assertHistoryMatches([{
193
+ instanceID: inst._debugID,
194
+ type: 'update attribute',
195
+ payload: { className: 'rad' },
196
+ }, {
197
+ instanceID: inst._debugID,
198
+ type: 'update attribute',
199
+ payload: { tabIndex: 42 },
200
+ }, {
201
+ instanceID: inst._debugID,
202
+ type: 'mount',
203
+ payload: 'DIV',
204
+ }]);
205
+ } else {
206
+ assertHistoryMatches([{
207
+ instanceID: inst._debugID,
208
+ type: 'mount',
209
+ payload: '<div class="rad" tabindex="42" data-reactroot="" ' +
210
+ 'data-reactid="1"></div>',
211
+ }]);
212
+ }
213
+ });
214
+
215
+ it('gets recorded during an update', () => {
216
+ var node = document.createElement('div');
217
+ ReactDOM.render(<div />, node);
218
+ var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
219
+
220
+ ReactNativeOperationHistoryDevtool.clearHistory();
221
+ ReactDOM.render(<div className="rad" />, node);
222
+ ReactDOM.render(<div className="mad" tabIndex={42} />, node);
223
+ ReactDOM.render(<div tabIndex={43} />, node);
224
+ assertHistoryMatches([{
225
+ instanceID: inst._debugID,
226
+ type: 'update attribute',
227
+ payload: { className: 'rad' },
228
+ }, {
229
+ instanceID: inst._debugID,
230
+ type: 'update attribute',
231
+ payload: { className: 'mad' },
232
+ }, {
233
+ instanceID: inst._debugID,
234
+ type: 'update attribute',
235
+ payload: { tabIndex: 42 },
236
+ }, {
237
+ instanceID: inst._debugID,
238
+ type: 'remove attribute',
239
+ payload: 'className',
240
+ }, {
241
+ instanceID: inst._debugID,
242
+ type: 'update attribute',
243
+ payload: { tabIndex: 43 },
244
+ }]);
245
+ });
246
+ });
247
+
248
+ describe('attribute that gets removed with certain values', () => {
249
+ it('gets recorded as a removal during an update', () => {
250
+ var node = document.createElement('div');
251
+ ReactDOM.render(<div />, node);
252
+ var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
253
+
254
+ ReactNativeOperationHistoryDevtool.clearHistory();
255
+ ReactDOM.render(<div disabled={true} />, node);
256
+ ReactDOM.render(<div disabled={false} />, node);
257
+ assertHistoryMatches([{
258
+ instanceID: inst._debugID,
259
+ type: 'update attribute',
260
+ payload: { disabled: true },
261
+ }, {
262
+ instanceID: inst._debugID,
263
+ type: 'remove attribute',
264
+ payload: 'disabled',
265
+ }]);
266
+ });
267
+ });
268
+
269
+ describe('custom attribute', () => {
270
+ it('gets recorded during mount', () => {
271
+ var node = document.createElement('div');
272
+ ReactDOM.render(<div data-x="rad" data-y={42} />, node);
273
+ var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
274
+
275
+ if (ReactDOMFeatureFlags.useCreateElement) {
276
+ assertHistoryMatches([{
277
+ instanceID: inst._debugID,
278
+ type: 'update attribute',
279
+ payload: { 'data-x': 'rad' },
280
+ }, {
281
+ instanceID: inst._debugID,
282
+ type: 'update attribute',
283
+ payload: { 'data-y': 42 },
284
+ }, {
285
+ instanceID: inst._debugID,
286
+ type: 'mount',
287
+ payload: 'DIV',
288
+ }]);
289
+ } else {
290
+ assertHistoryMatches([{
291
+ instanceID: inst._debugID,
292
+ type: 'mount',
293
+ payload: '<div data-x="rad" data-y="42" data-reactroot="" ' +
294
+ 'data-reactid="1"></div>',
295
+ }]);
296
+ }
297
+ });
298
+
299
+ it('gets recorded during an update', () => {
300
+ var node = document.createElement('div');
301
+ ReactDOM.render(<div />, node);
302
+ var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
303
+
304
+ ReactNativeOperationHistoryDevtool.clearHistory();
305
+ ReactDOM.render(<div data-x="rad" />, node);
306
+ ReactDOM.render(<div data-x="mad" data-y={42} />, node);
307
+ ReactDOM.render(<div data-y={43} />, node);
308
+ assertHistoryMatches([{
309
+ instanceID: inst._debugID,
310
+ type: 'update attribute',
311
+ payload: { 'data-x': 'rad' },
312
+ }, {
313
+ instanceID: inst._debugID,
314
+ type: 'update attribute',
315
+ payload: { 'data-x': 'mad' },
316
+ }, {
317
+ instanceID: inst._debugID,
318
+ type: 'update attribute',
319
+ payload: { 'data-y': 42 },
320
+ }, {
321
+ instanceID: inst._debugID,
322
+ type: 'remove attribute',
323
+ payload: 'data-x',
324
+ }, {
325
+ instanceID: inst._debugID,
326
+ type: 'update attribute',
327
+ payload: { 'data-y': 43 },
328
+ }]);
329
+ });
330
+ });
331
+
332
+ describe('attribute on a web component', () => {
333
+ it('gets recorded during mount', () => {
334
+ var node = document.createElement('div');
335
+ ReactDOM.render(<my-component className="rad" tabIndex={42} />, node);
336
+ var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
337
+
338
+ if (ReactDOMFeatureFlags.useCreateElement) {
339
+ assertHistoryMatches([{
340
+ instanceID: inst._debugID,
341
+ type: 'update attribute',
342
+ payload: { className: 'rad' },
343
+ }, {
344
+ instanceID: inst._debugID,
345
+ type: 'update attribute',
346
+ payload: { tabIndex: 42 },
347
+ }, {
348
+ instanceID: inst._debugID,
349
+ type: 'mount',
350
+ payload: 'MY-COMPONENT',
351
+ }]);
352
+ } else {
353
+ assertHistoryMatches([{
354
+ instanceID: inst._debugID,
355
+ type: 'mount',
356
+ payload: '<my-component className="rad" tabIndex="42" ' +
357
+ 'data-reactroot="" data-reactid="1"></my-component>',
358
+ }]);
359
+ }
360
+ });
361
+
362
+ it('gets recorded during an update', () => {
363
+ var node = document.createElement('div');
364
+ ReactDOM.render(<my-component />, node);
365
+ var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
366
+
367
+ ReactNativeOperationHistoryDevtool.clearHistory();
368
+ ReactDOM.render(<my-component className="rad" />, node);
369
+ ReactDOM.render(<my-component className="mad" tabIndex={42} />, node);
370
+ ReactDOM.render(<my-component tabIndex={43} />, node);
371
+ assertHistoryMatches([{
372
+ instanceID: inst._debugID,
373
+ type: 'update attribute',
374
+ payload: { className: 'rad' },
375
+ }, {
376
+ instanceID: inst._debugID,
377
+ type: 'update attribute',
378
+ payload: { className: 'mad' },
379
+ }, {
380
+ instanceID: inst._debugID,
381
+ type: 'update attribute',
382
+ payload: { tabIndex: 42 },
383
+ }, {
384
+ instanceID: inst._debugID,
385
+ type: 'remove attribute',
386
+ payload: 'className',
387
+ }, {
388
+ instanceID: inst._debugID,
389
+ type: 'update attribute',
390
+ payload: { tabIndex: 43 },
391
+ }]);
392
+ });
393
+ });
394
+ });
395
+
396
+ describe('replace text', () => {
397
+ describe('text content', () => {
398
+ it('gets recorded during an update from text content', () => {
399
+ var node = document.createElement('div');
400
+ ReactDOM.render(<div>Hi.</div>, node);
401
+ var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
402
+
403
+ ReactNativeOperationHistoryDevtool.clearHistory();
404
+ ReactDOM.render(<div>Bye.</div>, node);
405
+ assertHistoryMatches([{
406
+ instanceID: inst._debugID,
407
+ type: 'replace text',
408
+ payload: 'Bye.',
409
+ }]);
410
+ });
411
+
412
+ it('gets recorded during an update from html', () => {
413
+ var node = document.createElement('div');
414
+ ReactDOM.render(<div dangerouslySetInnerHTML={{__html: 'Hi.'}} />, node);
415
+ var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
416
+
417
+ ReactNativeOperationHistoryDevtool.clearHistory();
418
+ ReactDOM.render(<div>Bye.</div>, node);
419
+ assertHistoryMatches([{
420
+ instanceID: inst._debugID,
421
+ type: 'replace text',
422
+ payload: 'Bye.',
423
+ }]);
424
+ });
425
+
426
+ it('gets recorded during an update from children', () => {
427
+ var node = document.createElement('div');
428
+ ReactDOM.render(<div><span /><p /></div>, node);
429
+ var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
430
+
431
+ ReactNativeOperationHistoryDevtool.clearHistory();
432
+ ReactDOM.render(<div>Bye.</div>, node);
433
+ assertHistoryMatches([{
434
+ instanceID: inst._debugID,
435
+ type: 'remove child',
436
+ payload: {fromIndex: 0},
437
+ }, {
438
+ instanceID: inst._debugID,
439
+ type: 'remove child',
440
+ payload: {fromIndex: 1},
441
+ }, {
442
+ instanceID: inst._debugID,
443
+ type: 'replace text',
444
+ payload: 'Bye.',
445
+ }]);
446
+ });
447
+
448
+ it('gets ignored if new text is equal', () => {
449
+ var node = document.createElement('div');
450
+ ReactDOM.render(<div>Hi.</div>, node);
451
+
452
+ ReactNativeOperationHistoryDevtool.clearHistory();
453
+ ReactDOM.render(<div>Hi.</div>, node);
454
+ assertHistoryMatches([]);
455
+ });
456
+ });
457
+
458
+ describe('text node', () => {
459
+ it('gets recorded during an update', () => {
460
+ var node = document.createElement('div');
461
+ ReactDOM.render(<div>{'Hi.'}{42}</div>, node);
462
+ var inst1 = ReactDOMComponentTree.getInstanceFromNode(node.firstChild.childNodes[0]);
463
+ var inst2 = ReactDOMComponentTree.getInstanceFromNode(node.firstChild.childNodes[3]);
464
+
465
+ ReactNativeOperationHistoryDevtool.clearHistory();
466
+ ReactDOM.render(<div>{'Bye.'}{43}</div>, node);
467
+ assertHistoryMatches([{
468
+ instanceID: inst1._debugID,
469
+ type: 'replace text',
470
+ payload: 'Bye.',
471
+ }, {
472
+ instanceID: inst2._debugID,
473
+ type: 'replace text',
474
+ payload: '43',
475
+ }]);
476
+ });
477
+
478
+ it('gets ignored if new text is equal', () => {
479
+ var node = document.createElement('div');
480
+ ReactDOM.render(<div>{'Hi.'}{42}</div>, node);
481
+
482
+ ReactNativeOperationHistoryDevtool.clearHistory();
483
+ ReactDOM.render(<div>{'Hi.'}{42}</div>, node);
484
+ assertHistoryMatches([]);
485
+ });
486
+ });
487
+ });
488
+
489
+ describe('replace with', () => {
490
+ it('gets recorded when composite renders to a different type', () => {
491
+ var element;
492
+ function Foo() {
493
+ return element;
494
+ }
495
+
496
+ var node = document.createElement('div');
497
+ element = <div />;
498
+ ReactDOM.render(<Foo />, node);
499
+ var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
500
+
501
+ ReactNativeOperationHistoryDevtool.clearHistory();
502
+ element = <span />;
503
+ ReactDOM.render(<Foo />, node);
504
+ assertHistoryMatches([{
505
+ instanceID: inst._debugID,
506
+ type: 'replace with',
507
+ payload: 'SPAN',
508
+ }]);
509
+ });
510
+
511
+ it('gets ignored if the type has not changed', () => {
512
+ var element;
513
+ function Foo() {
514
+ return element;
515
+ }
516
+
517
+ var node = document.createElement('div');
518
+ element = <div />;
519
+ ReactDOM.render(<Foo />, node);
520
+
521
+ ReactNativeOperationHistoryDevtool.clearHistory();
522
+ element = <div />;
523
+ ReactDOM.render(<Foo />, node);
524
+ assertHistoryMatches([]);
525
+ });
526
+ });
527
+
528
+ describe('replace children', () => {
529
+ it('gets recorded during an update from text content', () => {
530
+ var node = document.createElement('div');
531
+ ReactDOM.render(<div>Hi.</div>, node);
532
+ var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
533
+
534
+ ReactNativeOperationHistoryDevtool.clearHistory();
535
+ ReactDOM.render(
536
+ <div dangerouslySetInnerHTML={{__html: 'Bye.'}} />,
537
+ node
538
+ );
539
+ assertHistoryMatches([{
540
+ instanceID: inst._debugID,
541
+ type: 'replace children',
542
+ payload: 'Bye.',
543
+ }]);
544
+ });
545
+
546
+ it('gets recorded during an update from html', () => {
547
+ var node = document.createElement('div');
548
+ ReactDOM.render(
549
+ <div dangerouslySetInnerHTML={{__html: 'Hi.'}} />,
550
+ node
551
+ );
552
+ var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
553
+
554
+ ReactNativeOperationHistoryDevtool.clearHistory();
555
+ ReactDOM.render(
556
+ <div dangerouslySetInnerHTML={{__html: 'Bye.'}} />,
557
+ node
558
+ );
559
+ assertHistoryMatches([{
560
+ instanceID: inst._debugID,
561
+ type: 'replace children',
562
+ payload: 'Bye.',
563
+ }]);
564
+ });
565
+
566
+ it('gets recorded during an update from children', () => {
567
+ var node = document.createElement('div');
568
+ ReactDOM.render(<div><span /><p /></div>, node);
569
+ var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
570
+
571
+ ReactNativeOperationHistoryDevtool.clearHistory();
572
+ ReactDOM.render(
573
+ <div dangerouslySetInnerHTML={{__html: 'Hi.'}} />,
574
+ node
575
+ );
576
+ assertHistoryMatches([{
577
+ instanceID: inst._debugID,
578
+ type: 'remove child',
579
+ payload: {fromIndex: 0},
580
+ }, {
581
+ instanceID: inst._debugID,
582
+ type: 'remove child',
583
+ payload: {fromIndex: 1},
584
+ }, {
585
+ instanceID: inst._debugID,
586
+ type: 'replace children',
587
+ payload: 'Hi.',
588
+ }]);
589
+ });
590
+
591
+ it('gets ignored if new html is equal', () => {
592
+ var node = document.createElement('div');
593
+ ReactDOM.render(
594
+ <div dangerouslySetInnerHTML={{__html: 'Hi.'}} />,
595
+ node
596
+ );
597
+
598
+ ReactNativeOperationHistoryDevtool.clearHistory();
599
+ ReactDOM.render(
600
+ <div dangerouslySetInnerHTML={{__html: 'Hi.'}} />,
601
+ node
602
+ );
603
+ assertHistoryMatches([]);
604
+ });
605
+ });
606
+
607
+ describe('insert child', () => {
608
+ it('gets reported when a child is inserted', () => {
609
+ var node = document.createElement('div');
610
+ ReactDOM.render(<div><span /></div>, node);
611
+ var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
612
+
613
+ ReactNativeOperationHistoryDevtool.clearHistory();
614
+ ReactDOM.render(<div><span /><p /></div>, node);
615
+ assertHistoryMatches([{
616
+ instanceID: inst._debugID,
617
+ type: 'insert child',
618
+ payload: {toIndex: 1, content: 'P'},
619
+ }]);
620
+ });
621
+ });
622
+
623
+ describe('move child', () => {
624
+ it('gets reported when a child is inserted', () => {
625
+ var node = document.createElement('div');
626
+ ReactDOM.render(<div><span key="a" /><p key="b" /></div>, node);
627
+ var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
628
+
629
+ ReactNativeOperationHistoryDevtool.clearHistory();
630
+ ReactDOM.render(<div><p key="b" /><span key="a" /></div>, node);
631
+ assertHistoryMatches([{
632
+ instanceID: inst._debugID,
633
+ type: 'move child',
634
+ payload: {fromIndex: 0, toIndex: 1},
635
+ }]);
636
+ });
637
+ });
638
+
639
+ describe('remove child', () => {
640
+ it('gets reported when a child is removed', () => {
641
+ var node = document.createElement('div');
642
+ ReactDOM.render(<div><span key="a" /><p key="b" /></div>, node);
643
+ var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
644
+
645
+ ReactNativeOperationHistoryDevtool.clearHistory();
646
+ ReactDOM.render(<div><span key="a" /></div>, node);
647
+ assertHistoryMatches([{
648
+ instanceID: inst._debugID,
649
+ type: 'remove child',
650
+ payload: {fromIndex: 1},
651
+ }]);
652
+ });
653
+ });
654
+});
src/renderers/dom/client/ReactMount.js
+8
@@ -693,6 +693,14 @@ var ReactMount = {
693
setInnerHTML(container, markup);
694
ReactDOMComponentTree.precacheNode(instance, container.firstChild);
695
}
696
+
697
+ if (__DEV__) {
698
+ ReactInstrumentation.debugTool.onNativeOperation(
699
+ ReactDOMComponentTree.getInstanceFromNode(container.firstChild)._debugID,
700
+ 'mount',
701
+ markup.toString()
702
+ );
703
+ }
704
},
705
};
706
src/renderers/dom/client/__tests__/ReactDOMIDOperations-test.js
+5
-2
@@ -12,15 +12,18 @@
12
'use strict';
13
14
describe('ReactDOMIDOperations', function() {
15
+ var ReactDOMComponentTree = require('ReactDOMComponentTree');
16
var ReactDOMIDOperations = require('ReactDOMIDOperations');
17
var ReactMultiChildUpdateTypes = require('ReactMultiChildUpdateTypes');
18
19
it('should update innerHTML and preserve whitespace', function() {
20
var stubNode = document.createElement('div');
20
- var html = '\n \t <span> \n testContent \t </span> \n \t';
21
+ var stubInstance = {};
22
+ ReactDOMComponentTree.precacheNode(stubInstance, stubNode);
23
24
+ var html = '\n \t <span> \n testContent \t </span> \n \t';
25
ReactDOMIDOperations.dangerouslyProcessChildrenUpdates(
23
- {_nativeNode: stubNode},
26
+ stubInstance,
27
[{
28
type: ReactMultiChildUpdateTypes.SET_MARKUP,
29
content: html,
src/renderers/dom/client/utils/DOMChildrenOperations.js
+63
-1
@@ -14,6 +14,8 @@
14
var DOMLazyTree = require('DOMLazyTree');
15
var Danger = require('Danger');
16
var ReactMultiChildUpdateTypes = require('ReactMultiChildUpdateTypes');
17
+var ReactDOMComponentTree = require('ReactDOMComponentTree');
18
+var ReactInstrumentation = require('ReactInstrumentation');
19
var ReactPerf = require('ReactPerf');
20
21
var createMicrosoftUnsafeLocalFunction = require('createMicrosoftUnsafeLocalFunction');
@@ -120,6 +122,26 @@ function replaceDelimitedText(openingComment, closingComment, stringText) {
122
removeDelimitedText(parentNode, openingComment, closingComment);
123
}
124
}
125
+
126
+ if (__DEV__) {
127
+ ReactInstrumentation.debugTool.onNativeOperation(
128
+ ReactDOMComponentTree.getInstanceFromNode(openingComment)._debugID,
129
+ 'replace text',
130
+ stringText
131
+ );
132
+ }
133
+}
134
+
135
+var dangerouslyReplaceNodeWithMarkup = Danger.dangerouslyReplaceNodeWithMarkup;
136
+if (__DEV__) {
137
+ dangerouslyReplaceNodeWithMarkup = function(oldChild, markup, prevInstance) {
138
+ Danger.dangerouslyReplaceNodeWithMarkup(oldChild, markup);
139
+ ReactInstrumentation.debugTool.onNativeOperation(
140
+ prevInstance._debugID,
141
+ 'replace with',
142
+ markup.toString()
143
+ );
144
+ };
145
}
146
147
/**
@@ -127,7 +149,7 @@ function replaceDelimitedText(openingComment, closingComment, stringText) {
149
*/
150
var DOMChildrenOperations = {
151
130
- dangerouslyReplaceNodeWithMarkup: Danger.dangerouslyReplaceNodeWithMarkup,
152
+ dangerouslyReplaceNodeWithMarkup: dangerouslyReplaceNodeWithMarkup,
153
154
replaceDelimitedText: replaceDelimitedText,
155
@@ -139,6 +161,11 @@ var DOMChildrenOperations = {
161
* @internal
162
*/
163
processUpdates: function(parentNode, updates) {
164
+ if (__DEV__) {
165
+ var parentNodeDebugID =
166
+ ReactDOMComponentTree.getInstanceFromNode(parentNode)._debugID;
167
+ }
168
+
169
for (var k = 0; k < updates.length; k++) {
170
var update = updates[k];
171
switch (update.type) {
@@ -148,6 +175,13 @@ var DOMChildrenOperations = {
175
update.content,
176
getNodeAfter(parentNode, update.afterNode)
177
);
178
+ if (__DEV__) {
179
+ ReactInstrumentation.debugTool.onNativeOperation(
180
+ parentNodeDebugID,
181
+ 'insert child',
182
+ {toIndex: update.toIndex, content: update.content.toString()}
183
+ );
184
+ }
185
break;
186
case ReactMultiChildUpdateTypes.MOVE_EXISTING:
187
moveChild(
@@ -155,21 +189,49 @@ var DOMChildrenOperations = {
189
update.fromNode,
190
getNodeAfter(parentNode, update.afterNode)
191
);
192
+ if (__DEV__) {
193
+ ReactInstrumentation.debugTool.onNativeOperation(
194
+ parentNodeDebugID,
195
+ 'move child',
196
+ {fromIndex: update.fromIndex, toIndex: update.toIndex}
197
+ );
198
+ }
199
break;
200
case ReactMultiChildUpdateTypes.SET_MARKUP:
201
setInnerHTML(
202
parentNode,
203
update.content
204
);
205
+ if (__DEV__) {
206
+ ReactInstrumentation.debugTool.onNativeOperation(
207
+ parentNodeDebugID,
208
+ 'replace children',
209
+ update.content.toString()
210
+ );
211
+ }
212
break;
213
case ReactMultiChildUpdateTypes.TEXT_CONTENT:
214
setTextContent(
215
parentNode,
216
update.content
217
);
218
+ if (__DEV__) {
219
+ ReactInstrumentation.debugTool.onNativeOperation(
220
+ parentNodeDebugID,
221
+ 'replace text',
222
+ update.content.toString()
223
+ );
224
+ }
225
break;
226
case ReactMultiChildUpdateTypes.REMOVE_NODE:
227
removeChild(parentNode, update.fromNode);
228
+ if (__DEV__) {
229
+ ReactInstrumentation.debugTool.onNativeOperation(
230
+ parentNodeDebugID,
231
+ 'remove child',
232
+ {fromIndex: update.fromIndex}
233
+ );
234
+ }
235
break;
236
}
237
}
src/renderers/dom/client/utils/DOMLazyTree.js
+5
@@ -96,12 +96,17 @@ function queueText(tree, text) {
96
}
97
}
98
99
+function toString() {
100
+ return this.node.nodeName;
101
+}
102
+
103
function DOMLazyTree(node) {
104
return {
105
node: node,
106
children: [],
107
html: null,
108
text: null,
109
+ toString,
110
};
111
}
112
src/renderers/dom/shared/CSSPropertyOperations.js
+9
@@ -13,6 +13,7 @@
13
14
var CSSProperty = require('CSSProperty');
15
var ExecutionEnvironment = require('ExecutionEnvironment');
16
+var ReactInstrumentation = require('ReactInstrumentation');
17
var ReactPerf = require('ReactPerf');
18
19
var camelizeStyleName = require('camelizeStyleName');
@@ -192,6 +193,14 @@ var CSSPropertyOperations = {
193
* @param {ReactDOMComponent} component
194
*/
195
setValueForStyles: function(node, styles, component) {
196
+ if (__DEV__) {
197
+ ReactInstrumentation.debugTool.onNativeOperation(
198
+ component._debugID,
199
+ 'update styles',
200
+ styles
201
+ );
202
+ }
203
+
204
var style = node.style;
205
for (var styleName in styles) {
206
if (!styles.hasOwnProperty(styleName)) {
src/renderers/dom/shared/DOMPropertyOperations.js
+34
-6
@@ -12,7 +12,9 @@
12
'use strict';
13
14
var DOMProperty = require('DOMProperty');
15
+var ReactDOMComponentTree = require('ReactDOMComponentTree');
16
var ReactDOMInstrumentation = require('ReactDOMInstrumentation');
17
+var ReactInstrumentation = require('ReactInstrumentation');
18
var ReactPerf = require('ReactPerf');
19
20
var quoteAttributeValueForBrowser = require('quoteAttributeValueForBrowser');
@@ -134,9 +136,6 @@ var DOMPropertyOperations = {
136
* @param {*} value
137
*/
138
setValueForProperty: function(node, name, value) {
137
- if (__DEV__) {
138
- ReactDOMInstrumentation.debugTool.onSetValueForProperty(node, name, value);
139
- }
139
var propertyInfo = DOMProperty.properties.hasOwnProperty(name) ?
140
DOMProperty.properties[name] : null;
141
if (propertyInfo) {
@@ -145,6 +144,7 @@ var DOMPropertyOperations = {
144
mutationMethod(node, value);
145
} else if (shouldIgnoreValue(propertyInfo, value)) {
146
this.deleteValueForProperty(node, name);
147
+ return;
148
} else if (propertyInfo.mustUseProperty) {
149
var propName = propertyInfo.propertyName;
150
// Must explicitly cast values for HAS_SIDE_EFFECTS-properties to the
@@ -171,6 +171,18 @@ var DOMPropertyOperations = {
171
}
172
} else if (DOMProperty.isCustomAttribute(name)) {
173
DOMPropertyOperations.setValueForAttribute(node, name, value);
174
+ return;
175
+ }
176
+
177
+ if (__DEV__) {
178
+ ReactDOMInstrumentation.debugTool.onSetValueForProperty(node, name, value);
179
+ var payload = {};
180
+ payload[name] = value;
181
+ ReactInstrumentation.debugTool.onNativeOperation(
182
+ ReactDOMComponentTree.getInstanceFromNode(node)._debugID,
183
+ 'update attribute',
184
+ payload
185
+ );
186
}
187
},
188
@@ -183,6 +195,16 @@ var DOMPropertyOperations = {
195
} else {
196
node.setAttribute(name, '' + value);
197
}
198
+
199
+ if (__DEV__) {
200
+ var payload = {};
201
+ payload[name] = value;
202
+ ReactInstrumentation.debugTool.onNativeOperation(
203
+ ReactDOMComponentTree.getInstanceFromNode(node)._debugID,
204
+ 'update attribute',
205
+ payload
206
+ );
207
+ }
208
},
209
210
/**
@@ -192,9 +214,6 @@ var DOMPropertyOperations = {
214
* @param {string} name
215
*/
216
deleteValueForProperty: function(node, name) {
195
- if (__DEV__) {
196
- ReactDOMInstrumentation.debugTool.onDeleteValueForProperty(node, name);
197
- }
217
var propertyInfo = DOMProperty.properties.hasOwnProperty(name) ?
218
DOMProperty.properties[name] : null;
219
if (propertyInfo) {
@@ -218,6 +237,15 @@ var DOMPropertyOperations = {
237
} else if (DOMProperty.isCustomAttribute(name)) {
238
node.removeAttribute(name);
239
}
240
+
241
+ if (__DEV__) {
242
+ ReactDOMInstrumentation.debugTool.onDeleteValueForProperty(node, name);
243
+ ReactInstrumentation.debugTool.onNativeOperation(
244
+ ReactDOMComponentTree.getInstanceFromNode(node)._debugID,
245
+ 'remove attribute',
246
+ name
247
+ );
248
+ }
249
},
250
251
};
src/renderers/dom/shared/__tests__/DOMPropertyOperations-test.js
+10
@@ -14,6 +14,7 @@
14
describe('DOMPropertyOperations', function() {
15
var DOMPropertyOperations;
16
var DOMProperty;
17
+ var ReactDOMComponentTree;
18
19
beforeEach(function() {
20
jest.resetModuleRegistry();
@@ -22,6 +23,7 @@ describe('DOMPropertyOperations', function() {
23
24
DOMPropertyOperations = require('DOMPropertyOperations');
25
DOMProperty = require('DOMProperty');
26
+ ReactDOMComponentTree = require('ReactDOMComponentTree');
27
});
28
29
describe('createMarkupForProperty', function() {
@@ -175,6 +177,7 @@ describe('DOMPropertyOperations', function() {
177
178
beforeEach(function() {
179
stubNode = document.createElement('div');
180
+ ReactDOMComponentTree.precacheNode({}, stubNode);
181
});
182
183
it('should set values as properties by default', function() {
@@ -223,6 +226,8 @@ describe('DOMPropertyOperations', function() {
226
227
it('should not remove empty attributes for special properties', function() {
228
stubNode = document.createElement('input');
229
+ ReactDOMComponentTree.precacheNode({}, stubNode);
230
+
231
DOMPropertyOperations.setValueForProperty(stubNode, 'value', '');
232
// JSDOM does not behave correctly for attributes/properties
233
//expect(stubNode.getAttribute('value')).toBe('');
@@ -346,6 +351,7 @@ describe('DOMPropertyOperations', function() {
351
352
beforeEach(function() {
353
stubNode = document.createElement('div');
354
+ ReactDOMComponentTree.precacheNode({}, stubNode);
355
});
356
357
it('should remove attributes for normal properties', function() {
@@ -361,6 +367,8 @@ describe('DOMPropertyOperations', function() {
367
368
it('should not remove attributes for special properties', function() {
369
stubNode = document.createElement('input');
370
+ ReactDOMComponentTree.precacheNode({}, stubNode);
371
+
372
stubNode.setAttribute('value', 'foo');
373
374
DOMPropertyOperations.deleteValueForProperty(stubNode, 'value');
@@ -371,6 +379,8 @@ describe('DOMPropertyOperations', function() {
379
380
it('should not leave all options selected when deleting multiple', function() {
381
stubNode = document.createElement('select');
382
+ ReactDOMComponentTree.precacheNode({}, stubNode);
383
+
384
stubNode.multiple = true;
385
stubNode.appendChild(document.createElement('option'));
386
stubNode.appendChild(document.createElement('option'));
src/renderers/shared/reconciler/ReactCompositeComponent.js
+8
-3
@@ -888,7 +888,11 @@ var ReactCompositeComponentMixin = {
888
}
889
}
890
891
- this._replaceNodeWithMarkup(oldNativeNode, nextMarkup);
891
+ this._replaceNodeWithMarkup(
892
+ oldNativeNode,
893
+ nextMarkup,
894
+ prevComponentInstance
895
+ );
896
}
897
},
898
@@ -897,10 +901,11 @@ var ReactCompositeComponentMixin = {
901
*
902
* @protected
903
*/
900
- _replaceNodeWithMarkup: function(oldNativeNode, nextMarkup) {
904
+ _replaceNodeWithMarkup: function(oldNativeNode, nextMarkup, prevInstance) {
905
ReactComponentEnvironment.replaceNodeWithMarkup(
906
oldNativeNode,
903
- nextMarkup
907
+ nextMarkup,
908
+ prevInstance
909
);
910
},
911