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(
, node);
+ var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
+
+ assertHistoryMatches([{
+ instanceID: inst._debugID,
+ type: 'mount',
+ payload: ReactDOMFeatureFlags.useCreateElement ?
+ 'DIV' :
+ '',
+ }]);
+ });
+
+ it('gets recorded for composite roots', () => {
+ function Foo() {
+ return ;
+ }
+ var node = document.createElement('div');
+ ReactDOM.render(, node);
+ var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
+ assertHistoryMatches([{
+ instanceID: inst._debugID,
+ type: 'mount',
+ payload: ReactDOMFeatureFlags.useCreateElement ?
+ 'DIV' :
+ '',
+ }]);
+ });
+
+ 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
);
},