UNPKG

arkit

Version:

Visualises JavaScript, TypeScript and Flow codebases as meaningful and committable architecture diagrams

1,371 lines (1,198 loc) 93.3 kB
/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @emails react-core */ 'use strict'; describe('ReactDOMComponent', () => { let React; let ReactTestUtils; let ReactDOM; let ReactDOMServer; function normalizeCodeLocInfo(str) { return str && str.replace(/\(at .+?:\d+\)/g, '(at **)'); } beforeEach(() => { jest.resetModules(); React = require('react'); ReactDOM = require('react-dom'); ReactDOMServer = require('react-dom/server'); ReactTestUtils = require('react-dom/test-utils'); }); describe('updateDOM', () => { it('should handle className', () => { const 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', () => { const container = document.createElement('div'); ReactDOM.render(<div style={{}} />, container); const stubStyle = container.firstChild.style; // set initial style const 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.top).toEqual('2px'); expect(stubStyle.fontFamily).toEqual('Arial'); // reset the style to their default state const 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(''); }); it('should not update styles when mutating a proxy style object', () => { const styleStore = { display: 'none', fontFamily: 'Arial', lineHeight: 1.2, }; // We use a proxy style object so that we can mutate it even if it is // frozen in DEV. const styles = { get display() { return styleStore.display; }, set display(v) { styleStore.display = v; }, get fontFamily() { return styleStore.fontFamily; }, set fontFamily(v) { styleStore.fontFamily = v; }, get lineHeight() { return styleStore.lineHeight; }, set lineHeight(v) { styleStore.lineHeight = v; }, }; const container = document.createElement('div'); ReactDOM.render(<div style={styles} />, container); const 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('none'); expect(stubStyle.fontFamily).toEqual('Arial'); expect(stubStyle.lineHeight).toEqual('1.2'); styles.fontFamily = 'Helvetica'; ReactDOM.render(<div style={styles} />, container); expect(stubStyle.display).toEqual('none'); expect(stubStyle.fontFamily).toEqual('Arial'); expect(stubStyle.lineHeight).toEqual('1.2'); styles.lineHeight = 0.5; ReactDOM.render(<div style={styles} />, container); expect(stubStyle.display).toEqual('none'); expect(stubStyle.fontFamily).toEqual('Arial'); expect(stubStyle.lineHeight).toEqual('1.2'); ReactDOM.render(<div style={undefined} />, container); expect(stubStyle.display).toBe(''); expect(stubStyle.fontFamily).toBe(''); expect(stubStyle.lineHeight).toBe(''); }); it('should throw when mutating style objects', () => { const style = {border: '1px solid black'}; class App extends React.Component { state = {style: style}; render() { return <div style={this.state.style}>asd</div>; } } ReactTestUtils.renderIntoDocument(<App />); if (__DEV__) { expect(() => (style.position = 'absolute')).toThrow(); } }); it('should warn for unknown prop', () => { const container = document.createElement('div'); expect(() => ReactDOM.render(<div foo={() => {}} />, container), ).toWarnDev( 'Warning: Invalid value for prop `foo` on <div> tag. Either remove it ' + 'from the element, or pass a string or number value to keep ' + 'it in the DOM. For details, see https://fb.me/react-attribute-behavior' + '\n in div (at **)', ); }); it('should group multiple unknown prop warnings together', () => { const container = document.createElement('div'); expect(() => ReactDOM.render(<div foo={() => {}} baz={() => {}} />, container), ).toWarnDev( 'Warning: Invalid values for props `foo`, `baz` on <div> tag. Either remove ' + 'them from the element, or pass a string or number value to keep ' + 'them in the DOM. For details, see https://fb.me/react-attribute-behavior' + '\n in div (at **)', ); }); it('should warn for onDblClick prop', () => { const container = document.createElement('div'); expect(() => ReactDOM.render(<div onDblClick={() => {}} />, container), ).toWarnDev( 'Warning: Invalid event handler property `onDblClick`. Did you mean `onDoubleClick`?\n in div (at **)', ); }); it('should warn for unknown string event handlers', () => { const container = document.createElement('div'); expect(() => ReactDOM.render(<div onUnknown="alert(&quot;hack&quot;)" />, container), ).toWarnDev( 'Warning: Unknown event handler property `onUnknown`. It will be ignored.\n in div (at **)', ); expect(container.firstChild.hasAttribute('onUnknown')).toBe(false); expect(container.firstChild.onUnknown).toBe(undefined); expect(() => ReactDOM.render(<div onunknown="alert(&quot;hack&quot;)" />, container), ).toWarnDev( 'Warning: Unknown event handler property `onunknown`. It will be ignored.\n in div (at **)', ); expect(container.firstChild.hasAttribute('onunknown')).toBe(false); expect(container.firstChild.onunknown).toBe(undefined); expect(() => ReactDOM.render( <div on-unknown="alert(&quot;hack&quot;)" />, container, ), ).toWarnDev( 'Warning: Unknown event handler property `on-unknown`. It will be ignored.\n in div (at **)', ); expect(container.firstChild.hasAttribute('on-unknown')).toBe(false); expect(container.firstChild['on-unknown']).toBe(undefined); }); it('should warn for unknown function event handlers', () => { const container = document.createElement('div'); expect(() => ReactDOM.render(<div onUnknown={function() {}} />, container), ).toWarnDev( 'Warning: Unknown event handler property `onUnknown`. It will be ignored.\n in div (at **)', ); expect(container.firstChild.hasAttribute('onUnknown')).toBe(false); expect(container.firstChild.onUnknown).toBe(undefined); expect(() => ReactDOM.render(<div onunknown={function() {}} />, container), ).toWarnDev( 'Warning: Unknown event handler property `onunknown`. It will be ignored.\n in div (at **)', ); expect(container.firstChild.hasAttribute('onunknown')).toBe(false); expect(container.firstChild.onunknown).toBe(undefined); expect(() => ReactDOM.render(<div on-unknown={function() {}} />, container), ).toWarnDev( 'Warning: Unknown event handler property `on-unknown`. It will be ignored.\n in div (at **)', ); expect(container.firstChild.hasAttribute('on-unknown')).toBe(false); expect(container.firstChild['on-unknown']).toBe(undefined); }); it('should warn for badly cased React attributes', () => { const container = document.createElement('div'); expect(() => ReactDOM.render(<div CHILDREN="5" />, container)).toWarnDev( 'Warning: Invalid DOM property `CHILDREN`. Did you mean `children`?\n in div (at **)', ); expect(container.firstChild.getAttribute('CHILDREN')).toBe('5'); }); it('should not warn for "0" as a unitless style value', () => { class Component extends React.Component { render() { return <div style={{margin: '0'}} />; } } ReactTestUtils.renderIntoDocument(<Component />); }); it('should warn nicely about NaN in style', () => { const style = {fontSize: NaN}; const div = document.createElement('div'); expect(() => ReactDOM.render(<span style={style} />, div)).toWarnDev( 'Warning: `NaN` is an invalid value for the `fontSize` css style property.' + '\n in span (at **)', ); ReactDOM.render(<span style={style} />, div); }); it('should update styles if initially null', () => { let styles = null; const container = document.createElement('div'); ReactDOM.render(<div style={styles} />, container); const 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', () => { let styles = null; const container = document.createElement('div'); ReactDOM.render(<div style={styles} />, container); styles = {display: 'block'}; const 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 allow named slot projection on both web components and regular DOM elements', () => { const container = document.createElement('div'); ReactDOM.render( <my-component> <my-second-component slot="first" /> <button slot="second">Hello</button> </my-component>, container, ); const lightDOM = container.firstChild.childNodes; expect(lightDOM[0].getAttribute('slot')).toBe('first'); expect(lightDOM[1].getAttribute('slot')).toBe('second'); }); it('should skip reserved props on web components', () => { const container = document.createElement('div'); ReactDOM.render( <my-component children={['foo']} suppressContentEditableWarning={true} suppressHydrationWarning={true} />, container, ); expect(container.firstChild.hasAttribute('children')).toBe(false); expect( container.firstChild.hasAttribute('suppressContentEditableWarning'), ).toBe(false); expect( container.firstChild.hasAttribute('suppressHydrationWarning'), ).toBe(false); ReactDOM.render( <my-component children={['bar']} suppressContentEditableWarning={false} suppressHydrationWarning={false} />, container, ); expect(container.firstChild.hasAttribute('children')).toBe(false); expect( container.firstChild.hasAttribute('suppressContentEditableWarning'), ).toBe(false); expect( container.firstChild.hasAttribute('suppressHydrationWarning'), ).toBe(false); }); it('should skip dangerouslySetInnerHTML on web components', () => { const 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 render null and undefined as empty but print other falsy values', () => { const container = document.createElement('div'); ReactDOM.render( <div dangerouslySetInnerHTML={{__html: 'textContent'}} />, container, ); expect(container.textContent).toEqual('textContent'); ReactDOM.render(<div dangerouslySetInnerHTML={{__html: 0}} />, container); expect(container.textContent).toEqual('0'); ReactDOM.render( <div dangerouslySetInnerHTML={{__html: false}} />, container, ); expect(container.textContent).toEqual('false'); ReactDOM.render( <div dangerouslySetInnerHTML={{__html: ''}} />, container, ); expect(container.textContent).toEqual(''); ReactDOM.render( <div dangerouslySetInnerHTML={{__html: null}} />, container, ); expect(container.textContent).toEqual(''); ReactDOM.render( <div dangerouslySetInnerHTML={{__html: undefined}} />, container, ); expect(container.textContent).toEqual(''); }); it('should remove attributes', () => { const 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', () => { const 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 not set null/undefined attributes', () => { const container = document.createElement('div'); // Initial render. ReactDOM.render(<img src={null} data-foo={undefined} />, container); const node = container.firstChild; expect(node.hasAttribute('src')).toBe(false); expect(node.hasAttribute('data-foo')).toBe(false); // Update in one direction. ReactDOM.render(<img src={undefined} data-foo={null} />, container); expect(node.hasAttribute('src')).toBe(false); expect(node.hasAttribute('data-foo')).toBe(false); // Update in another direction. ReactDOM.render(<img src={null} data-foo={undefined} />, container); expect(node.hasAttribute('src')).toBe(false); expect(node.hasAttribute('data-foo')).toBe(false); // Removal. ReactDOM.render(<img />, container); expect(node.hasAttribute('src')).toBe(false); expect(node.hasAttribute('data-foo')).toBe(false); // Addition. ReactDOM.render(<img src={undefined} data-foo={null} />, container); expect(node.hasAttribute('src')).toBe(false); expect(node.hasAttribute('data-foo')).toBe(false); }); it('should apply React-specific aliases to HTML elements', () => { const container = document.createElement('div'); ReactDOM.render(<form acceptCharset="foo" />, container); const node = container.firstChild; // Test attribute initialization. expect(node.getAttribute('accept-charset')).toBe('foo'); expect(node.hasAttribute('acceptCharset')).toBe(false); // Test attribute update. ReactDOM.render(<form acceptCharset="boo" />, container); expect(node.getAttribute('accept-charset')).toBe('boo'); expect(node.hasAttribute('acceptCharset')).toBe(false); // Test attribute removal by setting to null. ReactDOM.render(<form acceptCharset={null} />, container); expect(node.hasAttribute('accept-charset')).toBe(false); expect(node.hasAttribute('acceptCharset')).toBe(false); // Restore. ReactDOM.render(<form acceptCharset="foo" />, container); expect(node.getAttribute('accept-charset')).toBe('foo'); expect(node.hasAttribute('acceptCharset')).toBe(false); // Test attribute removal by setting to undefined. ReactDOM.render(<form acceptCharset={undefined} />, container); expect(node.hasAttribute('accept-charset')).toBe(false); expect(node.hasAttribute('acceptCharset')).toBe(false); // Restore. ReactDOM.render(<form acceptCharset="foo" />, container); expect(node.getAttribute('accept-charset')).toBe('foo'); expect(node.hasAttribute('acceptCharset')).toBe(false); // Test attribute removal. ReactDOM.render(<form />, container); expect(node.hasAttribute('accept-charset')).toBe(false); expect(node.hasAttribute('acceptCharset')).toBe(false); }); it('should apply React-specific aliases to SVG elements', () => { const container = document.createElement('div'); ReactDOM.render(<svg arabicForm="foo" />, container); const node = container.firstChild; // Test attribute initialization. expect(node.getAttribute('arabic-form')).toBe('foo'); expect(node.hasAttribute('arabicForm')).toBe(false); // Test attribute update. ReactDOM.render(<svg arabicForm="boo" />, container); expect(node.getAttribute('arabic-form')).toBe('boo'); expect(node.hasAttribute('arabicForm')).toBe(false); // Test attribute removal by setting to null. ReactDOM.render(<svg arabicForm={null} />, container); expect(node.hasAttribute('arabic-form')).toBe(false); expect(node.hasAttribute('arabicForm')).toBe(false); // Restore. ReactDOM.render(<svg arabicForm="foo" />, container); expect(node.getAttribute('arabic-form')).toBe('foo'); expect(node.hasAttribute('arabicForm')).toBe(false); // Test attribute removal by setting to undefined. ReactDOM.render(<svg arabicForm={undefined} />, container); expect(node.hasAttribute('arabic-form')).toBe(false); expect(node.hasAttribute('arabicForm')).toBe(false); // Restore. ReactDOM.render(<svg arabicForm="foo" />, container); expect(node.getAttribute('arabic-form')).toBe('foo'); expect(node.hasAttribute('arabicForm')).toBe(false); // Test attribute removal. ReactDOM.render(<svg />, container); expect(node.hasAttribute('arabic-form')).toBe(false); expect(node.hasAttribute('arabicForm')).toBe(false); }); it('should properly update custom attributes on custom elements', () => { const container = document.createElement('div'); ReactDOM.render(<some-custom-element foo="bar" />, container); ReactDOM.render(<some-custom-element bar="buzz" />, container); const node = container.firstChild; expect(node.hasAttribute('foo')).toBe(false); expect(node.getAttribute('bar')).toBe('buzz'); }); it('should not apply React-specific aliases to custom elements', () => { const container = document.createElement('div'); ReactDOM.render(<some-custom-element arabicForm="foo" />, container); const node = container.firstChild; // Should not get transformed to arabic-form as SVG would be. expect(node.getAttribute('arabicForm')).toBe('foo'); expect(node.hasAttribute('arabic-form')).toBe(false); // Test attribute update. ReactDOM.render(<some-custom-element arabicForm="boo" />, container); expect(node.getAttribute('arabicForm')).toBe('boo'); // Test attribute removal and addition. ReactDOM.render(<some-custom-element acceptCharset="buzz" />, container); // Verify the previous attribute was removed. expect(node.hasAttribute('arabicForm')).toBe(false); // Should not get transformed to accept-charset as HTML would be. expect(node.getAttribute('acceptCharset')).toBe('buzz'); expect(node.hasAttribute('accept-charset')).toBe(false); }); it('should clear a single style prop when changing `style`', () => { let styles = {display: 'none', color: 'red'}; const container = document.createElement('div'); ReactDOM.render(<div style={styles} />, container); const 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 for regular DOM (SSR)', () => { expect(() => { for (let i = 0; i < 3; i++) { const element1 = React.createElement( 'div', {'blah" onclick="beevil" noise="hi': 'selected'}, null, ); const element2 = React.createElement( 'div', {'></div><script>alert("hi")</script>': 'selected'}, null, ); let result1 = ReactDOMServer.renderToString(element1); let result2 = ReactDOMServer.renderToString(element2); expect(result1.toLowerCase()).not.toContain('onclick'); expect(result2.toLowerCase()).not.toContain('script'); } }).toWarnDev([ 'Warning: Invalid attribute name: `blah" onclick="beevil" noise="hi`', 'Warning: Invalid attribute name: `></div><script>alert("hi")</script>`', ]); }); it('should reject attribute key injection attack on markup for custom elements (SSR)', () => { expect(() => { for (let i = 0; i < 3; i++) { const element1 = React.createElement( 'x-foo-component', {'blah" onclick="beevil" noise="hi': 'selected'}, null, ); const element2 = React.createElement( 'x-foo-component', {'></x-foo-component><script>alert("hi")</script>': 'selected'}, null, ); let result1 = ReactDOMServer.renderToString(element1); let result2 = ReactDOMServer.renderToString(element2); expect(result1.toLowerCase()).not.toContain('onclick'); expect(result2.toLowerCase()).not.toContain('script'); } }).toWarnDev([ 'Warning: Invalid attribute name: `blah" onclick="beevil" noise="hi`', 'Warning: Invalid attribute name: `></x-foo-component><script>alert("hi")</script>`', ]); }); it('should reject attribute key injection attack on mount for regular DOM', () => { expect(() => { for (let i = 0; i < 3; i++) { const container = document.createElement('div'); ReactDOM.render( React.createElement( 'div', {'blah" onclick="beevil" noise="hi': 'selected'}, null, ), container, ); expect(container.firstChild.attributes.length).toBe(0); ReactDOM.unmountComponentAtNode(container); ReactDOM.render( React.createElement( 'div', {'></div><script>alert("hi")</script>': 'selected'}, null, ), container, ); expect(container.firstChild.attributes.length).toBe(0); } }).toWarnDev([ 'Warning: Invalid attribute name: `blah" onclick="beevil" noise="hi`', 'Warning: Invalid attribute name: `></div><script>alert("hi")</script>`', ]); }); it('should reject attribute key injection attack on mount for custom elements', () => { expect(() => { for (let i = 0; i < 3; i++) { const container = document.createElement('div'); ReactDOM.render( React.createElement( 'x-foo-component', {'blah" onclick="beevil" noise="hi': 'selected'}, null, ), container, ); expect(container.firstChild.attributes.length).toBe(0); ReactDOM.unmountComponentAtNode(container); ReactDOM.render( React.createElement( 'x-foo-component', {'></x-foo-component><script>alert("hi")</script>': 'selected'}, null, ), container, ); expect(container.firstChild.attributes.length).toBe(0); } }).toWarnDev([ 'Warning: Invalid attribute name: `blah" onclick="beevil" noise="hi`', 'Warning: Invalid attribute name: `></x-foo-component><script>alert("hi")</script>`', ]); }); it('should reject attribute key injection attack on update for regular DOM', () => { expect(() => { for (let i = 0; i < 3; i++) { const container = document.createElement('div'); const beforeUpdate = React.createElement('div', {}, null); ReactDOM.render(beforeUpdate, container); ReactDOM.render( React.createElement( 'div', {'blah" onclick="beevil" noise="hi': 'selected'}, null, ), container, ); expect(container.firstChild.attributes.length).toBe(0); ReactDOM.render( React.createElement( 'div', {'></div><script>alert("hi")</script>': 'selected'}, null, ), container, ); expect(container.firstChild.attributes.length).toBe(0); } }).toWarnDev([ 'Warning: Invalid attribute name: `blah" onclick="beevil" noise="hi`', 'Warning: Invalid attribute name: `></div><script>alert("hi")</script>`', ]); }); it('should reject attribute key injection attack on update for custom elements', () => { expect(() => { for (let i = 0; i < 3; i++) { const container = document.createElement('div'); const beforeUpdate = React.createElement('x-foo-component', {}, null); ReactDOM.render(beforeUpdate, container); ReactDOM.render( React.createElement( 'x-foo-component', {'blah" onclick="beevil" noise="hi': 'selected'}, null, ), container, ); expect(container.firstChild.attributes.length).toBe(0); ReactDOM.render( React.createElement( 'x-foo-component', {'></x-foo-component><script>alert("hi")</script>': 'selected'}, null, ), container, ); expect(container.firstChild.attributes.length).toBe(0); } }).toWarnDev([ 'Warning: Invalid attribute name: `blah" onclick="beevil" noise="hi`', 'Warning: Invalid attribute name: `></x-foo-component><script>alert("hi")</script>`', ]); }); it('should update arbitrary attributes for tags containing dashes', () => { const container = document.createElement('div'); const beforeUpdate = React.createElement('x-foo-component', {}, null); ReactDOM.render(beforeUpdate, container); const 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`', () => { const styles = {display: 'none', color: 'red'}; const container = document.createElement('div'); ReactDOM.render(<div style={styles} />, container); const 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', () => { const container = document.createElement('div'); const styles = {color: 'red'}; ReactDOM.render(<div style={styles} />, container); ReactDOM.render(<div />, container); ReactDOM.render(<div style={styles} />, container); const stubStyle = container.firstChild.style; expect(stubStyle.color).toEqual('red'); }); it('should not reset innerHTML for when children is null', () => { const container = document.createElement('div'); ReactDOM.render(<div />, container); container.firstChild.innerHTML = 'bonjour'; expect(container.firstChild.innerHTML).toEqual('bonjour'); ReactDOM.render(<div />, container); expect(container.firstChild.innerHTML).toEqual('bonjour'); }); it('should reset innerHTML when switching from a direct text child to an empty child', () => { const transitionToValues = [null, undefined, false]; transitionToValues.forEach(transitionToValue => { const container = document.createElement('div'); ReactDOM.render(<div>bonjour</div>, container); expect(container.firstChild.innerHTML).toEqual('bonjour'); ReactDOM.render(<div>{transitionToValue}</div>, container); expect(container.firstChild.innerHTML).toEqual(''); }); }); it('should empty element when removing innerHTML', () => { const 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', () => { const 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', () => { const 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', () => { const 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', () => { const 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', () => { const container = document.createElement('div'); ReactDOM.render(<div id="" />, container); const node = container.firstChild; const nodeSetAttribute = node.setAttribute; node.setAttribute = jest.fn(); node.setAttribute.mockImplementation(nodeSetAttribute); const nodeRemoveAttribute = node.removeAttribute; node.removeAttribute = jest.fn(); node.removeAttribute.mockImplementation(nodeRemoveAttribute); ReactDOM.render(<div id="" />, container); expect(node.setAttribute).toHaveBeenCalledTimes(0); expect(node.removeAttribute).toHaveBeenCalledTimes(0); ReactDOM.render(<div id="foo" />, container); expect(node.setAttribute).toHaveBeenCalledTimes(1); expect(node.removeAttribute).toHaveBeenCalledTimes(0); ReactDOM.render(<div id="foo" />, container); expect(node.setAttribute).toHaveBeenCalledTimes(1); expect(node.removeAttribute).toHaveBeenCalledTimes(0); ReactDOM.render(<div />, container); expect(node.setAttribute).toHaveBeenCalledTimes(1); expect(node.removeAttribute).toHaveBeenCalledTimes(1); ReactDOM.render(<div id="" />, container); expect(node.setAttribute).toHaveBeenCalledTimes(2); expect(node.removeAttribute).toHaveBeenCalledTimes(1); ReactDOM.render(<div />, container); expect(node.setAttribute).toHaveBeenCalledTimes(2); expect(node.removeAttribute).toHaveBeenCalledTimes(2); }); it('should not incur unnecessary DOM mutations for string properties', () => { const container = document.createElement('div'); ReactDOM.render(<div value="" />, container); const node = container.firstChild; const nodeValueSetter = jest.fn(); const oldSetAttribute = node.setAttribute.bind(node); node.setAttribute = function(key, value) { oldSetAttribute(key, value); nodeValueSetter(key, value); }; ReactDOM.render(<div value="foo" />, container); expect(nodeValueSetter).toHaveBeenCalledTimes(1); ReactDOM.render(<div value="foo" />, container); expect(nodeValueSetter).toHaveBeenCalledTimes(1); ReactDOM.render(<div />, container); expect(nodeValueSetter).toHaveBeenCalledTimes(1); ReactDOM.render(<div value={null} />, container); expect(nodeValueSetter).toHaveBeenCalledTimes(1); ReactDOM.render(<div value="" />, container); expect(nodeValueSetter).toHaveBeenCalledTimes(2); ReactDOM.render(<div />, container); expect(nodeValueSetter).toHaveBeenCalledTimes(2); }); it('should not incur unnecessary DOM mutations for boolean properties', () => { const container = document.createElement('div'); ReactDOM.render(<div checked={true} />, container); const node = container.firstChild; let nodeValue = true; const 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).toHaveBeenCalledTimes(0); ReactDOM.render(<div />, container); expect(nodeValueSetter).toHaveBeenCalledTimes(1); ReactDOM.render(<div checked={false} />, container); expect(nodeValueSetter).toHaveBeenCalledTimes(2); ReactDOM.render(<div checked={true} />, container); expect(nodeValueSetter).toHaveBeenCalledTimes(3); }); it('should ignore attribute whitelist for elements with the "is" attribute', () => { const container = document.createElement('div'); ReactDOM.render(<button is="test" cowabunga="chevynova" />, container); expect(container.firstChild.hasAttribute('cowabunga')).toBe(true); }); it('should warn about non-string "is" attribute', () => { const container = document.createElement('div'); expect(() => ReactDOM.render(<button is={function() {}} />, container), ).toWarnDev( 'Received a `function` for a string attribute `is`. If this is expected, cast ' + 'the value to a string.', ); }); it('should not update when switching between null/undefined', () => { const container = document.createElement('div'); const node = ReactDOM.render(<div />, container); const setter = jest.fn(); node.setAttribute = setter; ReactDOM.render(<div dir={null} />, container); ReactDOM.render(<div dir={undefined} />, container); ReactDOM.render(<div />, container); expect(setter).toHaveBeenCalledTimes(0); ReactDOM.render(<div dir="ltr" />, container); expect(setter).toHaveBeenCalledTimes(1); }); it('handles multiple child updates without interference', () => { // 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. const 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 quoteRegexp(str) { return (str + '').replace(/([.?*+\^$\[\]\\(){}|-])/g, '\\$1'); } function toHaveAttribute(actual, expected) { const [attr, value] = expected; let re = '(?:^|\\s)' + attr + '=[\\\'"]'; if (typeof value !== 'undefined') { re += quoteRegexp(value) + '[\\\'"]'; } return new RegExp(re).test(actual); } function genMarkup(props) { return ReactDOMServer.renderToString(<div {...props} />); } it('should generate the correct markup with className', () => { expect(toHaveAttribute(genMarkup({className: 'a'}), ['class', 'a'])); expect(toHaveAttribute(genMarkup({className: 'a b'}), ['class', 'a b'])); expect(toHaveAttribute(genMarkup({className: ''}), ['class', ''])); }); it('should escape style names and values', () => { expect( toHaveAttribute( genMarkup({ style: {'b&ckground': '<3'}, }), ['style', 'b&amp;ckground:&lt;3;'], ), ); }); }); describe('createContentMarkup', () => { function quoteRegexp(str) { return (str + '').replace(/([.?*+\^$\[\]\\(){}|-])/g, '\\$1'); } function genMarkup(props) { return ReactDOMServer.renderToString(<div {...props} />); } function toHaveInnerhtml(actual, expected) { const re = '^' + quoteRegexp(expected) + '$'; return new RegExp(re).test(actual); } it('should handle dangerouslySetInnerHTML', () => { const innerHTML = {__html: 'testContent'}; expect( toHaveInnerhtml( genMarkup({dangerouslySetInnerHTML: innerHTML}), 'testContent', ), ); }); }); describe('mountComponent', () => { let mountComponent; beforeEach(() => { mountComponent = function(props) { const container = document.createElement('div'); ReactDOM.render(<div {...props} />, container); }; }); it('should work error event on <source> element', () => { spyOnDevAndProd(console, 'log'); const container = document.createElement('div'); ReactDOM.render( <video> <source src="http://example.org/video" type="video/mp4" onError={e => console.log('onError called')} /> </video>, container, ); const errorEvent = document.createEvent('Event'); errorEvent.initEvent('error', false, false); container.getElementsByTagName('source')[0].dispatchEvent(errorEvent); if (__DEV__) { expect(console.log).toHaveBeenCalledTimes(1); expect(console.log.calls.argsFor(0)[0]).toContain('onError called'); } }); it('should not duplicate uppercased selfclosing tags', () => { class Container extends React.Component { render() { return React.createElement('BR', null); } } let returnedValue; expect(() => { returnedValue = ReactDOMServer.renderToString(<Container />); }).toWarnDev( '<BR /> is using incorrect casing. ' + 'Use PascalCase for React components, ' + 'or lowercase for HTML elements.', ); expect(returnedValue).not.toContain('</BR>'); }); it('should warn on upper case HTML tags, not SVG nor custom tags', () => { ReactTestUtils.renderIntoDocument( React.createElement('svg', null, React.createElement('PATH')), ); ReactTestUtils.renderIntoDocument(React.createElement('CUSTOM-TAG')); expect(() => ReactTestUtils.renderIntoDocument(React.createElement('IMG')), ).toWarnDev( '<IMG /> is using incorrect casing. ' + 'Use PascalCase for React components, ' + 'or lowercase for HTML elements.', ); }); it('should warn on props reserved for future use', () => { expect(() => ReactTestUtils.renderIntoDocument(<div aria="hello" />), ).toWarnDev( 'The `aria` attribute is reserved for future use in React. ' + 'Pass individual `aria-` attributes instead.', ); }); it('should warn if the tag is unrecognized', () => { let realToString; try { realToString = Object.prototype.toString; let wrappedToString = function() { // Emulate browser behavior which is missing in jsdom if (this instanceof window.HTMLUnknownElement) { return '[object HTMLUnknownElement]'; } // Special case! Read explanation below in the test. if (this instanceof window.HTMLTimeElement) { return '[object HTMLUnknownElement]'; } return realToString.apply(this, arguments); }; Object.prototype.toString = wrappedToString; // eslint-disable-line no-extend-native expect(() => ReactTestUtils.renderIntoDocument(<bar />)).toWarnDev( 'The tag <bar> is unrecognized in this browser', ); // Test deduplication expect(() => ReactTestUtils.renderIntoDocument(<foo />)).toWarnDev( 'The tag <foo> is unrecognized in this browser', ); ReactTestUtils.renderIntoDocument(<foo />); // This is a funny case. // Chrome is the only major browser not shipping <time>. But as of July // 2017 it intends to ship it due to widespread usage. We intentionally // *don't* warn for <time> even if it's unrecognized by Chrome because // it soon will be, and many apps have been using it anyway. ReactTestUtils.renderIntoDocument(<time />); // Corner case. Make sure out deduplication logic doesn't break with weird tag. expect(() => ReactTestUtils.renderIntoDocument(<hasOwnProperty />), ).toWarnDev([ '<hasOwnProperty /> is using incorrect casing. ' + 'Use PascalCase for React components, ' + 'or lowercase for HTML elements.', 'The tag <hasOwnProperty> is unrecognized in this browser', ]); } finally { Object.prototype.toString = realToString; // eslint-disable-line no-extend-native } }); it('should throw on children for void elements', () => { const container = document.createElement('div'); let caughtErr; try { ReactDOM.render(<input>children</input>, container); } catch (err) { caughtErr = err; } expect(caughtErr).not.toBe(undefined); expect(normalizeCodeLocInfo(caughtErr.message)).toContain( 'input is a void element tag and must neither have `children` nor ' + 'use `dangerouslySetInnerHTML`.' + (__DEV__ ? '\n in input (at **)' : ''), ); }); it('should throw on dangerouslySetInnerHTML for void elements', () => { const container = document.createElement('div'); let caughtErr; try { ReactDOM.render( <input dangerouslySetInnerHTML={{__html: 'content'}} />, container, ); } catch (err) { caughtErr = err; } expect(caughtErr).not.toBe(undefined); expect(normalizeCodeLocInfo(caughtErr.message)).toContain( 'input is a void element tag and must neither have `children` nor ' + 'use `dangerouslySetInnerHTML`.' + (__DEV__ ? '\n in input (at **)' : ''), ); }); it('should emit a warning once for a named custom component using shady DOM', () => { const defaultCreateElement = document.createElement.bind(document); try { document.createElement = element => { const container = defaultCreateElement(element); container.shadyRoot = {}; return container; }; class ShadyComponent extends React.Component { render() { return <polymer-component />; } } const node = document.createElement('div'); expect(() => ReactDOM.render(<ShadyComponent />, node)).toWarnDev( 'ShadyComponent is using shady DOM. Using shady DOM with React can ' + 'cause things to break subtly.', ); mountComponent({is: 'custom-shady-div2'}); } finally { document.createElement = defaultCreateElement; } }); it('should emit a warning once for an unnamed custom component using shady DOM', () => { const defaultCreateElement = document.createElement.bind(document); try { document.createElement = element => { const container = defaultCreateElement(element); container.shadyRoot = {}; return container; }; expect(() => mountComponent({is: 'custom-shady-div'})).toWarnDev( 'A component is using shady DOM. Using shady DOM with React can ' + 'cause things to break subtly.', ); // No additional warnings are expected mountComponent({is: 'custom-shady-div2'}); } finally { document.createElement = defaultCreateElement; } }); it('should treat menuitem as a void element but still create the closing tag', () => { // menuitem is not implemented in jsdom, so this triggers the unknown warning error const container = document.createElement('div'); const returnedValue = ReactDOMServer.renderToString( <menu> <menuitem /> </menu>, ); expect(returnedValue).toContain('</menuitem>'); expect(function() { expect(() => { ReactDOM.render( <menu> <menuitem>children</menuitem> </menu>, container, ); }).toWarnDev('The tag <menuitem> is unrecognized in this browser.'); }).toThrowError( 'menuitem is a void element tag and must neither have `children` nor use ' + '`dangerouslySetInnerHTML`.', ); }); it('should validate against multiple children props', () => { expect(function() { mountComponent({children: '', dangerouslySetInnerHTML: ''}); }).toThrowError( 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.', ); }); it('should validate against use of innerHTML', () => { expect(() => mountComponent({innerHTML: '<span>Hi Jim!</span>'}), ).toWarnDev('Directly setting property `innerHTML` is not permitted. '); }); it('should validate against use of innerHTML without case sensitivity', () => { expect(() => mountComponent({innerhtml: '<span>Hi Jim!</span>'}), ).toWarnDev('Directly setting property `innerHTML` is not permitted. '); }); it('should validate use of dangerouslySetInnerHTML', () => { 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', () => { 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}', () => { expect(function() { mountComponent({dangerouslySetInnerHTML: {__html: null}}); }).not.toThrow(); }); it('should warn about contentEditable and children', () => { expect(() => mountComponent({contentEditable: true, children: ''}), ).toWarnDev(