Files
react/src/browser/ui/__tests__/ReactDOMComponent-test.js
T
Robert Knight ee86942d77 Fix incorrect update of style when props.style transitions from null to non-null
ReactDOMComponent maintains a copy of the previous style
object to support in-place mutations of props.style.

This cached object was not cleared when the style
property was removed in a props update.

Fixes #3409
2015-03-13 22:16:19 +00:00

657 lines
22 KiB
JavaScript

/**
* Copyright 2013-2015, 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
*/
/*jslint evil: true */
'use strict';
var assign = require('Object.assign');
var mocks = require('mocks');
describe('ReactDOMComponent', function() {
describe('updateDOM', function() {
var React;
var ReactTestUtils;
beforeEach(function() {
React = require('React');
ReactTestUtils = require('ReactTestUtils');
});
it("should handle className", function() {
var container = document.createElement('div');
React.render(<div style={{}} />, container);
React.render(<div className={'foo'} />, container);
expect(container.firstChild.className).toEqual('foo');
React.render(<div className={'bar'} />, container);
expect(container.firstChild.className).toEqual('bar');
React.render(<div className={null} />, container);
expect(container.firstChild.className).toEqual('');
});
it("should gracefully handle various style value types", function() {
var container = document.createElement('div');
React.render(<div style={{}} />, container);
var stubStyle = container.firstChild.style;
// set initial style
var setup = {display: 'block', left: '1', top: 2, fontFamily: 'Arial'};
React.render(<div style={setup} />, container);
expect(stubStyle.display).toEqual('block');
expect(stubStyle.left).toEqual('1px');
expect(stubStyle.fontFamily).toEqual('Arial');
// reset the style to their default state
var reset = {display: '', left: null, top: false, fontFamily: true};
React.render(<div style={reset} />, container);
expect(stubStyle.display).toEqual('');
expect(stubStyle.left).toEqual('');
expect(stubStyle.top).toEqual('');
expect(stubStyle.fontFamily).toEqual('');
});
it("should update styles when mutating style object", function() {
var styles = {display: 'none', fontFamily: 'Arial', lineHeight: 1.2};
var container = document.createElement('div');
React.render(<div style={styles} />, container);
var stubStyle = container.firstChild.style;
stubStyle.display = styles.display;
stubStyle.fontFamily = styles.fontFamily;
styles.display = 'block';
React.render(<div style={styles} />, container);
expect(stubStyle.display).toEqual('block');
expect(stubStyle.fontFamily).toEqual('Arial');
expect(stubStyle.lineHeight).toEqual('1.2');
styles.fontFamily = 'Helvetica';
React.render(<div style={styles} />, container);
expect(stubStyle.display).toEqual('block');
expect(stubStyle.fontFamily).toEqual('Helvetica');
expect(stubStyle.lineHeight).toEqual('1.2');
styles.lineHeight = 0.5;
React.render(<div style={styles} />, container);
expect(stubStyle.display).toEqual('block');
expect(stubStyle.fontFamily).toEqual('Helvetica');
expect(stubStyle.lineHeight).toEqual('0.5');
React.render(<div style={undefined} />, container);
expect(stubStyle.display).toBe('');
expect(stubStyle.fontFamily).toBe('');
expect(stubStyle.lineHeight).toBe('');
});
it("should update styles if initially null", function() {
var styles = null;
var container = document.createElement('div');
React.render(<div style={styles} />, container);
var stubStyle = container.firstChild.style;
styles = {display: 'block'};
React.render(<div style={styles} />, container);
expect(stubStyle.display).toEqual('block');
});
it("should remove attributes", function() {
var container = document.createElement('div');
React.render(<img height='17' />, container);
expect(container.firstChild.hasAttribute('height')).toBe(true);
React.render(<img />, container);
expect(container.firstChild.hasAttribute('height')).toBe(false);
});
it("should remove properties", function() {
var container = document.createElement('div');
React.render(<div className='monkey' />, container);
expect(container.firstChild.className).toEqual('monkey');
React.render(<div />, container);
expect(container.firstChild.className).toEqual('');
});
it("should clear a single style prop when changing 'style'", function() {
var styles = {display: 'none', color: 'red'};
var container = document.createElement('div');
React.render(<div style={styles} />, container);
var stubStyle = container.firstChild.style;
styles = {color: 'green'};
React.render(<div style={styles} />, container);
expect(stubStyle.display).toEqual('');
expect(stubStyle.color).toEqual('green');
});
it("should clear all the styles when removing 'style'", function() {
var styles = {display: 'none', color: 'red'};
var container = document.createElement('div');
React.render(<div style={styles} />, container);
var stubStyle = container.firstChild.style;
React.render(<div />, container);
expect(stubStyle.display).toEqual('');
expect(stubStyle.color).toEqual('');
});
it("should update styles when 'style' changes from null to object", function() {
var container = document.createElement('div');
var styles = {color: 'red'};
React.render(<div style={styles} />, container);
React.render(<div />, container);
React.render(<div style={styles} />, container);
var stubStyle = container.firstChild.style;
expect(stubStyle.color).toEqual('red');
});
it("should empty element when removing innerHTML", function() {
var container = document.createElement('div');
React.render(<div dangerouslySetInnerHTML={{__html: ':)'}} />, container);
expect(container.firstChild.innerHTML).toEqual(':)');
React.render(<div />, container);
expect(container.firstChild.innerHTML).toEqual('');
});
it("should transition from string content to innerHTML", function() {
var container = document.createElement('div');
React.render(<div>hello</div>, container);
expect(container.firstChild.innerHTML).toEqual('hello');
React.render(
<div dangerouslySetInnerHTML={{__html: 'goodbye'}} />,
container
);
expect(container.firstChild.innerHTML).toEqual('goodbye');
});
it("should transition from innerHTML to string content", function() {
var container = document.createElement('div');
React.render(
<div dangerouslySetInnerHTML={{__html: 'bonjour'}} />,
container
);
expect(container.firstChild.innerHTML).toEqual('bonjour');
React.render(<div>adieu</div>, container);
expect(container.firstChild.innerHTML).toEqual('adieu');
});
it("should not incur unnecessary DOM mutations", function() {
var container = document.createElement('div');
React.render(<div value="" />, container);
var node = container.firstChild;
var nodeValue = ''; // node.value always returns undefined
var nodeValueSetter = mocks.getMockFunction();
Object.defineProperty(node, 'value', {
get: function() {
return nodeValue;
},
set: nodeValueSetter.mockImplementation(function(newValue) {
nodeValue = newValue;
})
});
React.render(<div value="" />, container);
expect(nodeValueSetter.mock.calls.length).toBe(0);
React.render(<div />, container);
expect(nodeValueSetter.mock.calls.length).toBe(1);
});
});
describe('createOpenTagMarkup', function() {
var genMarkup;
function quoteRegexp(str) {
return (str + '').replace(/([.?*+\^$\[\]\\(){}|-])/g, "\\$1");
}
beforeEach(function() {
require('mock-modules').dumpCache();
var ReactDefaultInjection = require('ReactDefaultInjection');
ReactDefaultInjection.inject();
var ReactDOMComponent = require('ReactDOMComponent');
var ReactReconcileTransaction = require('ReactReconcileTransaction');
var NodeStub = function(initialProps) {
this._currentElement = {props: initialProps};
this._rootNodeID = 'test';
};
assign(NodeStub.prototype, ReactDOMComponent.Mixin);
genMarkup = function(props) {
var transaction = new ReactReconcileTransaction();
return (new NodeStub(props))._createOpenTagMarkupAndPutListeners(
transaction
);
};
this.addMatchers({
toHaveAttribute: function(attr, value) {
var expected = '(?:^|\\s)' + attr + '=[\\\'"]';
if (typeof value != 'undefined') {
expected += quoteRegexp(value) + '[\\\'"]';
}
return this.actual.match(new RegExp(expected));
}
});
});
it("should generate the correct markup with className", function() {
expect(genMarkup({className: 'a'})).toHaveAttribute('class', 'a');
expect(genMarkup({className: 'a b'})).toHaveAttribute('class', 'a b');
expect(genMarkup({className: ''})).toHaveAttribute('class', '');
});
it("should escape style names and values", function() {
expect(genMarkup({
style: {'b&ckground': '<3'}
})).toHaveAttribute('style', 'b&amp;ckground:&lt;3;');
});
});
describe('createContentMarkup', function() {
var genMarkup;
function quoteRegexp(str) {
return (str + '').replace(/([.?*+\^$\[\]\\(){}|-])/g, "\\$1");
}
beforeEach(function() {
require('mock-modules').dumpCache();
var ReactDOMComponent = require('ReactDOMComponent');
var ReactReconcileTransaction = require('ReactReconcileTransaction');
var NodeStub = function(initialProps) {
this._currentElement = {props: initialProps};
this._rootNodeID = 'test';
};
assign(NodeStub.prototype, ReactDOMComponent.Mixin);
genMarkup = function(props) {
var transaction = new ReactReconcileTransaction();
return (new NodeStub(props))._createContentMarkup(transaction, {});
};
this.addMatchers({
toHaveInnerhtml: function(html) {
var expected = '^' + quoteRegexp(html) + '$';
return this.actual.match(new RegExp(expected));
}
});
});
it("should handle dangerouslySetInnerHTML", function() {
var innerHTML = {__html: 'testContent'};
expect(
genMarkup({dangerouslySetInnerHTML: innerHTML})
).toHaveInnerhtml('testContent');
});
});
describe('mountComponent', function() {
var React;
var mountComponent;
beforeEach(function() {
require('mock-modules').dumpCache();
React = require('React');
var ReactMultiChild = require('ReactMultiChild');
var ReactDOMComponent = require('ReactDOMComponent');
var ReactReconcileTransaction = require('ReactReconcileTransaction');
var StubNativeComponent = function(element) {
this._currentElement = element;
};
assign(StubNativeComponent.prototype, ReactDOMComponent.Mixin);
assign(StubNativeComponent.prototype, ReactMultiChild.Mixin);
mountComponent = function(props) {
var transaction = new ReactReconcileTransaction();
var stubComponent = new StubNativeComponent({
type: StubNativeComponent,
props: props,
_owner: null,
_context: null
});
return stubComponent.mountComponent('test', transaction, {});
};
});
it("should warn against children for void elements", function() {
spyOn(console, 'warn');
var container = document.createElement('div');
React.render(<input>children</input>, container);
expect(console.warn.argsForCall.length).toBe(1);
expect(console.warn.argsForCall[0][0]).toContain('void element');
});
it("should warn against dangerouslySetInnerHTML for void elements", function() {
spyOn(console, 'warn');
var container = document.createElement('div');
React.render(
<input dangerouslySetInnerHTML={{__html: 'content'}} />,
container
);
expect(console.warn.argsForCall.length).toBe(1);
expect(console.warn.argsForCall[0][0]).toContain('void element');
});
it("should treat menuitem as a void element but still create the closing tag", function() {
spyOn(console, 'warn');
var container = document.createElement('div');
React.render(<menuitem />, container);
expect(container.innerHTML).toContain('</menuitem>');
React.render(<menuitem>children</menuitem>, container);
expect(console.warn.argsForCall.length).toBe(1);
expect(console.warn.argsForCall[0][0]).toContain('void element');
});
it("should validate against multiple children props", function() {
expect(function() {
mountComponent({children: '', dangerouslySetInnerHTML: ''});
}).toThrow(
'Invariant Violation: Can only set one of `children` or ' +
'`props.dangerouslySetInnerHTML`.'
);
});
it('should validate against use of innerHTML', function() {
spyOn(console, 'warn');
mountComponent({innerHTML: '<span>Hi Jim!</span>'});
expect(console.warn.argsForCall.length).toBe(1);
expect(console.warn.argsForCall[0][0]).toContain(
'Directly setting property `innerHTML` is not permitted. '
);
});
it('should validate use of dangerouslySetInnerHTML', function() {
expect(function() {
mountComponent({dangerouslySetInnerHTML: '<span>Hi Jim!</span>'});
}).toThrow(
'Invariant Violation: ' +
'`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' +
'Please visit http://fb.me/react-invariant-dangerously-set-inner-html for more information.'
);
});
it('should validate use of dangerouslySetInnerHTML', function() {
expect(function() {
mountComponent({dangerouslySetInnerHTML: {foo: 'bar'} });
}).toThrow(
'Invariant Violation: ' +
'`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' +
'Please visit http://fb.me/react-invariant-dangerously-set-inner-html for more information.'
);
});
it("should warn about contentEditable and children", function() {
spyOn(console, 'warn');
mountComponent({contentEditable: true, children: ''});
expect(console.warn.argsForCall.length).toBe(1);
expect(console.warn.argsForCall[0][0]).toContain('contentEditable');
});
it("should validate against invalid styles", function() {
expect(function() {
mountComponent({style: 'display: none'});
}).toThrow(
'Invariant Violation: The `style` prop expects a mapping from style ' +
'properties to values, not a string. For example, ' +
'style={{marginRight: spacing + \'em\'}} when using JSX.'
);
});
it("should execute custom event plugin listening behavior", function() {
var React = require('React');
var SimpleEventPlugin = require('SimpleEventPlugin');
SimpleEventPlugin.didPutListener = mocks.getMockFunction();
SimpleEventPlugin.willDeleteListener = mocks.getMockFunction();
var container = document.createElement('div');
React.render(
<div onClick={() => true} />,
container
);
expect(SimpleEventPlugin.didPutListener.mock.calls.length).toBe(1);
React.unmountComponentAtNode(container);
expect(SimpleEventPlugin.willDeleteListener.mock.calls.length).toBe(1);
});
it("should handle null and missing properly with event hooks", function() {
var React = require('React');
var SimpleEventPlugin = require('SimpleEventPlugin');
SimpleEventPlugin.didPutListener = mocks.getMockFunction();
SimpleEventPlugin.willDeleteListener = mocks.getMockFunction();
var container = document.createElement('div');
React.render(<div onClick={null} />, container);
expect(SimpleEventPlugin.didPutListener.mock.calls.length).toBe(0);
expect(SimpleEventPlugin.willDeleteListener.mock.calls.length).toBe(0);
React.render(<div onClick={() => 'apple'} />, container);
expect(SimpleEventPlugin.didPutListener.mock.calls.length).toBe(1);
expect(SimpleEventPlugin.willDeleteListener.mock.calls.length).toBe(0);
React.render(<div onClick={() => 'banana'} />, container);
expect(SimpleEventPlugin.didPutListener.mock.calls.length).toBe(2);
expect(SimpleEventPlugin.willDeleteListener.mock.calls.length).toBe(0);
React.render(<div onClick={null} />, container);
expect(SimpleEventPlugin.didPutListener.mock.calls.length).toBe(2);
expect(SimpleEventPlugin.willDeleteListener.mock.calls.length).toBe(1);
React.render(<div />, container);
expect(SimpleEventPlugin.didPutListener.mock.calls.length).toBe(2);
expect(SimpleEventPlugin.willDeleteListener.mock.calls.length).toBe(1);
React.unmountComponentAtNode(container);
expect(SimpleEventPlugin.didPutListener.mock.calls.length).toBe(2);
expect(SimpleEventPlugin.willDeleteListener.mock.calls.length).toBe(1);
});
});
describe('updateComponent', function() {
var React;
var container;
beforeEach(function() {
React = require('React');
container = document.createElement('div');
});
it("should warn against children for void elements", function() {
spyOn(console, 'warn');
React.render(<input />, container);
React.render(<input>children</input>, container);
expect(console.warn.argsForCall.length).toBe(1);
expect(console.warn.argsForCall[0][0]).toContain('void element');
});
it("should warn against dangerouslySetInnerHTML for void elements", function() {
spyOn(console, 'warn');
React.render(<input />, container);
React.render(
<input dangerouslySetInnerHTML={{__html: 'content'}} />,
container
);
expect(console.warn.argsForCall.length).toBe(1);
expect(console.warn.argsForCall[0][0]).toContain('void element');
});
it("should validate against multiple children props", function() {
React.render(<div></div>, container);
expect(function() {
React.render(
<div children="" dangerouslySetInnerHTML={{__html: ''}}></div>,
container
);
}).toThrow(
'Invariant Violation: Can only set one of `children` or ' +
'`props.dangerouslySetInnerHTML`.'
);
});
it("should warn about contentEditable and children", function() {
spyOn(console, 'warn');
React.render(
<div contentEditable><div /></div>,
container
);
expect(console.warn.argsForCall.length).toBe(1);
expect(console.warn.argsForCall[0][0]).toContain('contentEditable');
});
it("should validate against invalid styles", function() {
React.render(<div></div>, container);
expect(function() {
React.render(<div style={1}></div>, container);
}).toThrow(
'Invariant Violation: The `style` prop expects a mapping from style ' +
'properties to values, not a string. For example, ' +
'style={{marginRight: spacing + \'em\'}} when using JSX.'
);
});
it("should properly escape text content and attributes values", function() {
expect(
React.renderToStaticMarkup(
React.DOM.div({
title: '\'"<>&',
style: {
textAlign: '\'"<>&'
}
}, '\'"<>&')
)
).toBe(
'<div title="&#x27;&quot;&lt;&gt;&amp;" style="text-align:&#x27;&quot;&lt;&gt;&amp;;">' +
'&#x27;&quot;&lt;&gt;&amp;' +
'</div>'
);
});
});
describe('unmountComponent', function() {
it("should clean up listeners", function() {
var React = require('React');
var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter');
var ReactMount = require('ReactMount');
var container = document.createElement('div');
document.documentElement.appendChild(container);
var callback = function() {};
var instance = <div onClick={callback} />;
instance = React.render(instance, container);
var rootNode = React.findDOMNode(instance);
var rootNodeID = ReactMount.getID(rootNode);
expect(
ReactBrowserEventEmitter.getListener(rootNodeID, 'onClick')
).toBe(callback);
expect(rootNode).toBe(instance.getDOMNode());
React.unmountComponentAtNode(container);
expect(
ReactBrowserEventEmitter.getListener(rootNodeID, 'onClick')
).toBe(undefined);
});
});
describe('onScroll warning', function() {
it('should warn about the `onScroll` issue when unsupported (IE8)', () => {
// Mock this here so we can mimic IE8 support. We require isEventSupported
// before React so it's pre-mocked before React qould require it.
require('mock-modules')
.dumpCache()
.mock('isEventSupported');
var isEventSupported = require('isEventSupported');
isEventSupported.mockReturnValueOnce(false);
var React = require('React');
var ReactTestUtils = require('ReactTestUtils');
spyOn(console, 'warn');
ReactTestUtils.renderIntoDocument(<div onScroll={function() {}} />);
expect(console.warn.calls.length).toBe(1);
expect(console.warn.mostRecentCall.args[0]).toBe(
'Warning: This browser doesn\'t support the `onScroll` event'
);
});
});
describe('tag sanitization', function() {
it('should throw when an invalid tag name is used', () => {
var React = require('React');
var ReactTestUtils = require('ReactTestUtils');
var hackzor = React.createElement('script tag');
expect(
() => ReactTestUtils.renderIntoDocument(hackzor)
).toThrow(
'Invariant Violation: Invalid tag: script tag'
);
});
it('should throw when an attack vector is used', () => {
var React = require('React');
var ReactTestUtils = require('ReactTestUtils');
var hackzor = React.createElement('div><img /><div');
expect(
() => ReactTestUtils.renderIntoDocument(hackzor)
).toThrow(
'Invariant Violation: Invalid tag: div><img /><div'
);
});
});
});