Merge pull request #1510 from syranide/propattr

Use removeAttribute to forcefully remove properties from the DOM
This commit is contained in:
Jim
2016-01-29 12:48:25 -08:00
6 changed files with 278 additions and 191 deletions
+6 -38
View File
@@ -22,13 +22,12 @@ var DOMPropertyInjection = {
* Mapping from normalized, camelcased property names to a configuration that
* specifies how the associated DOM property should be accessed or rendered.
*/
MUST_USE_ATTRIBUTE: 0x1,
MUST_USE_PROPERTY: 0x2,
HAS_SIDE_EFFECTS: 0x4,
HAS_BOOLEAN_VALUE: 0x8,
HAS_NUMERIC_VALUE: 0x10,
HAS_POSITIVE_NUMERIC_VALUE: 0x20 | 0x10,
HAS_OVERLOADED_BOOLEAN_VALUE: 0x40,
MUST_USE_PROPERTY: 0x1,
HAS_SIDE_EFFECTS: 0x2,
HAS_BOOLEAN_VALUE: 0x4,
HAS_NUMERIC_VALUE: 0x8,
HAS_POSITIVE_NUMERIC_VALUE: 0x10 | 0x8,
HAS_OVERLOADED_BOOLEAN_VALUE: 0x20,
/**
* Inject some specialized knowledge about the DOM. This takes a config object
@@ -91,7 +90,6 @@ var DOMPropertyInjection = {
propertyName: propName,
mutationMethod: null,
mustUseAttribute: checkMask(propConfig, Injection.MUST_USE_ATTRIBUTE),
mustUseProperty: checkMask(propConfig, Injection.MUST_USE_PROPERTY),
hasSideEffects: checkMask(propConfig, Injection.HAS_SIDE_EFFECTS),
hasBooleanValue: checkMask(propConfig, Injection.HAS_BOOLEAN_VALUE),
@@ -102,11 +100,6 @@ var DOMPropertyInjection = {
checkMask(propConfig, Injection.HAS_OVERLOADED_BOOLEAN_VALUE),
};
invariant(
!propertyInfo.mustUseAttribute || !propertyInfo.mustUseProperty,
'DOMProperty: Cannot require using both attribute and property: %s',
propName
);
invariant(
propertyInfo.mustUseProperty || !propertyInfo.hasSideEffects,
'DOMProperty: Properties that have side effects must use property: %s',
@@ -148,7 +141,6 @@ var DOMPropertyInjection = {
}
},
};
var defaultValueCache = {};
var ATTRIBUTE_NAME_START_CHAR = ':A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD';
@@ -186,9 +178,6 @@ var DOMProperty = {
* mutationMethod:
* If non-null, used instead of the property or `setAttribute()` after
* initial render.
* mustUseAttribute:
* Whether the property must be accessed and mutated using `*Attribute()`.
* (This includes anything that fails `<propName> in <element>`.)
* mustUseProperty:
* Whether the property must be accessed and mutated as an object property.
* hasSideEffects:
@@ -237,27 +226,6 @@ var DOMProperty = {
return false;
},
/**
* Returns the default property value for a DOM property (i.e., not an
* attribute). Most default values are '' or false, but not all. Worse yet,
* some (in particular, `type`) vary depending on the type of element.
*
* TODO: Is it better to grab all the possible properties when creating an
* element to avoid having to create the same element twice?
*/
getDefaultValueForProperty: function(nodeName, prop) {
var nodeDefaults = defaultValueCache[nodeName];
var testElement;
if (!nodeDefaults) {
defaultValueCache[nodeName] = nodeDefaults = {};
}
if (!(prop in nodeDefaults)) {
testElement = document.createElement(nodeName);
nodeDefaults[prop] = testElement[prop];
}
return nodeDefaults[prop];
},
injection: DOMPropertyInjection,
};
@@ -168,7 +168,17 @@ var DOMPropertyOperations = {
mutationMethod(node, value);
} else if (shouldIgnoreValue(propertyInfo, value)) {
this.deleteValueForProperty(node, name);
} else if (propertyInfo.mustUseAttribute) {
} else if (propertyInfo.mustUseProperty) {
var propName = propertyInfo.propertyName;
// Must explicitly cast values for HAS_SIDE_EFFECTS-properties to the
// property type before comparing; only `value` does and is string.
if (!propertyInfo.hasSideEffects ||
('' + node[propName]) !== ('' + value)) {
// Contrary to `setAttribute`, object properties are properly
// `toString`ed by IE8/9.
node[propName] = value;
}
} else {
var attributeName = propertyInfo.attributeName;
var namespace = propertyInfo.attributeNamespace;
// `setAttribute` with objects becomes only `[object]` in IE8/9,
@@ -181,16 +191,6 @@ var DOMPropertyOperations = {
} else {
node.setAttribute(attributeName, '' + value);
}
} else {
var propName = propertyInfo.propertyName;
// Must explicitly cast values for HAS_SIDE_EFFECTS-properties to the
// property type before comparing; only `value` does and is string.
if (!propertyInfo.hasSideEffects ||
('' + node[propName]) !== ('' + value)) {
// Contrary to `setAttribute`, object properties are properly
// `toString`ed by IE8/9.
node[propName] = value;
}
}
} else if (DOMProperty.isCustomAttribute(name)) {
DOMPropertyOperations.setValueForAttribute(node, name, value);
@@ -236,18 +236,19 @@ var DOMPropertyOperations = {
var mutationMethod = propertyInfo.mutationMethod;
if (mutationMethod) {
mutationMethod(node, undefined);
} else if (propertyInfo.mustUseAttribute) {
node.removeAttribute(propertyInfo.attributeName);
} else {
} else if (propertyInfo.mustUseProperty) {
var propName = propertyInfo.propertyName;
var defaultValue = DOMProperty.getDefaultValueForProperty(
node.nodeName,
propName
);
if (!propertyInfo.hasSideEffects ||
('' + node[propName]) !== defaultValue) {
node[propName] = defaultValue;
if (propertyInfo.hasBooleanValue) {
// No HAS_SIDE_EFFECTS logic here, only `value` has it and is string.
node[propName] = false;
} else {
if (!propertyInfo.hasSideEffects ||
('' + node[propName]) !== '') {
node[propName] = '';
}
}
} else {
node.removeAttribute(propertyInfo.attributeName);
}
} else if (DOMProperty.isCustomAttribute(name)) {
node.removeAttribute(name);
+76 -103
View File
@@ -12,9 +12,7 @@
'use strict';
var DOMProperty = require('DOMProperty');
var ExecutionEnvironment = require('ExecutionEnvironment');
var MUST_USE_ATTRIBUTE = DOMProperty.injection.MUST_USE_ATTRIBUTE;
var MUST_USE_PROPERTY = DOMProperty.injection.MUST_USE_PROPERTY;
var HAS_BOOLEAN_VALUE = DOMProperty.injection.HAS_BOOLEAN_VALUE;
var HAS_SIDE_EFFECTS = DOMProperty.injection.HAS_SIDE_EFFECTS;
@@ -24,19 +22,6 @@ var HAS_POSITIVE_NUMERIC_VALUE =
var HAS_OVERLOADED_BOOLEAN_VALUE =
DOMProperty.injection.HAS_OVERLOADED_BOOLEAN_VALUE;
var hasSVG;
if (ExecutionEnvironment.canUseDOM) {
var implementation = document.implementation;
hasSVG = (
implementation &&
implementation.hasFeature &&
implementation.hasFeature(
'http://www.w3.org/TR/SVG11/feature#BasicStructure',
'1.1'
)
);
}
var HTMLDOMPropertyConfig = {
isCustomAttribute: RegExp.prototype.test.bind(
new RegExp('^(data|aria)-[' + DOMProperty.ATTRIBUTE_NAME_CHAR + ']*$')
@@ -49,86 +34,83 @@ var HTMLDOMPropertyConfig = {
acceptCharset: null,
accessKey: null,
action: null,
allowFullScreen: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
allowTransparency: MUST_USE_ATTRIBUTE,
allowFullScreen: HAS_BOOLEAN_VALUE,
allowTransparency: null,
alt: null,
async: HAS_BOOLEAN_VALUE,
autoComplete: null,
// autoFocus is polyfilled/normalized by AutoFocusUtils
// autoFocus: HAS_BOOLEAN_VALUE,
autoPlay: HAS_BOOLEAN_VALUE,
capture: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
cellPadding: MUST_USE_ATTRIBUTE,
cellSpacing: MUST_USE_ATTRIBUTE,
challenge: MUST_USE_ATTRIBUTE,
charSet: MUST_USE_ATTRIBUTE,
capture: HAS_BOOLEAN_VALUE,
cellPadding: null,
cellSpacing: null,
charSet: null,
challenge: null,
checked: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
classID: MUST_USE_ATTRIBUTE,
// To set className on SVG elements, it's necessary to use .setAttribute;
// this works on HTML elements too in all browsers except IE8. Conveniently,
// IE8 doesn't support SVG and so we can simply use the attribute in
// browsers that support SVG and the property in browsers that don't,
// regardless of whether the element is HTML or SVG.
className: hasSVG ? MUST_USE_ATTRIBUTE : MUST_USE_PROPERTY,
cols: MUST_USE_ATTRIBUTE | HAS_POSITIVE_NUMERIC_VALUE,
colSpan: MUST_USE_ATTRIBUTE,
classID: null,
className: null,
cols: HAS_POSITIVE_NUMERIC_VALUE,
colSpan: null,
content: null,
contentEditable: MUST_USE_ATTRIBUTE,
contextMenu: MUST_USE_ATTRIBUTE,
controls: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
contentEditable: null,
contextMenu: null,
controls: HAS_BOOLEAN_VALUE,
coords: null,
crossOrigin: null,
data: null, // For `<object />` acts as `src`.
dateTime: MUST_USE_ATTRIBUTE,
dateTime: null,
default: HAS_BOOLEAN_VALUE,
defer: HAS_BOOLEAN_VALUE,
dir: MUST_USE_ATTRIBUTE,
disabled: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
dir: null,
disabled: HAS_BOOLEAN_VALUE,
download: HAS_OVERLOADED_BOOLEAN_VALUE,
draggable: null,
encType: MUST_USE_ATTRIBUTE,
form: MUST_USE_ATTRIBUTE,
formAction: MUST_USE_ATTRIBUTE,
formEncType: MUST_USE_ATTRIBUTE,
formMethod: MUST_USE_ATTRIBUTE,
encType: null,
form: null,
formAction: null,
formEncType: null,
formMethod: null,
formNoValidate: HAS_BOOLEAN_VALUE,
formTarget: MUST_USE_ATTRIBUTE,
frameBorder: MUST_USE_ATTRIBUTE,
formTarget: null,
frameBorder: null,
headers: null,
height: MUST_USE_ATTRIBUTE,
hidden: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
height: null,
hidden: HAS_BOOLEAN_VALUE,
high: null,
href: null,
hrefLang: null,
htmlFor: null,
httpEquiv: null,
icon: null,
id: MUST_USE_PROPERTY,
inputMode: MUST_USE_ATTRIBUTE,
id: null,
inputMode: null,
integrity: null,
is: MUST_USE_ATTRIBUTE,
keyParams: MUST_USE_ATTRIBUTE,
keyType: MUST_USE_ATTRIBUTE,
is: null,
keyParams: null,
keyType: null,
kind: null,
label: null,
lang: null,
list: MUST_USE_ATTRIBUTE,
loop: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
list: null,
loop: HAS_BOOLEAN_VALUE,
low: null,
manifest: MUST_USE_ATTRIBUTE,
marginHeight: MUST_USE_ATTRIBUTE,
marginWidth: MUST_USE_ATTRIBUTE,
manifest: null,
marginHeight: null,
marginWidth: null,
max: null,
maxLength: MUST_USE_ATTRIBUTE,
media: MUST_USE_ATTRIBUTE,
maxLength: null,
media: null,
mediaGroup: null,
method: MUST_USE_ATTRIBUTE,
method: null,
min: null,
minLength: MUST_USE_ATTRIBUTE,
minLength: null,
// Caution; `option.selected` is not updated if `select.multiple` is
// disabled with `removeAttribute`.
multiple: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
muted: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
name: null,
nonce: MUST_USE_ATTRIBUTE,
nonce: null,
noValidate: HAS_BOOLEAN_VALUE,
open: HAS_BOOLEAN_VALUE,
optimum: null,
@@ -137,29 +119,29 @@ var HTMLDOMPropertyConfig = {
poster: null,
preload: null,
radioGroup: null,
readOnly: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
readOnly: HAS_BOOLEAN_VALUE,
rel: null,
required: HAS_BOOLEAN_VALUE,
reversed: HAS_BOOLEAN_VALUE,
role: MUST_USE_ATTRIBUTE,
rows: MUST_USE_ATTRIBUTE | HAS_POSITIVE_NUMERIC_VALUE,
rowSpan: MUST_USE_ATTRIBUTE | HAS_NUMERIC_VALUE,
role: null,
rows: HAS_POSITIVE_NUMERIC_VALUE,
rowSpan: HAS_NUMERIC_VALUE,
sandbox: null,
scope: null,
scoped: HAS_BOOLEAN_VALUE,
scrolling: MUST_USE_ATTRIBUTE,
seamless: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
scrolling: null,
seamless: HAS_BOOLEAN_VALUE,
selected: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
shape: null,
size: MUST_USE_ATTRIBUTE | HAS_POSITIVE_NUMERIC_VALUE,
sizes: MUST_USE_ATTRIBUTE,
span: MUST_USE_ATTRIBUTE | HAS_POSITIVE_NUMERIC_VALUE,
size: HAS_POSITIVE_NUMERIC_VALUE,
sizes: null,
span: HAS_POSITIVE_NUMERIC_VALUE,
spellCheck: null,
src: null,
srcDoc: MUST_USE_PROPERTY,
srcDoc: null,
srcLang: null,
srcSet: MUST_USE_ATTRIBUTE,
start: MUST_USE_ATTRIBUTE | HAS_NUMERIC_VALUE,
srcSet: null,
start: HAS_NUMERIC_VALUE,
step: null,
style: null,
summary: null,
@@ -167,55 +149,55 @@ var HTMLDOMPropertyConfig = {
target: null,
title: null,
// Setting .type throws on non-<input> tags
type: MUST_USE_ATTRIBUTE,
type: null,
useMap: null,
value: MUST_USE_PROPERTY | HAS_SIDE_EFFECTS,
width: MUST_USE_ATTRIBUTE,
wmode: MUST_USE_ATTRIBUTE,
wrap: MUST_USE_ATTRIBUTE,
width: null,
wmode: null,
wrap: null,
/**
* RDFa Properties
*/
about: MUST_USE_ATTRIBUTE,
datatype: MUST_USE_ATTRIBUTE,
inlist: MUST_USE_ATTRIBUTE,
prefix: MUST_USE_ATTRIBUTE,
about: null,
datatype: null,
inlist: null,
prefix: null,
// property is also supported for OpenGraph in meta tags.
property: MUST_USE_ATTRIBUTE,
resource: MUST_USE_ATTRIBUTE,
typeof: MUST_USE_ATTRIBUTE,
vocab: MUST_USE_ATTRIBUTE,
property: null,
resource: null,
typeof: null,
vocab: null,
/**
* Non-standard Properties
*/
// autoCapitalize and autoCorrect are supported in Mobile Safari for
// keyboard hints.
autoCapitalize: MUST_USE_ATTRIBUTE,
autoCorrect: MUST_USE_ATTRIBUTE,
autoCapitalize: null,
autoCorrect: null,
// autoSave allows WebKit/Blink to persist values of input fields on page reloads
autoSave: null,
// color is for Safari mask-icon link
color: null,
// itemProp, itemScope, itemType are for
// Microdata support. See http://schema.org/docs/gs.html
itemProp: MUST_USE_ATTRIBUTE,
itemScope: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
itemType: MUST_USE_ATTRIBUTE,
itemProp: null,
itemScope: HAS_BOOLEAN_VALUE,
itemType: null,
// itemID and itemRef are for Microdata support as well but
// only specified in the WHATWG spec document. See
// https://html.spec.whatwg.org/multipage/microdata.html#microdata-dom-api
itemID: MUST_USE_ATTRIBUTE,
itemRef: MUST_USE_ATTRIBUTE,
itemID: null,
itemRef: null,
// results show looking glass icon and recent searches on input
// search fields in WebKit/Blink
results: null,
// IE-only attribute that specifies security restrictions on an iframe
// as an alternative to the sandbox attribute on IE<10
security: MUST_USE_ATTRIBUTE,
security: null,
// IE-only attribute that controls focus behavior
unselectable: MUST_USE_ATTRIBUTE,
unselectable: null,
},
DOMAttributeNames: {
acceptCharset: 'accept-charset',
@@ -224,15 +206,6 @@ var HTMLDOMPropertyConfig = {
httpEquiv: 'http-equiv',
},
DOMPropertyNames: {
autoComplete: 'autocomplete',
autoFocus: 'autofocus',
autoPlay: 'autoplay',
autoSave: 'autosave',
hrefLang: 'hreflang',
radioGroup: 'radiogroup',
spellCheck: 'spellcheck',
srcDoc: 'srcdoc',
srcSet: 'srcset',
},
};
@@ -11,10 +11,6 @@
'use strict';
var DOMProperty = require('DOMProperty');
var MUST_USE_ATTRIBUTE = DOMProperty.injection.MUST_USE_ATTRIBUTE;
var NS = {
xlink: 'http://www.w3.org/1999/xlink',
xml: 'http://www.w3.org/XML/1998/namespace',
@@ -22,30 +18,30 @@ var NS = {
var SVGDOMPropertyConfig = {
Properties: {
clipPath: MUST_USE_ATTRIBUTE,
fillOpacity: MUST_USE_ATTRIBUTE,
fontFamily: MUST_USE_ATTRIBUTE,
fontSize: MUST_USE_ATTRIBUTE,
markerEnd: MUST_USE_ATTRIBUTE,
markerMid: MUST_USE_ATTRIBUTE,
markerStart: MUST_USE_ATTRIBUTE,
stopColor: MUST_USE_ATTRIBUTE,
stopOpacity: MUST_USE_ATTRIBUTE,
strokeDasharray: MUST_USE_ATTRIBUTE,
strokeLinecap: MUST_USE_ATTRIBUTE,
strokeOpacity: MUST_USE_ATTRIBUTE,
strokeWidth: MUST_USE_ATTRIBUTE,
textAnchor: MUST_USE_ATTRIBUTE,
xlinkActuate: MUST_USE_ATTRIBUTE,
xlinkArcrole: MUST_USE_ATTRIBUTE,
xlinkHref: MUST_USE_ATTRIBUTE,
xlinkRole: MUST_USE_ATTRIBUTE,
xlinkShow: MUST_USE_ATTRIBUTE,
xlinkTitle: MUST_USE_ATTRIBUTE,
xlinkType: MUST_USE_ATTRIBUTE,
xmlBase: MUST_USE_ATTRIBUTE,
xmlLang: MUST_USE_ATTRIBUTE,
xmlSpace: MUST_USE_ATTRIBUTE,
clipPath: null,
fillOpacity: null,
fontFamily: null,
fontSize: null,
markerEnd: null,
markerMid: null,
markerStart: null,
stopColor: null,
stopOpacity: null,
strokeDasharray: null,
strokeLinecap: null,
strokeOpacity: null,
strokeWidth: null,
textAnchor: null,
xlinkActuate: null,
xlinkArcrole: null,
xlinkHref: null,
xlinkRole: null,
xlinkShow: null,
xlinkTitle: null,
xlinkType: null,
xmlBase: null,
xmlLang: null,
xmlSpace: null,
},
DOMAttributeNamespaces: {
xlinkActuate: NS.xlink,
@@ -282,6 +282,23 @@ describe('DOMPropertyOperations', function() {
// className should be '', not 'null' or null (which becomes 'null' in
// some browsers)
expect(stubNode.className).toBe('');
expect(stubNode.getAttribute('class')).toBe(null);
});
it('should remove property properly for boolean properties', function() {
DOMPropertyOperations.setValueForProperty(
stubNode,
'hidden',
true
);
expect(stubNode.hasAttribute('hidden')).toBe(true);
DOMPropertyOperations.setValueForProperty(
stubNode,
'hidden',
false
);
expect(stubNode.hasAttribute('hidden')).toBe(false);
});
it('should remove property properly even with different name', function() {
@@ -292,6 +309,9 @@ describe('DOMPropertyOperations', function() {
DOMPropertyNames: {
foobar: 'className',
},
DOMAttributeNames: {
foobar: 'class',
},
});
DOMPropertyOperations.setValueForProperty(
@@ -313,6 +333,53 @@ describe('DOMPropertyOperations', function() {
});
describe('deleteValueForProperty', function() {
var stubNode;
beforeEach(function() {
stubNode = document.createElement('div');
});
it('should remove attributes for normal properties', function() {
DOMPropertyOperations.setValueForProperty(stubNode, 'title', 'foo');
expect(stubNode.getAttribute('title')).toBe('foo');
expect(stubNode.title).toBe('foo');
DOMPropertyOperations.deleteValueForProperty(stubNode, 'title');
expect(stubNode.getAttribute('title')).toBe(null);
// JSDOM does not behave correctly for attributes/properties
//expect(stubNode.title).toBe('');
});
it('should not remove attributes for special properties', function() {
stubNode = document.createElement('input');
stubNode.setAttribute('value', 'foo');
DOMPropertyOperations.deleteValueForProperty(stubNode, 'value');
// JSDOM does not behave correctly for attributes/properties
//expect(stubNode.getAttribute('value')).toBe('foo');
expect(stubNode.value).toBe('');
});
it('should not leave all options selected when deleting multiple', function() {
stubNode = document.createElement('select');
stubNode.multiple = true;
stubNode.appendChild(document.createElement('option'));
stubNode.appendChild(document.createElement('option'));
stubNode.options[0].selected = true;
stubNode.options[1].selected = true;
DOMPropertyOperations.deleteValueForProperty(stubNode, 'multiple');
expect(stubNode.getAttribute('multiple')).toBe(null);
expect(stubNode.multiple).toBe(false);
expect(
stubNode.options[0].selected &&
stubNode.options[1].selected
).toBe(false);
});
});
describe('injectDOMPropertyConfig', function() {
it('should support custom attributes', function() {
// foobar does not exist yet
@@ -576,7 +576,45 @@ describe('ReactDOMComponent', function() {
expect(container.textContent).toEqual('bonjour');
});
it('should not incur unnecessary DOM mutations', function() {
it('should not incur unnecessary DOM mutations for attributes', function() {
var container = document.createElement('div');
ReactDOM.render(<div id="" />, container);
var node = container.firstChild;
var nodeSetAttribute = node.setAttribute;
node.setAttribute = jest.genMockFn();
node.setAttribute.mockImpl(nodeSetAttribute);
var nodeRemoveAttribute = node.removeAttribute;
node.removeAttribute = jest.genMockFn();
node.removeAttribute.mockImpl(nodeRemoveAttribute);
ReactDOM.render(<div id="" />, container);
expect(node.setAttribute.mock.calls.length).toBe(0);
expect(node.removeAttribute.mock.calls.length).toBe(0);
ReactDOM.render(<div id="foo" />, container);
expect(node.setAttribute.mock.calls.length).toBe(1);
expect(node.removeAttribute.mock.calls.length).toBe(0);
ReactDOM.render(<div id="foo" />, container);
expect(node.setAttribute.mock.calls.length).toBe(1);
expect(node.removeAttribute.mock.calls.length).toBe(0);
ReactDOM.render(<div />, container);
expect(node.setAttribute.mock.calls.length).toBe(1);
expect(node.removeAttribute.mock.calls.length).toBe(1);
ReactDOM.render(<div id="" />, container);
expect(node.setAttribute.mock.calls.length).toBe(2);
expect(node.removeAttribute.mock.calls.length).toBe(1);
ReactDOM.render(<div />, container);
expect(node.setAttribute.mock.calls.length).toBe(2);
expect(node.removeAttribute.mock.calls.length).toBe(2);
});
it('should not incur unnecessary DOM mutations for string properties', function() {
var container = document.createElement('div');
ReactDOM.render(<div value="" />, container);
@@ -595,8 +633,52 @@ describe('ReactDOMComponent', function() {
ReactDOM.render(<div value="" />, container);
expect(nodeValueSetter.mock.calls.length).toBe(0);
ReactDOM.render(<div value="foo" />, container);
expect(nodeValueSetter.mock.calls.length).toBe(1);
ReactDOM.render(<div value="foo" />, container);
expect(nodeValueSetter.mock.calls.length).toBe(1);
ReactDOM.render(<div />, container);
expect(nodeValueSetter.mock.calls.length).toBe(2);
ReactDOM.render(<div value={null} />, container);
expect(nodeValueSetter.mock.calls.length).toBe(2);
ReactDOM.render(<div value="" />, container);
expect(nodeValueSetter.mock.calls.length).toBe(2);
ReactDOM.render(<div />, container);
expect(nodeValueSetter.mock.calls.length).toBe(2);
});
it('should not incur unnecessary DOM mutations for boolean properties', function() {
var container = document.createElement('div');
ReactDOM.render(<div checked={true} />, container);
var node = container.firstChild;
var nodeValue = true;
var nodeValueSetter = jest.genMockFn();
Object.defineProperty(node, 'checked', {
get: function() {
return nodeValue;
},
set: nodeValueSetter.mockImplementation(function(newValue) {
nodeValue = newValue;
}),
});
ReactDOM.render(<div checked={true} />, container);
expect(nodeValueSetter.mock.calls.length).toBe(0);
ReactDOM.render(<div />, container);
expect(nodeValueSetter.mock.calls.length).toBe(1);
ReactDOM.render(<div checked={false} />, container);
expect(nodeValueSetter.mock.calls.length).toBe(2); // should be 1
ReactDOM.render(<div checked={true} />, container);
expect(nodeValueSetter.mock.calls.length).toBe(3);
});
it('should ignore attribute whitelist for elements with the "is: attribute', function() {