mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
1486 lines
52 KiB
JavaScript
1486 lines
52 KiB
JavaScript
/**
|
|
* Copyright 2013-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('ReactDOMComponent', function() {
|
|
var React;
|
|
var ReactDOM;
|
|
var ReactDOMFeatureFlags;
|
|
var ReactDOMServer;
|
|
var inputValueTracking;
|
|
|
|
function normalizeCodeLocInfo(str) {
|
|
return str.replace(/\(at .+?:\d+\)/g, '(at **)');
|
|
}
|
|
|
|
beforeEach(function() {
|
|
jest.resetModuleRegistry();
|
|
React = require('React');
|
|
ReactDOM = require('ReactDOM');
|
|
ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
|
|
ReactDOMServer = require('ReactDOMServer');
|
|
inputValueTracking = require('inputValueTracking');
|
|
});
|
|
|
|
describe('updateDOM', function() {
|
|
var ReactTestUtils;
|
|
|
|
beforeEach(function() {
|
|
ReactTestUtils = require('ReactTestUtils');
|
|
});
|
|
|
|
it('should handle className', function() {
|
|
var container = document.createElement('div');
|
|
ReactDOM.render(<div style={{}} />, container);
|
|
|
|
ReactDOM.render(<div className={'foo'} />, container);
|
|
expect(container.firstChild.className).toEqual('foo');
|
|
ReactDOM.render(<div className={'bar'} />, container);
|
|
expect(container.firstChild.className).toEqual('bar');
|
|
ReactDOM.render(<div className={null} />, container);
|
|
expect(container.firstChild.className).toEqual('');
|
|
});
|
|
|
|
it('should gracefully handle various style value types', function() {
|
|
var container = document.createElement('div');
|
|
ReactDOM.render(<div style={{}} />, container);
|
|
var stubStyle = container.firstChild.style;
|
|
|
|
// set initial style
|
|
var setup = {display: 'block', left: '1px', top: 2, fontFamily: 'Arial'};
|
|
ReactDOM.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};
|
|
ReactDOM.render(<div style={reset} />, container);
|
|
expect(stubStyle.display).toEqual('');
|
|
expect(stubStyle.left).toEqual('');
|
|
expect(stubStyle.top).toEqual('');
|
|
expect(stubStyle.fontFamily).toEqual('');
|
|
});
|
|
|
|
// TODO: (poshannessy) deprecate this pattern.
|
|
it('should update styles when mutating style object', function() {
|
|
// not actually used. Just to suppress the style mutation warning
|
|
spyOn(console, 'error');
|
|
|
|
var styles = {display: 'none', fontFamily: 'Arial', lineHeight: 1.2};
|
|
var container = document.createElement('div');
|
|
ReactDOM.render(<div style={styles} />, container);
|
|
|
|
var stubStyle = container.firstChild.style;
|
|
stubStyle.display = styles.display;
|
|
stubStyle.fontFamily = styles.fontFamily;
|
|
|
|
styles.display = 'block';
|
|
|
|
ReactDOM.render(<div style={styles} />, container);
|
|
expect(stubStyle.display).toEqual('block');
|
|
expect(stubStyle.fontFamily).toEqual('Arial');
|
|
expect(stubStyle.lineHeight).toEqual('1.2');
|
|
|
|
styles.fontFamily = 'Helvetica';
|
|
|
|
ReactDOM.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;
|
|
|
|
ReactDOM.render(<div style={styles} />, container);
|
|
expect(stubStyle.display).toEqual('block');
|
|
expect(stubStyle.fontFamily).toEqual('Helvetica');
|
|
expect(stubStyle.lineHeight).toEqual('0.5');
|
|
|
|
ReactDOM.render(<div style={undefined} />, container);
|
|
expect(stubStyle.display).toBe('');
|
|
expect(stubStyle.fontFamily).toBe('');
|
|
expect(stubStyle.lineHeight).toBe('');
|
|
});
|
|
|
|
it('should warn when mutating style', function() {
|
|
spyOn(console, 'error');
|
|
|
|
var style = {border: '1px solid black'};
|
|
var App = React.createClass({
|
|
getInitialState: function() {
|
|
return {style: style};
|
|
},
|
|
render: function() {
|
|
return <div style={this.state.style}>asd</div>;
|
|
},
|
|
});
|
|
|
|
var stub = ReactTestUtils.renderIntoDocument(<App />);
|
|
style.position = 'absolute';
|
|
stub.setState({style: style});
|
|
expect(console.error.calls.count()).toBe(1);
|
|
expect(console.error.calls.argsFor(0)[0]).toEqual(
|
|
'Warning: `div` was passed a style object that has previously been ' +
|
|
'mutated. Mutating `style` is deprecated. Consider cloning it ' +
|
|
'beforehand. Check the `render` of `App`. Previous style: ' +
|
|
'{border: "1px solid black"}. Mutated style: ' +
|
|
'{border: "1px solid black", position: "absolute"}.'
|
|
);
|
|
|
|
style = {background: 'red'};
|
|
stub = ReactTestUtils.renderIntoDocument(<App />);
|
|
style.background = 'green';
|
|
stub.setState({style: {background: 'green'}});
|
|
// already warned once for the same component and owner
|
|
expect(console.error.calls.count()).toBe(1);
|
|
|
|
style = {background: 'red'};
|
|
var div = document.createElement('div');
|
|
ReactDOM.render(<span style={style}></span>, div);
|
|
style.background = 'blue';
|
|
ReactDOM.render(<span style={style}></span>, div);
|
|
expect(console.error.calls.count()).toBe(2);
|
|
});
|
|
|
|
it('should warn for unknown prop', function() {
|
|
spyOn(console, 'error');
|
|
var container = document.createElement('div');
|
|
ReactDOM.render(<div foo="bar" />, container);
|
|
expect(console.error.calls.count(0)).toBe(1);
|
|
expect(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe(
|
|
'Warning: Unknown prop `foo` on <div> tag. Remove this prop from the element. ' +
|
|
'For details, see https://fb.me/react-unknown-prop\n in div (at **)'
|
|
);
|
|
});
|
|
|
|
it('should group multiple unknown prop warnings together', function() {
|
|
spyOn(console, 'error');
|
|
var container = document.createElement('div');
|
|
ReactDOM.render(<div foo="bar" baz="qux" />, container);
|
|
expect(console.error.calls.count(0)).toBe(1);
|
|
expect(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe(
|
|
'Warning: Unknown props `foo`, `baz` on <div> tag. Remove these props from the element. ' +
|
|
'For details, see https://fb.me/react-unknown-prop\n in div (at **)'
|
|
);
|
|
});
|
|
|
|
it('should warn for onDblClick prop', function() {
|
|
spyOn(console, 'error');
|
|
var container = document.createElement('div');
|
|
ReactDOM.render(<div onDblClick={() => {}} />, container);
|
|
expect(console.error.calls.count(0)).toBe(1);
|
|
expect(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe(
|
|
'Warning: Unknown event handler property onDblClick. Did you mean `onDoubleClick`?\n in div (at **)'
|
|
);
|
|
});
|
|
|
|
it('should not warn for "0" as a unitless style value', function() {
|
|
spyOn(console, 'error');
|
|
var Component = React.createClass({
|
|
render: function() {
|
|
return <div style={{margin: '0'}} />;
|
|
},
|
|
});
|
|
|
|
ReactTestUtils.renderIntoDocument(<Component />);
|
|
expect(console.error.calls.count()).toBe(0);
|
|
});
|
|
|
|
it('should warn nicely about NaN in style', function() {
|
|
spyOn(console, 'error');
|
|
|
|
var style = {fontSize: NaN};
|
|
var div = document.createElement('div');
|
|
ReactDOM.render(<span style={style}></span>, div);
|
|
ReactDOM.render(<span style={style}></span>, div);
|
|
|
|
expect(console.error.calls.count()).toBe(1);
|
|
expect(console.error.calls.argsFor(0)[0]).toEqual(
|
|
'Warning: `NaN` is an invalid value for the `fontSize` css style property.',
|
|
);
|
|
});
|
|
|
|
it('should update styles if initially null', function() {
|
|
var styles = null;
|
|
var container = document.createElement('div');
|
|
ReactDOM.render(<div style={styles} />, container);
|
|
|
|
var stubStyle = container.firstChild.style;
|
|
|
|
styles = {display: 'block'};
|
|
|
|
ReactDOM.render(<div style={styles} />, container);
|
|
expect(stubStyle.display).toEqual('block');
|
|
});
|
|
|
|
it('should update styles if updated to null multiple times', function() {
|
|
var styles = null;
|
|
var container = document.createElement('div');
|
|
ReactDOM.render(<div style={styles} />, container);
|
|
|
|
styles = {display: 'block'};
|
|
var stubStyle = container.firstChild.style;
|
|
|
|
ReactDOM.render(<div style={styles} />, container);
|
|
expect(stubStyle.display).toEqual('block');
|
|
|
|
ReactDOM.render(<div style={null} />, container);
|
|
expect(stubStyle.display).toEqual('');
|
|
|
|
ReactDOM.render(<div style={styles} />, container);
|
|
expect(stubStyle.display).toEqual('block');
|
|
|
|
ReactDOM.render(<div style={null} />, container);
|
|
expect(stubStyle.display).toEqual('');
|
|
});
|
|
|
|
it('should skip reserved props on web components', function() {
|
|
var container = document.createElement('div');
|
|
|
|
ReactDOM.render(
|
|
<my-component
|
|
children={['foo']}
|
|
suppressContentEditableWarning={true}
|
|
/>,
|
|
container
|
|
);
|
|
expect(container.firstChild.hasAttribute('children')).toBe(false);
|
|
expect(
|
|
container.firstChild.hasAttribute('suppressContentEditableWarning')
|
|
).toBe(false);
|
|
|
|
ReactDOM.render(
|
|
<my-component
|
|
children={['bar']}
|
|
suppressContentEditableWarning={false}
|
|
/>,
|
|
container
|
|
);
|
|
expect(container.firstChild.hasAttribute('children')).toBe(false);
|
|
expect(
|
|
container.firstChild.hasAttribute('suppressContentEditableWarning')
|
|
).toBe(false);
|
|
});
|
|
|
|
it('should skip dangerouslySetInnerHTML on web components', function() {
|
|
var container = document.createElement('div');
|
|
|
|
ReactDOM.render(
|
|
<my-component dangerouslySetInnerHTML={{__html: 'hi'}} />,
|
|
container
|
|
);
|
|
expect(
|
|
container.firstChild.hasAttribute('dangerouslySetInnerHTML')
|
|
).toBe(false);
|
|
|
|
ReactDOM.render(
|
|
<my-component dangerouslySetInnerHTML={{__html: 'bye'}} />,
|
|
container
|
|
);
|
|
expect(
|
|
container.firstChild.hasAttribute('dangerouslySetInnerHTML')
|
|
).toBe(false);
|
|
});
|
|
|
|
it('should remove attributes', function() {
|
|
var container = document.createElement('div');
|
|
ReactDOM.render(<img height="17" />, container);
|
|
|
|
expect(container.firstChild.hasAttribute('height')).toBe(true);
|
|
ReactDOM.render(<img />, container);
|
|
expect(container.firstChild.hasAttribute('height')).toBe(false);
|
|
});
|
|
|
|
it('should remove properties', function() {
|
|
var container = document.createElement('div');
|
|
ReactDOM.render(<div className="monkey" />, container);
|
|
|
|
expect(container.firstChild.className).toEqual('monkey');
|
|
ReactDOM.render(<div />, container);
|
|
expect(container.firstChild.className).toEqual('');
|
|
});
|
|
|
|
it('should properly update custom attributes on custom elements', function() {
|
|
var container = document.createElement('div');
|
|
ReactDOM.render(<some-custom-element foo="bar"/>, container);
|
|
ReactDOM.render(<some-custom-element bar="buzz"/>, container);
|
|
var node = container.firstChild;
|
|
expect(node.hasAttribute('foo')).toBe(false);
|
|
expect(node.getAttribute('bar')).toBe('buzz');
|
|
});
|
|
|
|
it('should clear a single style prop when changing `style`', function() {
|
|
var styles = {display: 'none', color: 'red'};
|
|
var container = document.createElement('div');
|
|
ReactDOM.render(<div style={styles} />, container);
|
|
|
|
var stubStyle = container.firstChild.style;
|
|
|
|
styles = {color: 'green'};
|
|
ReactDOM.render(<div style={styles} />, container);
|
|
expect(stubStyle.display).toEqual('');
|
|
expect(stubStyle.color).toEqual('green');
|
|
});
|
|
|
|
it('should reject attribute key injection attack on markup', function() {
|
|
spyOn(console, 'error');
|
|
for (var i = 0; i < 3; i++) {
|
|
var container = document.createElement('div');
|
|
var element = React.createElement(
|
|
'x-foo-component',
|
|
{'blah" onclick="beevil" noise="hi': 'selected'},
|
|
null
|
|
);
|
|
ReactDOM.render(element, container);
|
|
}
|
|
expect(console.error.calls.count()).toBe(1);
|
|
expect(console.error.calls.argsFor(0)[0]).toEqual(
|
|
'Warning: Invalid attribute name: `blah" onclick="beevil" noise="hi`'
|
|
);
|
|
});
|
|
|
|
it('should reject attribute key injection attack on update', function() {
|
|
spyOn(console, 'error');
|
|
for (var i = 0; i < 3; i++) {
|
|
var container = document.createElement('div');
|
|
var beforeUpdate = React.createElement('x-foo-component', {}, null);
|
|
ReactDOM.render(beforeUpdate, container);
|
|
|
|
var afterUpdate = React.createElement(
|
|
'x-foo-component',
|
|
{'blah" onclick="beevil" noise="hi': 'selected'},
|
|
null
|
|
);
|
|
ReactDOM.render(afterUpdate, container);
|
|
}
|
|
expect(console.error.calls.count()).toBe(1);
|
|
expect(console.error.calls.argsFor(0)[0]).toEqual(
|
|
'Warning: Invalid attribute name: `blah" onclick="beevil" noise="hi`'
|
|
);
|
|
});
|
|
|
|
it('should update arbitrary attributes for tags containing dashes', function() {
|
|
var container = document.createElement('div');
|
|
|
|
var beforeUpdate = React.createElement('x-foo-component', {}, null);
|
|
ReactDOM.render(beforeUpdate, container);
|
|
|
|
var afterUpdate = <x-foo-component myattr="myval" />;
|
|
ReactDOM.render(afterUpdate, container);
|
|
|
|
expect(container.childNodes[0].getAttribute('myattr')).toBe('myval');
|
|
});
|
|
|
|
it('should clear all the styles when removing `style`', function() {
|
|
var styles = {display: 'none', color: 'red'};
|
|
var container = document.createElement('div');
|
|
ReactDOM.render(<div style={styles} />, container);
|
|
|
|
var stubStyle = container.firstChild.style;
|
|
|
|
ReactDOM.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'};
|
|
ReactDOM.render(<div style={styles} />, container);
|
|
ReactDOM.render(<div />, container);
|
|
ReactDOM.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');
|
|
ReactDOM.render(<div dangerouslySetInnerHTML={{__html: ':)'}} />, container);
|
|
|
|
expect(container.firstChild.innerHTML).toEqual(':)');
|
|
ReactDOM.render(<div />, container);
|
|
expect(container.firstChild.innerHTML).toEqual('');
|
|
});
|
|
|
|
it('should transition from string content to innerHTML', function() {
|
|
var container = document.createElement('div');
|
|
ReactDOM.render(<div>hello</div>, container);
|
|
|
|
expect(container.firstChild.innerHTML).toEqual('hello');
|
|
ReactDOM.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');
|
|
ReactDOM.render(
|
|
<div dangerouslySetInnerHTML={{__html: 'bonjour'}} />,
|
|
container
|
|
);
|
|
|
|
expect(container.firstChild.innerHTML).toEqual('bonjour');
|
|
ReactDOM.render(<div>adieu</div>, container);
|
|
expect(container.firstChild.innerHTML).toEqual('adieu');
|
|
});
|
|
|
|
it('should transition from innerHTML to children in nested el', function() {
|
|
var container = document.createElement('div');
|
|
ReactDOM.render(
|
|
<div><div dangerouslySetInnerHTML={{__html: 'bonjour'}} /></div>,
|
|
container
|
|
);
|
|
|
|
expect(container.textContent).toEqual('bonjour');
|
|
ReactDOM.render(<div><div><span>adieu</span></div></div>, container);
|
|
expect(container.textContent).toEqual('adieu');
|
|
});
|
|
|
|
it('should transition from children to innerHTML in nested el', function() {
|
|
var container = document.createElement('div');
|
|
ReactDOM.render(<div><div><span>adieu</span></div></div>, container);
|
|
|
|
expect(container.textContent).toEqual('adieu');
|
|
ReactDOM.render(
|
|
<div><div dangerouslySetInnerHTML={{__html: 'bonjour'}} /></div>,
|
|
container
|
|
);
|
|
expect(container.textContent).toEqual('bonjour');
|
|
});
|
|
|
|
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.fn();
|
|
node.setAttribute.mockImpl(nodeSetAttribute);
|
|
|
|
var nodeRemoveAttribute = node.removeAttribute;
|
|
node.removeAttribute = jest.fn();
|
|
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);
|
|
|
|
var node = container.firstChild;
|
|
|
|
var nodeValueSetter = jest.genMockFn();
|
|
|
|
var oldSetAttribute = node.setAttribute.bind(node);
|
|
node.setAttribute = function(key, value) {
|
|
oldSetAttribute(key, value);
|
|
nodeValueSetter(key, value);
|
|
};
|
|
|
|
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(1);
|
|
|
|
ReactDOM.render(<div value={null} />, container);
|
|
expect(nodeValueSetter.mock.calls.length).toBe(1);
|
|
|
|
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.fn();
|
|
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);
|
|
|
|
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() {
|
|
var container = document.createElement('div');
|
|
ReactDOM.render(<button is="test" cowabunga="chevynova"/>, container);
|
|
expect(container.firstChild.hasAttribute('cowabunga')).toBe(true);
|
|
});
|
|
|
|
it('should not update when switching between null/undefined', function() {
|
|
var container = document.createElement('div');
|
|
var node = ReactDOM.render(<div />, container);
|
|
|
|
var setter = jest.fn();
|
|
node.setAttribute = setter;
|
|
|
|
ReactDOM.render(<div dir={null} />, container);
|
|
ReactDOM.render(<div dir={undefined} />, container);
|
|
ReactDOM.render(<div />, container);
|
|
expect(setter.mock.calls.length).toBe(0);
|
|
ReactDOM.render(<div dir="ltr" />, container);
|
|
expect(setter.mock.calls.length).toBe(1);
|
|
});
|
|
|
|
it('handles multiple child updates without interference', function() {
|
|
// This test might look like it's just testing ReactMultiChild but the
|
|
// last bug in this was actually in DOMChildrenOperations so this test
|
|
// needs to be in some DOM-specific test file.
|
|
var container = document.createElement('div');
|
|
|
|
// ABCD
|
|
ReactDOM.render(
|
|
<div>
|
|
<div key="one">
|
|
<div key="A">A</div><div key="B">B</div>
|
|
</div>
|
|
<div key="two">
|
|
<div key="C">C</div><div key="D">D</div>
|
|
</div>
|
|
</div>,
|
|
container
|
|
);
|
|
// BADC
|
|
ReactDOM.render(
|
|
<div>
|
|
<div key="one">
|
|
<div key="B">B</div><div key="A">A</div>
|
|
</div>
|
|
<div key="two">
|
|
<div key="D">D</div><div key="C">C</div>
|
|
</div>
|
|
</div>,
|
|
container
|
|
);
|
|
|
|
expect(container.textContent).toBe('BADC');
|
|
});
|
|
});
|
|
|
|
describe('createOpenTagMarkup', function() {
|
|
var genMarkup;
|
|
|
|
function quoteRegexp(str) {
|
|
return (str + '').replace(/([.?*+\^$\[\]\\(){}|-])/g, '\\$1');
|
|
}
|
|
|
|
beforeEach(function() {
|
|
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';
|
|
};
|
|
Object.assign(NodeStub.prototype, ReactDOMComponent.Mixin);
|
|
|
|
genMarkup = function(props) {
|
|
var transaction = new ReactReconcileTransaction();
|
|
return (new NodeStub(props))._createOpenTagMarkupAndPutListeners(
|
|
transaction,
|
|
props
|
|
);
|
|
};
|
|
|
|
jasmine.addMatchers({
|
|
toHaveAttribute() {
|
|
return {
|
|
compare(actual, expected) {
|
|
var [attr, value] = expected;
|
|
var re = '(?:^|\\s)' + attr + '=[\\\'"]';
|
|
if (typeof value !== 'undefined') {
|
|
re += quoteRegexp(value) + '[\\\'"]';
|
|
}
|
|
return {
|
|
pass: (new RegExp(re)).test(actual),
|
|
};
|
|
},
|
|
};
|
|
},
|
|
});
|
|
});
|
|
|
|
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&ckground:<3;']);
|
|
});
|
|
});
|
|
|
|
describe('createContentMarkup', function() {
|
|
var genMarkup;
|
|
|
|
function quoteRegexp(str) {
|
|
return (str + '').replace(/([.?*+\^$\[\]\\(){}|-])/g, '\\$1');
|
|
}
|
|
|
|
beforeEach(function() {
|
|
var ReactDOMComponent = require('ReactDOMComponent');
|
|
var ReactReconcileTransaction = require('ReactReconcileTransaction');
|
|
|
|
var NodeStub = function(initialProps) {
|
|
this._currentElement = {props: initialProps};
|
|
this._rootNodeID = 'test';
|
|
};
|
|
Object.assign(NodeStub.prototype, ReactDOMComponent.Mixin);
|
|
|
|
genMarkup = function(props) {
|
|
var transaction = new ReactReconcileTransaction();
|
|
return (new NodeStub(props))._createContentMarkup(
|
|
transaction,
|
|
props,
|
|
{}
|
|
);
|
|
};
|
|
|
|
jasmine.addMatchers({
|
|
toHaveInnerhtml() {
|
|
return {
|
|
compare(actual, expected) {
|
|
var re = '^' + quoteRegexp(expected) + '$';
|
|
return {
|
|
pass: (new RegExp(re)).test(actual),
|
|
};
|
|
},
|
|
};
|
|
},
|
|
});
|
|
});
|
|
|
|
it('should handle dangerouslySetInnerHTML', function() {
|
|
var innerHTML = {__html: 'testContent'};
|
|
expect(
|
|
genMarkup({dangerouslySetInnerHTML: innerHTML})
|
|
).toHaveInnerhtml('testContent');
|
|
});
|
|
});
|
|
|
|
describe('mountComponent', function() {
|
|
var mountComponent;
|
|
|
|
beforeEach(function() {
|
|
mountComponent = function(props) {
|
|
var container = document.createElement('div');
|
|
ReactDOM.render(<div {...props} />, container);
|
|
};
|
|
});
|
|
|
|
it('should work error event on <source> element', function() {
|
|
spyOn(console, 'error');
|
|
var container = document.createElement('div');
|
|
ReactDOM.render(
|
|
<video>
|
|
<source src="http://example.org/video" type="video/mp4" onError={(e) => console.error('onError called')} />
|
|
</video>,
|
|
container
|
|
);
|
|
|
|
var errorEvent = document.createEvent('Event');
|
|
errorEvent.initEvent('error', false, false);
|
|
container.getElementsByTagName('source')[0].dispatchEvent(errorEvent);
|
|
|
|
expect(console.error.calls.count()).toBe(1);
|
|
expect(console.error.calls.argsFor(0)[0]).toContain(
|
|
'onError called'
|
|
);
|
|
});
|
|
|
|
it('should not duplicate uppercased selfclosing tags', function() {
|
|
var Container = React.createClass({
|
|
render: function() {
|
|
return React.createElement('BR', null);
|
|
},
|
|
});
|
|
var returnedValue = ReactDOMServer.renderToString(<Container/>);
|
|
expect(returnedValue).not.toContain('</BR>');
|
|
});
|
|
|
|
it('should warn against children for void elements', function() {
|
|
var container = document.createElement('div');
|
|
|
|
expect(function() {
|
|
ReactDOM.render(<input>children</input>, container);
|
|
}).toThrowError(
|
|
'input is a void element tag and must neither have `children` nor ' +
|
|
'use `dangerouslySetInnerHTML`.'
|
|
);
|
|
});
|
|
|
|
it('should warn against dangerouslySetInnerHTML for void elements', function() {
|
|
var container = document.createElement('div');
|
|
|
|
expect(function() {
|
|
ReactDOM.render(
|
|
<input dangerouslySetInnerHTML={{__html: 'content'}} />,
|
|
container
|
|
);
|
|
}).toThrowError(
|
|
'input is a void element tag and must neither have `children` nor use ' +
|
|
'`dangerouslySetInnerHTML`.'
|
|
);
|
|
});
|
|
|
|
it('should treat menuitem as a void element but still create the closing tag', function() {
|
|
var container = document.createElement('div');
|
|
|
|
var returnedValue = ReactDOMServer.renderToString(<menu><menuitem /></menu>);
|
|
|
|
expect(returnedValue).toContain('</menuitem>');
|
|
|
|
expect(function() {
|
|
ReactDOM.render(<menu><menuitem>children</menuitem></menu>, container);
|
|
}).toThrowError(
|
|
'menuitem is a void element tag and must neither have `children` nor use ' +
|
|
'`dangerouslySetInnerHTML`.'
|
|
);
|
|
|
|
});
|
|
|
|
it('should validate against multiple children props', function() {
|
|
expect(function() {
|
|
mountComponent({children: '', dangerouslySetInnerHTML: ''});
|
|
}).toThrowError(
|
|
'Can only set one of `children` or `props.dangerouslySetInnerHTML`.'
|
|
);
|
|
});
|
|
|
|
it('should validate against use of innerHTML', function() {
|
|
|
|
spyOn(console, 'error');
|
|
mountComponent({innerHTML: '<span>Hi Jim!</span>'});
|
|
expect(console.error.calls.count()).toBe(1);
|
|
expect(console.error.calls.argsFor(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>'});
|
|
}).toThrowError(
|
|
'`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' +
|
|
'Please visit https://fb.me/react-invariant-dangerously-set-inner-html for more information.'
|
|
);
|
|
});
|
|
|
|
it('should validate use of dangerouslySetInnerHTML', function() {
|
|
expect(function() {
|
|
mountComponent({dangerouslySetInnerHTML: {foo: 'bar'} });
|
|
}).toThrowError(
|
|
'`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' +
|
|
'Please visit https://fb.me/react-invariant-dangerously-set-inner-html for more information.'
|
|
);
|
|
});
|
|
|
|
it('should allow {__html: null}', function() {
|
|
expect(function() {
|
|
mountComponent({dangerouslySetInnerHTML: {__html: null} });
|
|
}).not.toThrow();
|
|
});
|
|
|
|
it('should warn about contentEditable and children', function() {
|
|
spyOn(console, 'error');
|
|
mountComponent({contentEditable: true, children: ''});
|
|
expect(console.error.calls.count()).toBe(1);
|
|
expect(console.error.calls.argsFor(0)[0]).toContain('contentEditable');
|
|
});
|
|
|
|
it('should respect suppressContentEditableWarning', function() {
|
|
spyOn(console, 'error');
|
|
mountComponent({contentEditable: true, children: '', suppressContentEditableWarning: true});
|
|
expect(console.error.calls.count()).toBe(0);
|
|
});
|
|
|
|
it('should validate against invalid styles', function() {
|
|
expect(function() {
|
|
mountComponent({style: 'display: none'});
|
|
}).toThrowError(
|
|
'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 track input values', function() {
|
|
var container = document.createElement('div');
|
|
var inst = ReactDOM.render(<input type="text" defaultValue="foo"/>, container);
|
|
|
|
var tracker = inputValueTracking._getTrackerFromNode(inst);
|
|
|
|
expect(tracker.getValue()).toEqual('foo');
|
|
});
|
|
|
|
it('should track textarea values', function() {
|
|
var container = document.createElement('div');
|
|
var inst = ReactDOM.render(<textarea defaultValue="foo"/>, container);
|
|
|
|
var tracker = inputValueTracking._getTrackerFromNode(inst);
|
|
|
|
expect(tracker.getValue()).toEqual('foo');
|
|
});
|
|
|
|
it('should execute custom event plugin listening behavior', function() {
|
|
var SimpleEventPlugin = require('SimpleEventPlugin');
|
|
|
|
SimpleEventPlugin.didPutListener = jest.fn();
|
|
SimpleEventPlugin.willDeleteListener = jest.fn();
|
|
|
|
var container = document.createElement('div');
|
|
ReactDOM.render(
|
|
<div onClick={() => true} />,
|
|
container
|
|
);
|
|
|
|
expect(SimpleEventPlugin.didPutListener.mock.calls.length).toBe(1);
|
|
|
|
ReactDOM.unmountComponentAtNode(container);
|
|
|
|
expect(SimpleEventPlugin.willDeleteListener.mock.calls.length).toBe(1);
|
|
});
|
|
|
|
it('should handle null and missing properly with event hooks', function() {
|
|
var SimpleEventPlugin = require('SimpleEventPlugin');
|
|
|
|
SimpleEventPlugin.didPutListener = jest.fn();
|
|
SimpleEventPlugin.willDeleteListener = jest.fn();
|
|
var container = document.createElement('div');
|
|
|
|
ReactDOM.render(<div onClick={false} />, container);
|
|
expect(SimpleEventPlugin.didPutListener.mock.calls.length).toBe(0);
|
|
expect(SimpleEventPlugin.willDeleteListener.mock.calls.length).toBe(0);
|
|
|
|
ReactDOM.render(<div onClick={null} />, container);
|
|
expect(SimpleEventPlugin.didPutListener.mock.calls.length).toBe(0);
|
|
expect(SimpleEventPlugin.willDeleteListener.mock.calls.length).toBe(0);
|
|
|
|
ReactDOM.render(<div onClick={() => 'apple'} />, container);
|
|
expect(SimpleEventPlugin.didPutListener.mock.calls.length).toBe(1);
|
|
expect(SimpleEventPlugin.willDeleteListener.mock.calls.length).toBe(0);
|
|
|
|
ReactDOM.render(<div onClick={() => 'banana'} />, container);
|
|
expect(SimpleEventPlugin.didPutListener.mock.calls.length).toBe(2);
|
|
expect(SimpleEventPlugin.willDeleteListener.mock.calls.length).toBe(0);
|
|
|
|
ReactDOM.render(<div onClick={null} />, container);
|
|
expect(SimpleEventPlugin.didPutListener.mock.calls.length).toBe(2);
|
|
expect(SimpleEventPlugin.willDeleteListener.mock.calls.length).toBe(1);
|
|
|
|
ReactDOM.render(<div />, container);
|
|
expect(SimpleEventPlugin.didPutListener.mock.calls.length).toBe(2);
|
|
expect(SimpleEventPlugin.willDeleteListener.mock.calls.length).toBe(1);
|
|
|
|
ReactDOM.unmountComponentAtNode(container);
|
|
expect(SimpleEventPlugin.didPutListener.mock.calls.length).toBe(2);
|
|
expect(SimpleEventPlugin.willDeleteListener.mock.calls.length).toBe(1);
|
|
});
|
|
|
|
it('should warn for children on void elements', function() {
|
|
var X = React.createClass({
|
|
render: function() {
|
|
return <input>moo</input>;
|
|
},
|
|
});
|
|
|
|
var container = document.createElement('div');
|
|
expect(function() {
|
|
ReactDOM.render(<X />, container);
|
|
}).toThrowError(
|
|
'input is a void element tag and must neither have `children` ' +
|
|
'nor use `dangerouslySetInnerHTML`. Check the render method of X.'
|
|
);
|
|
});
|
|
|
|
it('should support custom elements which extend native elements', function() {
|
|
if (ReactDOMFeatureFlags.useCreateElement) {
|
|
var container = document.createElement('div');
|
|
spyOn(document, 'createElement').and.callThrough();
|
|
ReactDOM.render(<div is="custom-div" />, container);
|
|
expect(document.createElement).toHaveBeenCalledWith('div', 'custom-div');
|
|
} else {
|
|
expect(ReactDOMServer.renderToString(<div is="custom-div" />)).toContain('is="custom-div"');
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('updateComponent', function() {
|
|
var container;
|
|
|
|
beforeEach(function() {
|
|
container = document.createElement('div');
|
|
});
|
|
|
|
it('should warn against children for void elements', function() {
|
|
ReactDOM.render(<input />, container);
|
|
|
|
expect(function() {
|
|
ReactDOM.render(<input>children</input>, container);
|
|
}).toThrowError(
|
|
'input is a void element tag and must neither have `children` nor use ' +
|
|
'`dangerouslySetInnerHTML`.'
|
|
);
|
|
});
|
|
|
|
it('should warn against dangerouslySetInnerHTML for void elements', function() {
|
|
ReactDOM.render(<input />, container);
|
|
|
|
expect(function() {
|
|
ReactDOM.render(
|
|
<input dangerouslySetInnerHTML={{__html: 'content'}} />,
|
|
container
|
|
);
|
|
}).toThrowError(
|
|
'input is a void element tag and must neither have `children` nor use ' +
|
|
'`dangerouslySetInnerHTML`.'
|
|
);
|
|
});
|
|
|
|
it('should validate against multiple children props', function() {
|
|
ReactDOM.render(<div></div>, container);
|
|
|
|
expect(function() {
|
|
ReactDOM.render(
|
|
<div children="" dangerouslySetInnerHTML={{__html: ''}}></div>,
|
|
container
|
|
);
|
|
}).toThrowError(
|
|
'Can only set one of `children` or `props.dangerouslySetInnerHTML`.'
|
|
);
|
|
});
|
|
|
|
it('should warn about contentEditable and children', function() {
|
|
spyOn(console, 'error');
|
|
ReactDOM.render(
|
|
<div contentEditable={true}><div /></div>,
|
|
container
|
|
);
|
|
expect(console.error.calls.count()).toBe(1);
|
|
expect(console.error.calls.argsFor(0)[0]).toContain('contentEditable');
|
|
});
|
|
|
|
it('should validate against invalid styles', function() {
|
|
ReactDOM.render(<div></div>, container);
|
|
|
|
expect(function() {
|
|
ReactDOM.render(<div style={1}></div>, container);
|
|
}).toThrowError(
|
|
'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 report component containing invalid styles', function() {
|
|
var Animal = React.createClass({
|
|
render: function() {
|
|
return <div style={1}></div>;
|
|
},
|
|
});
|
|
|
|
expect(function() {
|
|
ReactDOM.render(<Animal/>, container);
|
|
}).toThrowError(
|
|
'The `style` prop expects a mapping from style properties to values, ' +
|
|
'not a string. For example, style={{marginRight: spacing + \'em\'}} ' +
|
|
'when using JSX. This DOM node was rendered by `Animal`.'
|
|
);
|
|
});
|
|
|
|
it('should properly escape text content and attributes values', function() {
|
|
expect(
|
|
ReactDOMServer.renderToStaticMarkup(
|
|
React.DOM.div({
|
|
title: '\'"<>&',
|
|
style: {
|
|
textAlign: '\'"<>&',
|
|
},
|
|
}, '\'"<>&')
|
|
)
|
|
).toBe(
|
|
'<div title="'"<>&" style="text-align:'"<>&;">' +
|
|
''"<>&' +
|
|
'</div>'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('unmountComponent', function() {
|
|
it('should clean up listeners', function() {
|
|
var EventPluginHub = require('EventPluginHub');
|
|
var ReactDOMComponentTree = require('ReactDOMComponentTree');
|
|
|
|
var container = document.createElement('div');
|
|
document.body.appendChild(container);
|
|
|
|
var callback = function() {};
|
|
var instance = <div onClick={callback} />;
|
|
instance = ReactDOM.render(instance, container);
|
|
|
|
var rootNode = ReactDOM.findDOMNode(instance);
|
|
var inst = ReactDOMComponentTree.getInstanceFromNode(rootNode);
|
|
expect(
|
|
EventPluginHub.getListener(inst, 'onClick')
|
|
).toBe(callback);
|
|
expect(rootNode).toBe(ReactDOM.findDOMNode(instance));
|
|
|
|
ReactDOM.unmountComponentAtNode(container);
|
|
|
|
expect(
|
|
EventPluginHub.getListener(inst, 'onClick')
|
|
).toBe(undefined);
|
|
});
|
|
|
|
it('should clean up input value tracking', function() {
|
|
var container = document.createElement('div');
|
|
var node = ReactDOM.render(<input type="text" defaultValue="foo"/>, container);
|
|
var tracker = inputValueTracking._getTrackerFromNode(node);
|
|
|
|
spyOn(tracker, 'stopTracking');
|
|
|
|
ReactDOM.unmountComponentAtNode(container);
|
|
|
|
expect(tracker.stopTracking.calls.count()).toBe(1);
|
|
});
|
|
|
|
it('should clean up input textarea tracking', function() {
|
|
var container = document.createElement('div');
|
|
var node = ReactDOM.render(<textarea defaultValue="foo"/>, container);
|
|
var tracker = inputValueTracking._getTrackerFromNode(node);
|
|
|
|
spyOn(tracker, 'stopTracking');
|
|
|
|
ReactDOM.unmountComponentAtNode(container);
|
|
|
|
expect(tracker.stopTracking.calls.count()).toBe(1);
|
|
});
|
|
|
|
it('unmounts children before unsetting DOM node info', function() {
|
|
var Inner = React.createClass({
|
|
render: function() {
|
|
return <span />;
|
|
},
|
|
componentWillUnmount: function() {
|
|
// Should not throw
|
|
expect(ReactDOM.findDOMNode(this).nodeName).toBe('SPAN');
|
|
},
|
|
});
|
|
|
|
var container = document.createElement('div');
|
|
ReactDOM.render(<div><Inner /></div>, container);
|
|
ReactDOM.unmountComponentAtNode(container);
|
|
});
|
|
});
|
|
|
|
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 would require it.
|
|
jest.resetModuleRegistry()
|
|
.mock('isEventSupported');
|
|
var isEventSupported = require('isEventSupported');
|
|
isEventSupported.mockReturnValueOnce(false);
|
|
|
|
var ReactTestUtils = require('ReactTestUtils');
|
|
|
|
spyOn(console, 'error');
|
|
ReactTestUtils.renderIntoDocument(<div onScroll={function() {}} />);
|
|
expect(console.error.calls.count()).toBe(1);
|
|
expect(console.error.calls.argsFor(0)[0]).toBe(
|
|
'Warning: This browser doesn\'t support the `onScroll` event'
|
|
);
|
|
});
|
|
|
|
it('should not warn when server-side rendering `onScroll`', function() {
|
|
spyOn(console, 'error');
|
|
ReactDOMServer.renderToString(<div onScroll={() => {}}/>);
|
|
expect(console.error).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('tag sanitization', function() {
|
|
it('should throw when an invalid tag name is used', () => {
|
|
var ReactTestUtils = require('ReactTestUtils');
|
|
var hackzor = React.createElement('script tag');
|
|
expect(
|
|
() => ReactTestUtils.renderIntoDocument(hackzor)
|
|
).toThrowError(
|
|
'Invalid tag: script tag'
|
|
);
|
|
});
|
|
|
|
it('should throw when an attack vector is used', () => {
|
|
var ReactTestUtils = require('ReactTestUtils');
|
|
var hackzor = React.createElement('div><img /><div');
|
|
expect(
|
|
() => ReactTestUtils.renderIntoDocument(hackzor)
|
|
).toThrowError(
|
|
'Invalid tag: div><img /><div'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('nesting validation', function() {
|
|
var ReactTestUtils;
|
|
|
|
beforeEach(function() {
|
|
ReactTestUtils = require('ReactTestUtils');
|
|
});
|
|
|
|
it('warns on invalid nesting', () => {
|
|
spyOn(console, 'error');
|
|
ReactTestUtils.renderIntoDocument(<div><tr /><tr /></div>);
|
|
|
|
expect(console.error.calls.count()).toBe(1);
|
|
expect(console.error.calls.argsFor(0)[0]).toBe(
|
|
'Warning: validateDOMNesting(...): <tr> cannot appear as a child of ' +
|
|
'<div>. See div > tr.'
|
|
);
|
|
});
|
|
|
|
it('warns on invalid nesting at root', () => {
|
|
spyOn(console, 'error');
|
|
var p = document.createElement('p');
|
|
ReactDOM.render(<span><p /></span>, p);
|
|
|
|
expect(console.error.calls.count()).toBe(1);
|
|
expect(console.error.calls.argsFor(0)[0]).toBe(
|
|
'Warning: validateDOMNesting(...): <p> cannot appear as a descendant ' +
|
|
'of <p>. See p > ... > p.'
|
|
);
|
|
});
|
|
|
|
it('warns nicely for table rows', () => {
|
|
spyOn(console, 'error');
|
|
var Row = React.createClass({
|
|
render: function() {
|
|
return <tr />;
|
|
},
|
|
});
|
|
var Foo = React.createClass({
|
|
render: function() {
|
|
return <table><Row /> </table>;
|
|
},
|
|
});
|
|
ReactTestUtils.renderIntoDocument(<Foo />);
|
|
|
|
expect(console.error.calls.count()).toBe(2);
|
|
expect(console.error.calls.argsFor(0)[0]).toBe(
|
|
'Warning: validateDOMNesting(...): <tr> cannot appear as a child of ' +
|
|
'<table>. See Foo > table > Row > tr. Add a <tbody> to your code to ' +
|
|
'match the DOM tree generated by the browser.'
|
|
);
|
|
expect(console.error.calls.argsFor(1)[0]).toBe(
|
|
'Warning: validateDOMNesting(...): #text cannot appear as a child ' +
|
|
'of <table>. See Foo > table > #text.'
|
|
);
|
|
});
|
|
|
|
it('gives useful context in warnings', () => {
|
|
spyOn(console, 'error');
|
|
var Row = React.createClass({
|
|
render: () => <tr />,
|
|
});
|
|
var FancyRow = React.createClass({
|
|
render: () => <Row />,
|
|
});
|
|
var Table = React.createClass({
|
|
render: function() {
|
|
return <table>{this.props.children}</table>;
|
|
},
|
|
});
|
|
var FancyTable = React.createClass({
|
|
render: function() {
|
|
return <Table>{this.props.children}</Table>;
|
|
},
|
|
});
|
|
|
|
var Viz1 = React.createClass({
|
|
render: () => <table><FancyRow /></table>,
|
|
});
|
|
var App1 = React.createClass({
|
|
render: () => <Viz1 />,
|
|
});
|
|
ReactTestUtils.renderIntoDocument(<App1 />);
|
|
expect(console.error.calls.count()).toBe(1);
|
|
expect(console.error.calls.argsFor(0)[0]).toContain(
|
|
'See Viz1 > table > FancyRow > Row > tr.'
|
|
);
|
|
|
|
var Viz2 = React.createClass({
|
|
render: () => <FancyTable><FancyRow /></FancyTable>,
|
|
});
|
|
var App2 = React.createClass({
|
|
render: () => <Viz2 />,
|
|
});
|
|
ReactTestUtils.renderIntoDocument(<App2 />);
|
|
expect(console.error.calls.count()).toBe(2);
|
|
expect(console.error.calls.argsFor(1)[0]).toContain(
|
|
'See Viz2 > FancyTable > Table > table > FancyRow > Row > tr.'
|
|
);
|
|
|
|
ReactTestUtils.renderIntoDocument(<FancyTable><FancyRow /></FancyTable>);
|
|
expect(console.error.calls.count()).toBe(3);
|
|
expect(console.error.calls.argsFor(2)[0]).toContain(
|
|
'See FancyTable > Table > table > FancyRow > Row > tr.'
|
|
);
|
|
|
|
ReactTestUtils.renderIntoDocument(<table><FancyRow /></table>);
|
|
expect(console.error.calls.count()).toBe(4);
|
|
expect(console.error.calls.argsFor(3)[0]).toContain(
|
|
'See table > FancyRow > Row > tr.'
|
|
);
|
|
|
|
ReactTestUtils.renderIntoDocument(<FancyTable><tr /></FancyTable>);
|
|
expect(console.error.calls.count()).toBe(5);
|
|
expect(console.error.calls.argsFor(4)[0]).toContain(
|
|
'See FancyTable > Table > table > tr.'
|
|
);
|
|
|
|
var Link = React.createClass({
|
|
render: function() {
|
|
return <a>{this.props.children}</a>;
|
|
},
|
|
});
|
|
ReactTestUtils.renderIntoDocument(<Link><div><Link /></div></Link>);
|
|
expect(console.error.calls.count()).toBe(6);
|
|
expect(console.error.calls.argsFor(5)[0]).toContain(
|
|
'See Link > a > ... > Link > a.'
|
|
);
|
|
});
|
|
|
|
it('should warn about incorrect casing on properties (ssr)', function() {
|
|
spyOn(console, 'error');
|
|
ReactDOMServer.renderToString(React.createElement('input', {type: 'text', tabindex: '1'}));
|
|
expect(console.error.calls.count()).toBe(1);
|
|
expect(console.error.calls.argsFor(0)[0]).toContain('tabIndex');
|
|
});
|
|
|
|
it('should warn about incorrect casing on event handlers (ssr)', function() {
|
|
spyOn(console, 'error');
|
|
ReactDOMServer.renderToString(React.createElement('input', {type: 'text', onclick: '1'}));
|
|
ReactDOMServer.renderToString(React.createElement('input', {type: 'text', onKeydown: '1'}));
|
|
expect(console.error.calls.count()).toBe(2);
|
|
expect(console.error.calls.argsFor(0)[0]).toContain('onClick');
|
|
expect(console.error.calls.argsFor(1)[0]).toContain('onKeyDown');
|
|
});
|
|
|
|
it('should warn about incorrect casing on properties', function() {
|
|
spyOn(console, 'error');
|
|
ReactTestUtils.renderIntoDocument(React.createElement('input', {type: 'text', tabindex: '1'}));
|
|
expect(console.error.calls.count()).toBe(1);
|
|
expect(console.error.calls.argsFor(0)[0]).toContain('tabIndex');
|
|
});
|
|
|
|
it('should warn about incorrect casing on event handlers', function() {
|
|
spyOn(console, 'error');
|
|
ReactTestUtils.renderIntoDocument(React.createElement('input', {type: 'text', onclick: '1'}));
|
|
ReactTestUtils.renderIntoDocument(React.createElement('input', {type: 'text', onKeydown: '1'}));
|
|
expect(console.error.calls.count()).toBe(2);
|
|
expect(console.error.calls.argsFor(0)[0]).toContain('onClick');
|
|
expect(console.error.calls.argsFor(1)[0]).toContain('onKeyDown');
|
|
});
|
|
|
|
it('should warn about class', function() {
|
|
spyOn(console, 'error');
|
|
ReactDOMServer.renderToString(React.createElement('div', {class: 'muffins'}));
|
|
expect(console.error.calls.count()).toBe(1);
|
|
expect(console.error.calls.argsFor(0)[0]).toContain('className');
|
|
});
|
|
|
|
it('should warn about props that are no longer supported', function() {
|
|
spyOn(console, 'error');
|
|
ReactTestUtils.renderIntoDocument(<div />);
|
|
expect(console.error.calls.count()).toBe(0);
|
|
|
|
ReactTestUtils.renderIntoDocument(<div onFocusIn={() => {}} />);
|
|
expect(console.error.calls.count()).toBe(1);
|
|
|
|
ReactTestUtils.renderIntoDocument(<div onFocusOut={() => {}} />);
|
|
expect(console.error.calls.count()).toBe(2);
|
|
});
|
|
|
|
it('gives source code refs for unknown prop warning', function() {
|
|
spyOn(console, 'error');
|
|
ReactDOMServer.renderToString(<div class="paladin"/>);
|
|
ReactDOMServer.renderToString(<input type="text" onclick="1"/>);
|
|
expect(console.error.calls.count()).toBe(2);
|
|
expect(
|
|
normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])
|
|
).toBe(
|
|
'Warning: Unknown DOM property class. Did you mean className?\n in div (at **)'
|
|
);
|
|
expect(
|
|
normalizeCodeLocInfo(console.error.calls.argsFor(1)[0])
|
|
).toBe(
|
|
'Warning: Unknown event handler property onclick. Did you mean ' +
|
|
'`onClick`?\n in input (at **)'
|
|
);
|
|
});
|
|
|
|
it('gives source code refs for unknown prop warning for update render', function() {
|
|
spyOn(console, 'error');
|
|
var container = document.createElement('div');
|
|
|
|
ReactDOMServer.renderToString(<div className="paladin" />, container);
|
|
expect(console.error.calls.count()).toBe(0);
|
|
|
|
ReactDOMServer.renderToString(<div class="paladin" />, container);
|
|
expect(console.error.calls.count()).toBe(1);
|
|
expect(
|
|
normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])
|
|
).toBe(
|
|
'Warning: Unknown DOM property class. Did you mean className?\n in div (at **)'
|
|
);
|
|
|
|
});
|
|
|
|
it('gives source code refs for unknown prop warning for exact elements ', function() {
|
|
spyOn(console, 'error');
|
|
|
|
ReactDOMServer.renderToString(
|
|
<div className="foo1">
|
|
<div class="foo2"/>
|
|
<div onClick="foo3"/>
|
|
<div onclick="foo4"/>
|
|
<div className="foo5"/>
|
|
<div className="foo6"/>
|
|
</div>
|
|
);
|
|
|
|
expect(console.error.calls.count()).toBe(2);
|
|
|
|
expect(console.error.calls.argsFor(0)[0]).toContain('className');
|
|
var matches = console.error.calls.argsFor(0)[0].match(/.*\(.*:(\d+)\).*/);
|
|
var previousLine = matches[1];
|
|
|
|
expect(console.error.calls.argsFor(1)[0]).toContain('onClick');
|
|
matches = console.error.calls.argsFor(1)[0].match(/.*\(.*:(\d+)\).*/);
|
|
var currentLine = matches[1];
|
|
|
|
//verify line number has a proper relative difference,
|
|
//since hard coding the line number would make test too brittle
|
|
expect(parseInt(previousLine, 10) + 2).toBe(parseInt(currentLine, 10));
|
|
});
|
|
|
|
it('gives source code refs for unknown prop warning for exact elements in composition ', function() {
|
|
spyOn(console, 'error');
|
|
var container = document.createElement('div');
|
|
|
|
var Parent = React.createClass({
|
|
render: function() {
|
|
return <div><Child1 /><Child2 /><Child3 /><Child4 /></div>;
|
|
},
|
|
});
|
|
|
|
var Child1 = React.createClass({
|
|
render: function() {
|
|
return <div class="paladin">Child1</div>;
|
|
},
|
|
});
|
|
|
|
var Child2 = React.createClass({
|
|
render: function() {
|
|
return <div>Child2</div>;
|
|
},
|
|
});
|
|
|
|
var Child3 = React.createClass({
|
|
render: function() {
|
|
return <div onclick="1">Child3</div>;
|
|
},
|
|
});
|
|
|
|
var Child4 = React.createClass({
|
|
render: function() {
|
|
return <div>Child4</div>;
|
|
},
|
|
});
|
|
|
|
ReactDOMServer.renderToString(<Parent />, container);
|
|
|
|
expect(console.error.calls.count()).toBe(2);
|
|
|
|
expect(console.error.calls.argsFor(0)[0]).toContain('className');
|
|
var matches = console.error.calls.argsFor(0)[0].match(/.*\(.*:(\d+)\).*/);
|
|
var previousLine = matches[1];
|
|
|
|
expect(console.error.calls.argsFor(1)[0]).toContain('onClick');
|
|
matches = console.error.calls.argsFor(1)[0].match(/.*\(.*:(\d+)\).*/);
|
|
var currentLine = matches[1];
|
|
|
|
//verify line number has a proper relative difference,
|
|
//since hard coding the line number would make test too brittle
|
|
expect(parseInt(previousLine, 10) + 12).toBe(parseInt(currentLine, 10));
|
|
|
|
});
|
|
});
|
|
});
|