From aaadb31827621470fb9be92b67ffdd75ade7775b Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Tue, 26 Apr 2016 01:28:44 +0100 Subject: [PATCH] Add ReactNativeOperationHistoryDevtool to track native operations --- src/isomorphic/ReactDebugTool.js | 3 + .../ReactNativeOperationHistoryDevtool.js | 34 + ...ReactNativeOperationHistoryDevtool-test.js | 654 ++++++++++++++++++ src/renderers/dom/client/ReactMount.js | 8 + .../__tests__/ReactDOMIDOperations-test.js | 7 +- .../dom/client/utils/DOMChildrenOperations.js | 64 +- src/renderers/dom/client/utils/DOMLazyTree.js | 5 + .../dom/shared/CSSPropertyOperations.js | 9 + .../dom/shared/DOMPropertyOperations.js | 40 +- .../__tests__/DOMPropertyOperations-test.js | 10 + .../reconciler/ReactCompositeComponent.js | 11 +- 11 files changed, 833 insertions(+), 12 deletions(-) create mode 100644 src/isomorphic/devtools/ReactNativeOperationHistoryDevtool.js create mode 100644 src/isomorphic/devtools/__tests__/ReactNativeOperationHistoryDevtool-test.js diff --git a/src/isomorphic/ReactDebugTool.js b/src/isomorphic/ReactDebugTool.js index deb3ba73a4a..9f86052ea5c 100644 --- a/src/isomorphic/ReactDebugTool.js +++ b/src/isomorphic/ReactDebugTool.js @@ -55,6 +55,9 @@ var ReactDebugTool = { onEndProcessingChildContext() { emitEvent('onEndProcessingChildContext'); }, + onNativeOperation(debugID, type, payload) { + emitEvent('onNativeOperation', debugID, type, payload); + }, onSetState() { emitEvent('onSetState'); }, diff --git a/src/isomorphic/devtools/ReactNativeOperationHistoryDevtool.js b/src/isomorphic/devtools/ReactNativeOperationHistoryDevtool.js new file mode 100644 index 00000000000..50478cc405d --- /dev/null +++ b/src/isomorphic/devtools/ReactNativeOperationHistoryDevtool.js @@ -0,0 +1,34 @@ +/** + * Copyright 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ReactNativeOperationHistoryDevtool + */ + +'use strict'; + +var history = []; + +var ReactNativeOperationHistoryDevtool = { + onNativeOperation(debugID, type, payload) { + history.push({ + instanceID: debugID, + type, + payload, + }); + }, + + clearHistory() { + history = []; + }, + + getHistory() { + return history; + }, +}; + +module.exports = ReactNativeOperationHistoryDevtool; diff --git a/src/isomorphic/devtools/__tests__/ReactNativeOperationHistoryDevtool-test.js b/src/isomorphic/devtools/__tests__/ReactNativeOperationHistoryDevtool-test.js new file mode 100644 index 00000000000..3b5924aae5f --- /dev/null +++ b/src/isomorphic/devtools/__tests__/ReactNativeOperationHistoryDevtool-test.js @@ -0,0 +1,654 @@ +/** + * Copyright 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @emails react-core + */ + +'use strict'; + +describe('ReactNativeOperationHistoryDevtool', () => { + var React; + var ReactDebugTool; + var ReactDOM; + var ReactDOMComponentTree; + var ReactDOMFeatureFlags; + var ReactNativeOperationHistoryDevtool; + + beforeEach(() => { + jest.resetModuleRegistry(); + + React = require('React'); + ReactDebugTool = require('ReactDebugTool'); + ReactDOM = require('ReactDOM'); + ReactDOMComponentTree = require('ReactDOMComponentTree'); + ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); + ReactNativeOperationHistoryDevtool = require('ReactNativeOperationHistoryDevtool'); + + ReactDebugTool.addDevtool(ReactNativeOperationHistoryDevtool); + }); + + afterEach(() => { + ReactDebugTool.removeDevtool(ReactNativeOperationHistoryDevtool); + }); + + function assertHistoryMatches(expectedHistory) { + var actualHistory = ReactNativeOperationHistoryDevtool.getHistory(); + expect(actualHistory).toEqual(expectedHistory); + } + + describe('mount', () => { + it('gets recorded for native roots', () => { + var node = document.createElement('div'); + ReactDOM.render(

Hi.

, node); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'mount', + payload: ReactDOMFeatureFlags.useCreateElement ? + 'DIV' : + '

Hi.

', + }]); + }); + + it('gets recorded for composite roots', () => { + function Foo() { + return

Hi.

; + } + var node = document.createElement('div'); + ReactDOM.render(, node); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'mount', + payload: ReactDOMFeatureFlags.useCreateElement ? + 'DIV' : + '
' + + '

Hi.

', + }]); + }); + + it('gets recorded for composite roots that return null', () => { + function Foo() { + return null; + } + var node = document.createElement('div'); + ReactDOM.render(, node); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'mount', + payload: ReactDOMFeatureFlags.useCreateElement ? + '#comment' : + '', + }]); + }); + }); + + describe('update styles', () => { + it('gets recorded during mount', () => { + var node = document.createElement('div'); + ReactDOM.render(
, node); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + + if (ReactDOMFeatureFlags.useCreateElement) { + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'update styles', + payload: { + color: 'red', + backgroundColor: 'yellow', + }, + }, { + instanceID: inst._debugID, + type: 'mount', + payload: 'DIV', + }]); + } else { + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'mount', + payload: '
', + }]); + } + }); + + it('gets recorded during an update', () => { + var node = document.createElement('div'); + ReactDOM.render(
, node); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + + ReactNativeOperationHistoryDevtool.clearHistory(); + ReactDOM.render(
, node); + ReactDOM.render(
, node); + ReactDOM.render(
, node); + ReactDOM.render(
, node); + + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'update styles', + payload: { color: 'red' }, + }, { + instanceID: inst._debugID, + type: 'update styles', + payload: { color: 'blue', backgroundColor: 'yellow' }, + }, { + instanceID: inst._debugID, + type: 'update styles', + payload: { color: '', backgroundColor: 'green' }, + }, { + instanceID: inst._debugID, + type: 'update styles', + payload: { backgroundColor: '' }, + }]); + }); + + it('gets ignored if the styles are shallowly equal', () => { + var node = document.createElement('div'); + ReactDOM.render(
, node); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + + ReactNativeOperationHistoryDevtool.clearHistory(); + ReactDOM.render(
, node); + ReactDOM.render(
, node); + + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'update styles', + payload: { + color: 'red', + backgroundColor: 'yellow', + }, + }]); + }); + }); + + describe('update attribute', () => { + describe('simple attribute', () => { + it('gets recorded during mount', () => { + var node = document.createElement('div'); + ReactDOM.render(
, node); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + + if (ReactDOMFeatureFlags.useCreateElement) { + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'update attribute', + payload: { className: 'rad' }, + }, { + instanceID: inst._debugID, + type: 'update attribute', + payload: { tabIndex: 42 }, + }, { + instanceID: inst._debugID, + type: 'mount', + payload: 'DIV', + }]); + } else { + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'mount', + payload: '
', + }]); + } + }); + + it('gets recorded during an update', () => { + var node = document.createElement('div'); + ReactDOM.render(
, node); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + + ReactNativeOperationHistoryDevtool.clearHistory(); + ReactDOM.render(
, node); + ReactDOM.render(
, node); + ReactDOM.render(
, node); + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'update attribute', + payload: { className: 'rad' }, + }, { + instanceID: inst._debugID, + type: 'update attribute', + payload: { className: 'mad' }, + }, { + instanceID: inst._debugID, + type: 'update attribute', + payload: { tabIndex: 42 }, + }, { + instanceID: inst._debugID, + type: 'remove attribute', + payload: 'className', + }, { + instanceID: inst._debugID, + type: 'update attribute', + payload: { tabIndex: 43 }, + }]); + }); + }); + + describe('attribute that gets removed with certain values', () => { + it('gets recorded as a removal during an update', () => { + var node = document.createElement('div'); + ReactDOM.render(
, node); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + + ReactNativeOperationHistoryDevtool.clearHistory(); + ReactDOM.render(
, node); + ReactDOM.render(
, node); + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'update attribute', + payload: { disabled: true }, + }, { + instanceID: inst._debugID, + type: 'remove attribute', + payload: 'disabled', + }]); + }); + }); + + describe('custom attribute', () => { + it('gets recorded during mount', () => { + var node = document.createElement('div'); + ReactDOM.render(
, node); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + + if (ReactDOMFeatureFlags.useCreateElement) { + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'update attribute', + payload: { 'data-x': 'rad' }, + }, { + instanceID: inst._debugID, + type: 'update attribute', + payload: { 'data-y': 42 }, + }, { + instanceID: inst._debugID, + type: 'mount', + payload: 'DIV', + }]); + } else { + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'mount', + payload: '
', + }]); + } + }); + + it('gets recorded during an update', () => { + var node = document.createElement('div'); + ReactDOM.render(
, node); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + + ReactNativeOperationHistoryDevtool.clearHistory(); + ReactDOM.render(
, node); + ReactDOM.render(
, node); + ReactDOM.render(
, node); + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'update attribute', + payload: { 'data-x': 'rad' }, + }, { + instanceID: inst._debugID, + type: 'update attribute', + payload: { 'data-x': 'mad' }, + }, { + instanceID: inst._debugID, + type: 'update attribute', + payload: { 'data-y': 42 }, + }, { + instanceID: inst._debugID, + type: 'remove attribute', + payload: 'data-x', + }, { + instanceID: inst._debugID, + type: 'update attribute', + payload: { 'data-y': 43 }, + }]); + }); + }); + + describe('attribute on a web component', () => { + it('gets recorded during mount', () => { + var node = document.createElement('div'); + ReactDOM.render(, node); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + + if (ReactDOMFeatureFlags.useCreateElement) { + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'update attribute', + payload: { className: 'rad' }, + }, { + instanceID: inst._debugID, + type: 'update attribute', + payload: { tabIndex: 42 }, + }, { + instanceID: inst._debugID, + type: 'mount', + payload: 'MY-COMPONENT', + }]); + } else { + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'mount', + payload: '', + }]); + } + }); + + it('gets recorded during an update', () => { + var node = document.createElement('div'); + ReactDOM.render(, node); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + + ReactNativeOperationHistoryDevtool.clearHistory(); + ReactDOM.render(, node); + ReactDOM.render(, node); + ReactDOM.render(, node); + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'update attribute', + payload: { className: 'rad' }, + }, { + instanceID: inst._debugID, + type: 'update attribute', + payload: { className: 'mad' }, + }, { + instanceID: inst._debugID, + type: 'update attribute', + payload: { tabIndex: 42 }, + }, { + instanceID: inst._debugID, + type: 'remove attribute', + payload: 'className', + }, { + instanceID: inst._debugID, + type: 'update attribute', + payload: { tabIndex: 43 }, + }]); + }); + }); + }); + + describe('replace text', () => { + describe('text content', () => { + it('gets recorded during an update from text content', () => { + var node = document.createElement('div'); + ReactDOM.render(
Hi.
, node); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + + ReactNativeOperationHistoryDevtool.clearHistory(); + ReactDOM.render(
Bye.
, node); + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'replace text', + payload: 'Bye.', + }]); + }); + + it('gets recorded during an update from html', () => { + var node = document.createElement('div'); + ReactDOM.render(
, node); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + + ReactNativeOperationHistoryDevtool.clearHistory(); + ReactDOM.render(
Bye.
, node); + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'replace text', + payload: 'Bye.', + }]); + }); + + it('gets recorded during an update from children', () => { + var node = document.createElement('div'); + ReactDOM.render(

, node); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + + ReactNativeOperationHistoryDevtool.clearHistory(); + ReactDOM.render(
Bye.
, node); + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'remove child', + payload: {fromIndex: 0}, + }, { + instanceID: inst._debugID, + type: 'remove child', + payload: {fromIndex: 1}, + }, { + instanceID: inst._debugID, + type: 'replace text', + payload: 'Bye.', + }]); + }); + + it('gets ignored if new text is equal', () => { + var node = document.createElement('div'); + ReactDOM.render(
Hi.
, node); + + ReactNativeOperationHistoryDevtool.clearHistory(); + ReactDOM.render(
Hi.
, node); + assertHistoryMatches([]); + }); + }); + + describe('text node', () => { + it('gets recorded during an update', () => { + var node = document.createElement('div'); + ReactDOM.render(
{'Hi.'}{42}
, node); + var inst1 = ReactDOMComponentTree.getInstanceFromNode(node.firstChild.childNodes[0]); + var inst2 = ReactDOMComponentTree.getInstanceFromNode(node.firstChild.childNodes[3]); + + ReactNativeOperationHistoryDevtool.clearHistory(); + ReactDOM.render(
{'Bye.'}{43}
, node); + assertHistoryMatches([{ + instanceID: inst1._debugID, + type: 'replace text', + payload: 'Bye.', + }, { + instanceID: inst2._debugID, + type: 'replace text', + payload: '43', + }]); + }); + + it('gets ignored if new text is equal', () => { + var node = document.createElement('div'); + ReactDOM.render(
{'Hi.'}{42}
, node); + + ReactNativeOperationHistoryDevtool.clearHistory(); + ReactDOM.render(
{'Hi.'}{42}
, node); + assertHistoryMatches([]); + }); + }); + }); + + describe('replace with', () => { + it('gets recorded when composite renders to a different type', () => { + var element; + function Foo() { + return element; + } + + var node = document.createElement('div'); + element =
; + ReactDOM.render(, node); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + + ReactNativeOperationHistoryDevtool.clearHistory(); + element = ; + ReactDOM.render(, node); + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'replace with', + payload: 'SPAN', + }]); + }); + + it('gets ignored if the type has not changed', () => { + var element; + function Foo() { + return element; + } + + var node = document.createElement('div'); + element =
; + ReactDOM.render(, node); + + ReactNativeOperationHistoryDevtool.clearHistory(); + element =
; + ReactDOM.render(, node); + assertHistoryMatches([]); + }); + }); + + describe('replace children', () => { + it('gets recorded during an update from text content', () => { + var node = document.createElement('div'); + ReactDOM.render(
Hi.
, node); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + + ReactNativeOperationHistoryDevtool.clearHistory(); + ReactDOM.render( +
, + node + ); + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'replace children', + payload: 'Bye.', + }]); + }); + + it('gets recorded during an update from html', () => { + var node = document.createElement('div'); + ReactDOM.render( +
, + node + ); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + + ReactNativeOperationHistoryDevtool.clearHistory(); + ReactDOM.render( +
, + node + ); + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'replace children', + payload: 'Bye.', + }]); + }); + + it('gets recorded during an update from children', () => { + var node = document.createElement('div'); + ReactDOM.render(

, node); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + + ReactNativeOperationHistoryDevtool.clearHistory(); + ReactDOM.render( +
, + node + ); + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'remove child', + payload: {fromIndex: 0}, + }, { + instanceID: inst._debugID, + type: 'remove child', + payload: {fromIndex: 1}, + }, { + instanceID: inst._debugID, + type: 'replace children', + payload: 'Hi.', + }]); + }); + + it('gets ignored if new html is equal', () => { + var node = document.createElement('div'); + ReactDOM.render( +
, + node + ); + + ReactNativeOperationHistoryDevtool.clearHistory(); + ReactDOM.render( +
, + node + ); + assertHistoryMatches([]); + }); + }); + + describe('insert child', () => { + it('gets reported when a child is inserted', () => { + var node = document.createElement('div'); + ReactDOM.render(
, node); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + + ReactNativeOperationHistoryDevtool.clearHistory(); + ReactDOM.render(

, node); + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'insert child', + payload: {toIndex: 1, content: 'P'}, + }]); + }); + }); + + describe('move child', () => { + it('gets reported when a child is inserted', () => { + var node = document.createElement('div'); + ReactDOM.render(

, node); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + + ReactNativeOperationHistoryDevtool.clearHistory(); + ReactDOM.render(

, node); + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'move child', + payload: {fromIndex: 0, toIndex: 1}, + }]); + }); + }); + + describe('remove child', () => { + it('gets reported when a child is removed', () => { + var node = document.createElement('div'); + ReactDOM.render(

, node); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + + ReactNativeOperationHistoryDevtool.clearHistory(); + ReactDOM.render(
, node); + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'remove child', + payload: {fromIndex: 1}, + }]); + }); + }); +}); diff --git a/src/renderers/dom/client/ReactMount.js b/src/renderers/dom/client/ReactMount.js index 1984e146f3f..2981814ead2 100644 --- a/src/renderers/dom/client/ReactMount.js +++ b/src/renderers/dom/client/ReactMount.js @@ -693,6 +693,14 @@ var ReactMount = { setInnerHTML(container, markup); ReactDOMComponentTree.precacheNode(instance, container.firstChild); } + + if (__DEV__) { + ReactInstrumentation.debugTool.onNativeOperation( + ReactDOMComponentTree.getInstanceFromNode(container.firstChild)._debugID, + 'mount', + markup.toString() + ); + } }, }; diff --git a/src/renderers/dom/client/__tests__/ReactDOMIDOperations-test.js b/src/renderers/dom/client/__tests__/ReactDOMIDOperations-test.js index bdeb485e244..1e5c05a6aae 100644 --- a/src/renderers/dom/client/__tests__/ReactDOMIDOperations-test.js +++ b/src/renderers/dom/client/__tests__/ReactDOMIDOperations-test.js @@ -12,15 +12,18 @@ 'use strict'; describe('ReactDOMIDOperations', function() { + var ReactDOMComponentTree = require('ReactDOMComponentTree'); var ReactDOMIDOperations = require('ReactDOMIDOperations'); var ReactMultiChildUpdateTypes = require('ReactMultiChildUpdateTypes'); it('should update innerHTML and preserve whitespace', function() { var stubNode = document.createElement('div'); - var html = '\n \t \n testContent \t \n \t'; + var stubInstance = {}; + ReactDOMComponentTree.precacheNode(stubInstance, stubNode); + var html = '\n \t \n testContent \t \n \t'; ReactDOMIDOperations.dangerouslyProcessChildrenUpdates( - {_nativeNode: stubNode}, + stubInstance, [{ type: ReactMultiChildUpdateTypes.SET_MARKUP, content: html, diff --git a/src/renderers/dom/client/utils/DOMChildrenOperations.js b/src/renderers/dom/client/utils/DOMChildrenOperations.js index f123aef6ba5..3072a8d5b5e 100644 --- a/src/renderers/dom/client/utils/DOMChildrenOperations.js +++ b/src/renderers/dom/client/utils/DOMChildrenOperations.js @@ -14,6 +14,8 @@ var DOMLazyTree = require('DOMLazyTree'); var Danger = require('Danger'); var ReactMultiChildUpdateTypes = require('ReactMultiChildUpdateTypes'); +var ReactDOMComponentTree = require('ReactDOMComponentTree'); +var ReactInstrumentation = require('ReactInstrumentation'); var ReactPerf = require('ReactPerf'); var createMicrosoftUnsafeLocalFunction = require('createMicrosoftUnsafeLocalFunction'); @@ -120,6 +122,26 @@ function replaceDelimitedText(openingComment, closingComment, stringText) { removeDelimitedText(parentNode, openingComment, closingComment); } } + + if (__DEV__) { + ReactInstrumentation.debugTool.onNativeOperation( + ReactDOMComponentTree.getInstanceFromNode(openingComment)._debugID, + 'replace text', + stringText + ); + } +} + +var dangerouslyReplaceNodeWithMarkup = Danger.dangerouslyReplaceNodeWithMarkup; +if (__DEV__) { + dangerouslyReplaceNodeWithMarkup = function(oldChild, markup, prevInstance) { + Danger.dangerouslyReplaceNodeWithMarkup(oldChild, markup); + ReactInstrumentation.debugTool.onNativeOperation( + prevInstance._debugID, + 'replace with', + markup.toString() + ); + }; } /** @@ -127,7 +149,7 @@ function replaceDelimitedText(openingComment, closingComment, stringText) { */ var DOMChildrenOperations = { - dangerouslyReplaceNodeWithMarkup: Danger.dangerouslyReplaceNodeWithMarkup, + dangerouslyReplaceNodeWithMarkup: dangerouslyReplaceNodeWithMarkup, replaceDelimitedText: replaceDelimitedText, @@ -139,6 +161,11 @@ var DOMChildrenOperations = { * @internal */ processUpdates: function(parentNode, updates) { + if (__DEV__) { + var parentNodeDebugID = + ReactDOMComponentTree.getInstanceFromNode(parentNode)._debugID; + } + for (var k = 0; k < updates.length; k++) { var update = updates[k]; switch (update.type) { @@ -148,6 +175,13 @@ var DOMChildrenOperations = { update.content, getNodeAfter(parentNode, update.afterNode) ); + if (__DEV__) { + ReactInstrumentation.debugTool.onNativeOperation( + parentNodeDebugID, + 'insert child', + {toIndex: update.toIndex, content: update.content.toString()} + ); + } break; case ReactMultiChildUpdateTypes.MOVE_EXISTING: moveChild( @@ -155,21 +189,49 @@ var DOMChildrenOperations = { update.fromNode, getNodeAfter(parentNode, update.afterNode) ); + if (__DEV__) { + ReactInstrumentation.debugTool.onNativeOperation( + parentNodeDebugID, + 'move child', + {fromIndex: update.fromIndex, toIndex: update.toIndex} + ); + } break; case ReactMultiChildUpdateTypes.SET_MARKUP: setInnerHTML( parentNode, update.content ); + if (__DEV__) { + ReactInstrumentation.debugTool.onNativeOperation( + parentNodeDebugID, + 'replace children', + update.content.toString() + ); + } break; case ReactMultiChildUpdateTypes.TEXT_CONTENT: setTextContent( parentNode, update.content ); + if (__DEV__) { + ReactInstrumentation.debugTool.onNativeOperation( + parentNodeDebugID, + 'replace text', + update.content.toString() + ); + } break; case ReactMultiChildUpdateTypes.REMOVE_NODE: removeChild(parentNode, update.fromNode); + if (__DEV__) { + ReactInstrumentation.debugTool.onNativeOperation( + parentNodeDebugID, + 'remove child', + {fromIndex: update.fromIndex} + ); + } break; } } diff --git a/src/renderers/dom/client/utils/DOMLazyTree.js b/src/renderers/dom/client/utils/DOMLazyTree.js index 249eb475c5c..4e62889f2df 100644 --- a/src/renderers/dom/client/utils/DOMLazyTree.js +++ b/src/renderers/dom/client/utils/DOMLazyTree.js @@ -96,12 +96,17 @@ function queueText(tree, text) { } } +function toString() { + return this.node.nodeName; +} + function DOMLazyTree(node) { return { node: node, children: [], html: null, text: null, + toString, }; } diff --git a/src/renderers/dom/shared/CSSPropertyOperations.js b/src/renderers/dom/shared/CSSPropertyOperations.js index aac1b75f87b..721fb72a0f2 100644 --- a/src/renderers/dom/shared/CSSPropertyOperations.js +++ b/src/renderers/dom/shared/CSSPropertyOperations.js @@ -13,6 +13,7 @@ var CSSProperty = require('CSSProperty'); var ExecutionEnvironment = require('ExecutionEnvironment'); +var ReactInstrumentation = require('ReactInstrumentation'); var ReactPerf = require('ReactPerf'); var camelizeStyleName = require('camelizeStyleName'); @@ -192,6 +193,14 @@ var CSSPropertyOperations = { * @param {ReactDOMComponent} component */ setValueForStyles: function(node, styles, component) { + if (__DEV__) { + ReactInstrumentation.debugTool.onNativeOperation( + component._debugID, + 'update styles', + styles + ); + } + var style = node.style; for (var styleName in styles) { if (!styles.hasOwnProperty(styleName)) { diff --git a/src/renderers/dom/shared/DOMPropertyOperations.js b/src/renderers/dom/shared/DOMPropertyOperations.js index 1ce9e29b753..9c67c50cfbf 100644 --- a/src/renderers/dom/shared/DOMPropertyOperations.js +++ b/src/renderers/dom/shared/DOMPropertyOperations.js @@ -12,7 +12,9 @@ 'use strict'; var DOMProperty = require('DOMProperty'); +var ReactDOMComponentTree = require('ReactDOMComponentTree'); var ReactDOMInstrumentation = require('ReactDOMInstrumentation'); +var ReactInstrumentation = require('ReactInstrumentation'); var ReactPerf = require('ReactPerf'); var quoteAttributeValueForBrowser = require('quoteAttributeValueForBrowser'); @@ -134,9 +136,6 @@ var DOMPropertyOperations = { * @param {*} value */ setValueForProperty: function(node, name, value) { - if (__DEV__) { - ReactDOMInstrumentation.debugTool.onSetValueForProperty(node, name, value); - } var propertyInfo = DOMProperty.properties.hasOwnProperty(name) ? DOMProperty.properties[name] : null; if (propertyInfo) { @@ -145,6 +144,7 @@ var DOMPropertyOperations = { mutationMethod(node, value); } else if (shouldIgnoreValue(propertyInfo, value)) { this.deleteValueForProperty(node, name); + return; } else if (propertyInfo.mustUseProperty) { var propName = propertyInfo.propertyName; // Must explicitly cast values for HAS_SIDE_EFFECTS-properties to the @@ -171,6 +171,18 @@ var DOMPropertyOperations = { } } else if (DOMProperty.isCustomAttribute(name)) { DOMPropertyOperations.setValueForAttribute(node, name, value); + return; + } + + if (__DEV__) { + ReactDOMInstrumentation.debugTool.onSetValueForProperty(node, name, value); + var payload = {}; + payload[name] = value; + ReactInstrumentation.debugTool.onNativeOperation( + ReactDOMComponentTree.getInstanceFromNode(node)._debugID, + 'update attribute', + payload + ); } }, @@ -183,6 +195,16 @@ var DOMPropertyOperations = { } else { node.setAttribute(name, '' + value); } + + if (__DEV__) { + var payload = {}; + payload[name] = value; + ReactInstrumentation.debugTool.onNativeOperation( + ReactDOMComponentTree.getInstanceFromNode(node)._debugID, + 'update attribute', + payload + ); + } }, /** @@ -192,9 +214,6 @@ var DOMPropertyOperations = { * @param {string} name */ deleteValueForProperty: function(node, name) { - if (__DEV__) { - ReactDOMInstrumentation.debugTool.onDeleteValueForProperty(node, name); - } var propertyInfo = DOMProperty.properties.hasOwnProperty(name) ? DOMProperty.properties[name] : null; if (propertyInfo) { @@ -218,6 +237,15 @@ var DOMPropertyOperations = { } else if (DOMProperty.isCustomAttribute(name)) { node.removeAttribute(name); } + + if (__DEV__) { + ReactDOMInstrumentation.debugTool.onDeleteValueForProperty(node, name); + ReactInstrumentation.debugTool.onNativeOperation( + ReactDOMComponentTree.getInstanceFromNode(node)._debugID, + 'remove attribute', + name + ); + } }, }; diff --git a/src/renderers/dom/shared/__tests__/DOMPropertyOperations-test.js b/src/renderers/dom/shared/__tests__/DOMPropertyOperations-test.js index 14b72282ae2..08bb3e455e6 100644 --- a/src/renderers/dom/shared/__tests__/DOMPropertyOperations-test.js +++ b/src/renderers/dom/shared/__tests__/DOMPropertyOperations-test.js @@ -14,6 +14,7 @@ describe('DOMPropertyOperations', function() { var DOMPropertyOperations; var DOMProperty; + var ReactDOMComponentTree; beforeEach(function() { jest.resetModuleRegistry(); @@ -22,6 +23,7 @@ describe('DOMPropertyOperations', function() { DOMPropertyOperations = require('DOMPropertyOperations'); DOMProperty = require('DOMProperty'); + ReactDOMComponentTree = require('ReactDOMComponentTree'); }); describe('createMarkupForProperty', function() { @@ -175,6 +177,7 @@ describe('DOMPropertyOperations', function() { beforeEach(function() { stubNode = document.createElement('div'); + ReactDOMComponentTree.precacheNode({}, stubNode); }); it('should set values as properties by default', function() { @@ -223,6 +226,8 @@ describe('DOMPropertyOperations', function() { it('should not remove empty attributes for special properties', function() { stubNode = document.createElement('input'); + ReactDOMComponentTree.precacheNode({}, stubNode); + DOMPropertyOperations.setValueForProperty(stubNode, 'value', ''); // JSDOM does not behave correctly for attributes/properties //expect(stubNode.getAttribute('value')).toBe(''); @@ -346,6 +351,7 @@ describe('DOMPropertyOperations', function() { beforeEach(function() { stubNode = document.createElement('div'); + ReactDOMComponentTree.precacheNode({}, stubNode); }); it('should remove attributes for normal properties', function() { @@ -361,6 +367,8 @@ describe('DOMPropertyOperations', function() { it('should not remove attributes for special properties', function() { stubNode = document.createElement('input'); + ReactDOMComponentTree.precacheNode({}, stubNode); + stubNode.setAttribute('value', 'foo'); DOMPropertyOperations.deleteValueForProperty(stubNode, 'value'); @@ -371,6 +379,8 @@ describe('DOMPropertyOperations', function() { it('should not leave all options selected when deleting multiple', function() { stubNode = document.createElement('select'); + ReactDOMComponentTree.precacheNode({}, stubNode); + stubNode.multiple = true; stubNode.appendChild(document.createElement('option')); stubNode.appendChild(document.createElement('option')); diff --git a/src/renderers/shared/reconciler/ReactCompositeComponent.js b/src/renderers/shared/reconciler/ReactCompositeComponent.js index 0f8574545a0..f27cbdbe801 100644 --- a/src/renderers/shared/reconciler/ReactCompositeComponent.js +++ b/src/renderers/shared/reconciler/ReactCompositeComponent.js @@ -888,7 +888,11 @@ var ReactCompositeComponentMixin = { } } - this._replaceNodeWithMarkup(oldNativeNode, nextMarkup); + this._replaceNodeWithMarkup( + oldNativeNode, + nextMarkup, + prevComponentInstance + ); } }, @@ -897,10 +901,11 @@ var ReactCompositeComponentMixin = { * * @protected */ - _replaceNodeWithMarkup: function(oldNativeNode, nextMarkup) { + _replaceNodeWithMarkup: function(oldNativeNode, nextMarkup, prevInstance) { ReactComponentEnvironment.replaceNodeWithMarkup( oldNativeNode, - nextMarkup + nextMarkup, + prevInstance ); },