@samitouri / QOS-React / commits / aaadb31827

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