Add ReactNativeOperationHistoryDevtool to track native operations

This commit is contained in:
Dan Abramov
2016-04-26 01:28:44 +01:00
parent f5b20bf79c
commit aaadb31827
11 changed files with 833 additions and 12 deletions
+3
View File
@@ -55,6 +55,9 @@ var ReactDebugTool = {
onEndProcessingChildContext() {
emitEvent('onEndProcessingChildContext');
},
onNativeOperation(debugID, type, payload) {
emitEvent('onNativeOperation', debugID, type, payload);
},
onSetState() {
emitEvent('onSetState');
},
@@ -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;
@@ -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(<div><p>Hi.</p></div>, node);
var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
assertHistoryMatches([{
instanceID: inst._debugID,
type: 'mount',
payload: ReactDOMFeatureFlags.useCreateElement ?
'DIV' :
'<div data-reactroot="" data-reactid="1"><p data-reactid="2">Hi.</p></div>',
}]);
});
it('gets recorded for composite roots', () => {
function Foo() {
return <div><p>Hi.</p></div>;
}
var node = document.createElement('div');
ReactDOM.render(<Foo />, node);
var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
assertHistoryMatches([{
instanceID: inst._debugID,
type: 'mount',
payload: ReactDOMFeatureFlags.useCreateElement ?
'DIV' :
'<div data-reactroot="" data-reactid="1">' +
'<p data-reactid="2">Hi.</p></div>',
}]);
});
it('gets recorded for composite roots that return null', () => {
function Foo() {
return null;
}
var node = document.createElement('div');
ReactDOM.render(<Foo />, node);
var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
assertHistoryMatches([{
instanceID: inst._debugID,
type: 'mount',
payload: ReactDOMFeatureFlags.useCreateElement ?
'#comment' :
'<!-- react-empty: 1 -->',
}]);
});
});
describe('update styles', () => {
it('gets recorded during mount', () => {
var node = document.createElement('div');
ReactDOM.render(<div style={{
color: 'red',
backgroundColor: 'yellow',
}} />, 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: '<div style="color:red;background-color:yellow;" ' +
'data-reactroot="" data-reactid="1"></div>',
}]);
}
});
it('gets recorded during an update', () => {
var node = document.createElement('div');
ReactDOM.render(<div />, node);
var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
ReactNativeOperationHistoryDevtool.clearHistory();
ReactDOM.render(<div style={{ color: 'red' }} />, node);
ReactDOM.render(<div style={{
color: 'blue',
backgroundColor: 'yellow',
}} />, node);
ReactDOM.render(<div style={{ backgroundColor: 'green' }} />, node);
ReactDOM.render(<div />, 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(<div />, node);
var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
ReactNativeOperationHistoryDevtool.clearHistory();
ReactDOM.render(<div style={{
color: 'red',
backgroundColor: 'yellow',
}} />, node);
ReactDOM.render(<div style={{
color: 'red',
backgroundColor: 'yellow',
}} />, 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(<div className="rad" tabIndex={42} />, 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: '<div class="rad" tabindex="42" data-reactroot="" ' +
'data-reactid="1"></div>',
}]);
}
});
it('gets recorded during an update', () => {
var node = document.createElement('div');
ReactDOM.render(<div />, node);
var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
ReactNativeOperationHistoryDevtool.clearHistory();
ReactDOM.render(<div className="rad" />, node);
ReactDOM.render(<div className="mad" tabIndex={42} />, node);
ReactDOM.render(<div tabIndex={43} />, 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(<div />, node);
var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
ReactNativeOperationHistoryDevtool.clearHistory();
ReactDOM.render(<div disabled={true} />, node);
ReactDOM.render(<div disabled={false} />, 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(<div data-x="rad" data-y={42} />, 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: '<div data-x="rad" data-y="42" data-reactroot="" ' +
'data-reactid="1"></div>',
}]);
}
});
it('gets recorded during an update', () => {
var node = document.createElement('div');
ReactDOM.render(<div />, node);
var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
ReactNativeOperationHistoryDevtool.clearHistory();
ReactDOM.render(<div data-x="rad" />, node);
ReactDOM.render(<div data-x="mad" data-y={42} />, node);
ReactDOM.render(<div data-y={43} />, 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(<my-component className="rad" tabIndex={42} />, 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: '<my-component className="rad" tabIndex="42" ' +
'data-reactroot="" data-reactid="1"></my-component>',
}]);
}
});
it('gets recorded during an update', () => {
var node = document.createElement('div');
ReactDOM.render(<my-component />, node);
var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
ReactNativeOperationHistoryDevtool.clearHistory();
ReactDOM.render(<my-component className="rad" />, node);
ReactDOM.render(<my-component className="mad" tabIndex={42} />, node);
ReactDOM.render(<my-component tabIndex={43} />, 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(<div>Hi.</div>, node);
var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
ReactNativeOperationHistoryDevtool.clearHistory();
ReactDOM.render(<div>Bye.</div>, 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(<div dangerouslySetInnerHTML={{__html: 'Hi.'}} />, node);
var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
ReactNativeOperationHistoryDevtool.clearHistory();
ReactDOM.render(<div>Bye.</div>, 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(<div><span /><p /></div>, node);
var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
ReactNativeOperationHistoryDevtool.clearHistory();
ReactDOM.render(<div>Bye.</div>, 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(<div>Hi.</div>, node);
ReactNativeOperationHistoryDevtool.clearHistory();
ReactDOM.render(<div>Hi.</div>, node);
assertHistoryMatches([]);
});
});
describe('text node', () => {
it('gets recorded during an update', () => {
var node = document.createElement('div');
ReactDOM.render(<div>{'Hi.'}{42}</div>, node);
var inst1 = ReactDOMComponentTree.getInstanceFromNode(node.firstChild.childNodes[0]);
var inst2 = ReactDOMComponentTree.getInstanceFromNode(node.firstChild.childNodes[3]);
ReactNativeOperationHistoryDevtool.clearHistory();
ReactDOM.render(<div>{'Bye.'}{43}</div>, 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(<div>{'Hi.'}{42}</div>, node);
ReactNativeOperationHistoryDevtool.clearHistory();
ReactDOM.render(<div>{'Hi.'}{42}</div>, 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 = <div />;
ReactDOM.render(<Foo />, node);
var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
ReactNativeOperationHistoryDevtool.clearHistory();
element = <span />;
ReactDOM.render(<Foo />, 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 = <div />;
ReactDOM.render(<Foo />, node);
ReactNativeOperationHistoryDevtool.clearHistory();
element = <div />;
ReactDOM.render(<Foo />, node);
assertHistoryMatches([]);
});
});
describe('replace children', () => {
it('gets recorded during an update from text content', () => {
var node = document.createElement('div');
ReactDOM.render(<div>Hi.</div>, node);
var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
ReactNativeOperationHistoryDevtool.clearHistory();
ReactDOM.render(
<div dangerouslySetInnerHTML={{__html: 'Bye.'}} />,
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(
<div dangerouslySetInnerHTML={{__html: 'Hi.'}} />,
node
);
var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
ReactNativeOperationHistoryDevtool.clearHistory();
ReactDOM.render(
<div dangerouslySetInnerHTML={{__html: 'Bye.'}} />,
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(<div><span /><p /></div>, node);
var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
ReactNativeOperationHistoryDevtool.clearHistory();
ReactDOM.render(
<div dangerouslySetInnerHTML={{__html: 'Hi.'}} />,
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(
<div dangerouslySetInnerHTML={{__html: 'Hi.'}} />,
node
);
ReactNativeOperationHistoryDevtool.clearHistory();
ReactDOM.render(
<div dangerouslySetInnerHTML={{__html: 'Hi.'}} />,
node
);
assertHistoryMatches([]);
});
});
describe('insert child', () => {
it('gets reported when a child is inserted', () => {
var node = document.createElement('div');
ReactDOM.render(<div><span /></div>, node);
var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
ReactNativeOperationHistoryDevtool.clearHistory();
ReactDOM.render(<div><span /><p /></div>, 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(<div><span key="a" /><p key="b" /></div>, node);
var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
ReactNativeOperationHistoryDevtool.clearHistory();
ReactDOM.render(<div><p key="b" /><span key="a" /></div>, 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(<div><span key="a" /><p key="b" /></div>, node);
var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild);
ReactNativeOperationHistoryDevtool.clearHistory();
ReactDOM.render(<div><span key="a" /></div>, node);
assertHistoryMatches([{
instanceID: inst._debugID,
type: 'remove child',
payload: {fromIndex: 1},
}]);
});
});
});
+8
View File
@@ -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()
);
}
},
};
@@ -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 <span> \n testContent \t </span> \n \t';
var stubInstance = {};
ReactDOMComponentTree.precacheNode(stubInstance, stubNode);
var html = '\n \t <span> \n testContent \t </span> \n \t';
ReactDOMIDOperations.dangerouslyProcessChildrenUpdates(
{_nativeNode: stubNode},
stubInstance,
[{
type: ReactMultiChildUpdateTypes.SET_MARKUP,
content: html,
@@ -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;
}
}
@@ -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,
};
}
@@ -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)) {
@@ -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
);
}
},
};
@@ -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'));
@@ -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
);
},