UNPKG

arkit

Version:

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

1,274 lines (1,115 loc) 37.4 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'; const React = require('react'); const ReactDOM = require('react-dom'); const PropTypes = require('prop-types'); describe('ReactDOMFiber', () => { let container; beforeEach(() => { container = document.createElement('div'); document.body.appendChild(container); }); afterEach(() => { document.body.removeChild(container); container = null; }); it('should render strings as children', () => { const Box = ({value}) => <div>{value}</div>; ReactDOM.render(<Box value="foo" />, container); expect(container.textContent).toEqual('foo'); }); it('should render numbers as children', () => { const Box = ({value}) => <div>{value}</div>; ReactDOM.render(<Box value={10} />, container); expect(container.textContent).toEqual('10'); }); it('should be called a callback argument', () => { // mounting phase let called = false; ReactDOM.render(<div>Foo</div>, container, () => (called = true)); expect(called).toEqual(true); // updating phase called = false; ReactDOM.render(<div>Foo</div>, container, () => (called = true)); expect(called).toEqual(true); }); it('should call a callback argument when the same element is re-rendered', () => { class Foo extends React.Component { render() { return <div>Foo</div>; } } const element = <Foo />; // mounting phase let called = false; ReactDOM.render(element, container, () => (called = true)); expect(called).toEqual(true); // updating phase called = false; ReactDOM.unstable_batchedUpdates(() => { ReactDOM.render(element, container, () => (called = true)); }); expect(called).toEqual(true); }); it('should render a component returning strings directly from render', () => { const Text = ({value}) => value; ReactDOM.render(<Text value="foo" />, container); expect(container.textContent).toEqual('foo'); }); it('should render a component returning numbers directly from render', () => { const Text = ({value}) => value; ReactDOM.render(<Text value={10} />, container); expect(container.textContent).toEqual('10'); }); it('finds the DOM Text node of a string child', () => { class Text extends React.Component { render() { return this.props.value; } } let instance = null; ReactDOM.render( <Text value="foo" ref={ref => (instance = ref)} />, container, ); const textNode = ReactDOM.findDOMNode(instance); expect(textNode).toBe(container.firstChild); expect(textNode.nodeType).toBe(3); expect(textNode.nodeValue).toBe('foo'); }); it('finds the first child when a component returns a fragment', () => { class Fragment extends React.Component { render() { return [<div key="a" />, <span key="b" />]; } } let instance = null; ReactDOM.render(<Fragment ref={ref => (instance = ref)} />, container); expect(container.childNodes.length).toBe(2); const firstNode = ReactDOM.findDOMNode(instance); expect(firstNode).toBe(container.firstChild); expect(firstNode.tagName).toBe('DIV'); }); it('finds the first child even when fragment is nested', () => { class Wrapper extends React.Component { render() { return this.props.children; } } class Fragment extends React.Component { render() { return [ <Wrapper key="a"> <div /> </Wrapper>, <span key="b" />, ]; } } let instance = null; ReactDOM.render(<Fragment ref={ref => (instance = ref)} />, container); expect(container.childNodes.length).toBe(2); const firstNode = ReactDOM.findDOMNode(instance); expect(firstNode).toBe(container.firstChild); expect(firstNode.tagName).toBe('DIV'); }); it('finds the first child even when first child renders null', () => { class NullComponent extends React.Component { render() { return null; } } class Fragment extends React.Component { render() { return [<NullComponent key="a" />, <div key="b" />, <span key="c" />]; } } let instance = null; ReactDOM.render(<Fragment ref={ref => (instance = ref)} />, container); expect(container.childNodes.length).toBe(2); const firstNode = ReactDOM.findDOMNode(instance); expect(firstNode).toBe(container.firstChild); expect(firstNode.tagName).toBe('DIV'); }); it('renders an empty fragment', () => { const Div = () => <div />; const EmptyFragment = () => <React.Fragment />; const NonEmptyFragment = () => ( <React.Fragment> <Div /> </React.Fragment> ); ReactDOM.render(<EmptyFragment />, container); expect(container.firstChild).toBe(null); ReactDOM.render(<NonEmptyFragment />, container); expect(container.firstChild.tagName).toBe('DIV'); ReactDOM.render(<EmptyFragment />, container); expect(container.firstChild).toBe(null); ReactDOM.render(<Div />, container); expect(container.firstChild.tagName).toBe('DIV'); ReactDOM.render(<EmptyFragment />, container); expect(container.firstChild).toBe(null); }); let svgEls, htmlEls, mathEls; const expectSVG = {ref: el => svgEls.push(el)}; const expectHTML = {ref: el => htmlEls.push(el)}; const expectMath = {ref: el => mathEls.push(el)}; const usePortal = function(tree) { return ReactDOM.createPortal(tree, document.createElement('div')); }; const assertNamespacesMatch = function(tree) { let testContainer = document.createElement('div'); svgEls = []; htmlEls = []; mathEls = []; ReactDOM.render(tree, testContainer); svgEls.forEach(el => { expect(el.namespaceURI).toBe('http://www.w3.org/2000/svg'); }); htmlEls.forEach(el => { expect(el.namespaceURI).toBe('http://www.w3.org/1999/xhtml'); }); mathEls.forEach(el => { expect(el.namespaceURI).toBe('http://www.w3.org/1998/Math/MathML'); }); ReactDOM.unmountComponentAtNode(testContainer); expect(testContainer.innerHTML).toBe(''); }; it('should render one portal', () => { const portalContainer = document.createElement('div'); ReactDOM.render( <div>{ReactDOM.createPortal(<div>portal</div>, portalContainer)}</div>, container, ); expect(portalContainer.innerHTML).toBe('<div>portal</div>'); expect(container.innerHTML).toBe('<div></div>'); ReactDOM.unmountComponentAtNode(container); expect(portalContainer.innerHTML).toBe(''); expect(container.innerHTML).toBe(''); }); // TODO: remove in React 17 it('should support unstable_createPortal alias', () => { const portalContainer = document.createElement('div'); expect(() => ReactDOM.render( <div> {ReactDOM.unstable_createPortal(<div>portal</div>, portalContainer)} </div>, container, ), ).toLowPriorityWarnDev( 'The ReactDOM.unstable_createPortal() alias has been deprecated, ' + 'and will be removed in React 17+. Update your code to use ' + 'ReactDOM.createPortal() instead. It has the exact same API, ' + 'but without the "unstable_" prefix.', {withoutStack: true}, ); expect(portalContainer.innerHTML).toBe('<div>portal</div>'); expect(container.innerHTML).toBe('<div></div>'); ReactDOM.unmountComponentAtNode(container); expect(portalContainer.innerHTML).toBe(''); expect(container.innerHTML).toBe(''); }); it('should render many portals', () => { const portalContainer1 = document.createElement('div'); const portalContainer2 = document.createElement('div'); const ops = []; class Child extends React.Component { componentDidMount() { ops.push(`${this.props.name} componentDidMount`); } componentDidUpdate() { ops.push(`${this.props.name} componentDidUpdate`); } componentWillUnmount() { ops.push(`${this.props.name} componentWillUnmount`); } render() { return <div>{this.props.name}</div>; } } class Parent extends React.Component { componentDidMount() { ops.push(`Parent:${this.props.step} componentDidMount`); } componentDidUpdate() { ops.push(`Parent:${this.props.step} componentDidUpdate`); } componentWillUnmount() { ops.push(`Parent:${this.props.step} componentWillUnmount`); } render() { const {step} = this.props; return [ <Child key="a" name={`normal[0]:${step}`} />, ReactDOM.createPortal( <Child key="b" name={`portal1[0]:${step}`} />, portalContainer1, ), <Child key="c" name={`normal[1]:${step}`} />, ReactDOM.createPortal( [ <Child key="d" name={`portal2[0]:${step}`} />, <Child key="e" name={`portal2[1]:${step}`} />, ], portalContainer2, ), ]; } } ReactDOM.render(<Parent step="a" />, container); expect(portalContainer1.innerHTML).toBe('<div>portal1[0]:a</div>'); expect(portalContainer2.innerHTML).toBe( '<div>portal2[0]:a</div><div>portal2[1]:a</div>', ); expect(container.innerHTML).toBe( '<div>normal[0]:a</div><div>normal[1]:a</div>', ); expect(ops).toEqual([ 'normal[0]:a componentDidMount', 'portal1[0]:a componentDidMount', 'normal[1]:a componentDidMount', 'portal2[0]:a componentDidMount', 'portal2[1]:a componentDidMount', 'Parent:a componentDidMount', ]); ops.length = 0; ReactDOM.render(<Parent step="b" />, container); expect(portalContainer1.innerHTML).toBe('<div>portal1[0]:b</div>'); expect(portalContainer2.innerHTML).toBe( '<div>portal2[0]:b</div><div>portal2[1]:b</div>', ); expect(container.innerHTML).toBe( '<div>normal[0]:b</div><div>normal[1]:b</div>', ); expect(ops).toEqual([ 'normal[0]:b componentDidUpdate', 'portal1[0]:b componentDidUpdate', 'normal[1]:b componentDidUpdate', 'portal2[0]:b componentDidUpdate', 'portal2[1]:b componentDidUpdate', 'Parent:b componentDidUpdate', ]); ops.length = 0; ReactDOM.unmountComponentAtNode(container); expect(portalContainer1.innerHTML).toBe(''); expect(portalContainer2.innerHTML).toBe(''); expect(container.innerHTML).toBe(''); expect(ops).toEqual([ 'Parent:b componentWillUnmount', 'normal[0]:b componentWillUnmount', 'portal1[0]:b componentWillUnmount', 'normal[1]:b componentWillUnmount', 'portal2[0]:b componentWillUnmount', 'portal2[1]:b componentWillUnmount', ]); }); it('should render nested portals', () => { const portalContainer1 = document.createElement('div'); const portalContainer2 = document.createElement('div'); const portalContainer3 = document.createElement('div'); ReactDOM.render( [ <div key="a">normal[0]</div>, ReactDOM.createPortal( [ <div key="b">portal1[0]</div>, ReactDOM.createPortal( <div key="c">portal2[0]</div>, portalContainer2, ), ReactDOM.createPortal( <div key="d">portal3[0]</div>, portalContainer3, ), <div key="e">portal1[1]</div>, ], portalContainer1, ), <div key="f">normal[1]</div>, ], container, ); expect(portalContainer1.innerHTML).toBe( '<div>portal1[0]</div><div>portal1[1]</div>', ); expect(portalContainer2.innerHTML).toBe('<div>portal2[0]</div>'); expect(portalContainer3.innerHTML).toBe('<div>portal3[0]</div>'); expect(container.innerHTML).toBe( '<div>normal[0]</div><div>normal[1]</div>', ); ReactDOM.unmountComponentAtNode(container); expect(portalContainer1.innerHTML).toBe(''); expect(portalContainer2.innerHTML).toBe(''); expect(portalContainer3.innerHTML).toBe(''); expect(container.innerHTML).toBe(''); }); it('should reconcile portal children', () => { const portalContainer = document.createElement('div'); ReactDOM.render( <div>{ReactDOM.createPortal(<div>portal:1</div>, portalContainer)}</div>, container, ); expect(portalContainer.innerHTML).toBe('<div>portal:1</div>'); expect(container.innerHTML).toBe('<div></div>'); ReactDOM.render( <div>{ReactDOM.createPortal(<div>portal:2</div>, portalContainer)}</div>, container, ); expect(portalContainer.innerHTML).toBe('<div>portal:2</div>'); expect(container.innerHTML).toBe('<div></div>'); ReactDOM.render( <div>{ReactDOM.createPortal(<p>portal:3</p>, portalContainer)}</div>, container, ); expect(portalContainer.innerHTML).toBe('<p>portal:3</p>'); expect(container.innerHTML).toBe('<div></div>'); ReactDOM.render( <div>{ReactDOM.createPortal(['Hi', 'Bye'], portalContainer)}</div>, container, ); expect(portalContainer.innerHTML).toBe('HiBye'); expect(container.innerHTML).toBe('<div></div>'); ReactDOM.render( <div>{ReactDOM.createPortal(['Bye', 'Hi'], portalContainer)}</div>, container, ); expect(portalContainer.innerHTML).toBe('ByeHi'); expect(container.innerHTML).toBe('<div></div>'); ReactDOM.render( <div>{ReactDOM.createPortal(null, portalContainer)}</div>, container, ); expect(portalContainer.innerHTML).toBe(''); expect(container.innerHTML).toBe('<div></div>'); }); it('should unmount empty portal component wherever it appears', () => { const portalContainer = document.createElement('div'); class Wrapper extends React.Component { constructor(props) { super(props); this.state = { show: true, }; } render() { return ( <div> {this.state.show && ( <React.Fragment> {ReactDOM.createPortal(null, portalContainer)} <div>child</div> </React.Fragment> )} <div>parent</div> </div> ); } } const instance = ReactDOM.render(<Wrapper />, container); expect(container.innerHTML).toBe( '<div><div>child</div><div>parent</div></div>', ); instance.setState({show: false}); expect(instance.state.show).toBe(false); expect(container.innerHTML).toBe('<div><div>parent</div></div>'); }); it('should keep track of namespace across portals (simple)', () => { assertNamespacesMatch( <svg {...expectSVG}> <image {...expectSVG} /> {usePortal(<div {...expectHTML} />)} <image {...expectSVG} /> </svg>, ); assertNamespacesMatch( <math {...expectMath}> <mi {...expectMath} /> {usePortal(<div {...expectHTML} />)} <mi {...expectMath} /> </math>, ); assertNamespacesMatch( <div {...expectHTML}> <p {...expectHTML} /> {usePortal( <svg {...expectSVG}> <image {...expectSVG} /> </svg>, )} <p {...expectHTML} /> </div>, ); }); it('should keep track of namespace across portals (medium)', () => { assertNamespacesMatch( <svg {...expectSVG}> <image {...expectSVG} /> {usePortal(<div {...expectHTML} />)} <image {...expectSVG} /> {usePortal(<div {...expectHTML} />)} <image {...expectSVG} /> </svg>, ); assertNamespacesMatch( <div {...expectHTML}> <math {...expectMath}> <mi {...expectMath} /> {usePortal( <svg {...expectSVG}> <image {...expectSVG} /> </svg>, )} </math> <p {...expectHTML} /> </div>, ); assertNamespacesMatch( <math {...expectMath}> <mi {...expectMath} /> {usePortal( <svg {...expectSVG}> <image {...expectSVG} /> <foreignObject {...expectSVG}> <p {...expectHTML} /> <math {...expectMath}> <mi {...expectMath} /> </math> <p {...expectHTML} /> </foreignObject> <image {...expectSVG} /> </svg>, )} <mi {...expectMath} /> </math>, ); assertNamespacesMatch( <div {...expectHTML}> {usePortal( <svg {...expectSVG}> {usePortal(<div {...expectHTML} />)} <image {...expectSVG} /> </svg>, )} <p {...expectHTML} /> </div>, ); assertNamespacesMatch( <svg {...expectSVG}> <svg {...expectSVG}> {usePortal(<div {...expectHTML} />)} <image {...expectSVG} /> </svg> <image {...expectSVG} /> </svg>, ); }); it('should keep track of namespace across portals (complex)', () => { assertNamespacesMatch( <div {...expectHTML}> {usePortal( <svg {...expectSVG}> <image {...expectSVG} /> </svg>, )} <p {...expectHTML} /> <svg {...expectSVG}> <image {...expectSVG} /> </svg> <svg {...expectSVG}> <svg {...expectSVG}> <image {...expectSVG} /> </svg> <image {...expectSVG} /> </svg> <p {...expectHTML} /> </div>, ); assertNamespacesMatch( <div {...expectHTML}> <svg {...expectSVG}> <svg {...expectSVG}> <image {...expectSVG} /> {usePortal( <svg {...expectSVG}> <image {...expectSVG} /> <svg {...expectSVG}> <image {...expectSVG} /> </svg> <image {...expectSVG} /> </svg>, )} <image {...expectSVG} /> <foreignObject {...expectSVG}> <p {...expectHTML} /> {usePortal(<p {...expectHTML} />)} <p {...expectHTML} /> </foreignObject> </svg> <image {...expectSVG} /> </svg> <p {...expectHTML} /> </div>, ); assertNamespacesMatch( <div {...expectHTML}> <svg {...expectSVG}> <foreignObject {...expectSVG}> <p {...expectHTML} /> {usePortal( <svg {...expectSVG}> <image {...expectSVG} /> <svg {...expectSVG}> <image {...expectSVG} /> <foreignObject {...expectSVG}> <p {...expectHTML} /> </foreignObject> {usePortal(<p {...expectHTML} />)} </svg> <image {...expectSVG} /> </svg>, )} <p {...expectHTML} /> </foreignObject> <image {...expectSVG} /> </svg> <p {...expectHTML} /> </div>, ); }); it('should unwind namespaces on uncaught errors', () => { function BrokenRender() { throw new Error('Hello'); } expect(() => { assertNamespacesMatch( <svg {...expectSVG}> <BrokenRender /> </svg>, ); }).toThrow('Hello'); assertNamespacesMatch(<div {...expectHTML} />); }); it('should unwind namespaces on caught errors', () => { function BrokenRender() { throw new Error('Hello'); } class ErrorBoundary extends React.Component { state = {error: null}; componentDidCatch(error) { this.setState({error}); } render() { if (this.state.error) { return <p {...expectHTML} />; } return this.props.children; } } assertNamespacesMatch( <svg {...expectSVG}> <foreignObject {...expectSVG}> <ErrorBoundary> <math {...expectMath}> <BrokenRender /> </math> </ErrorBoundary> </foreignObject> <image {...expectSVG} /> </svg>, ); assertNamespacesMatch(<div {...expectHTML} />); }); it('should unwind namespaces on caught errors in a portal', () => { function BrokenRender() { throw new Error('Hello'); } class ErrorBoundary extends React.Component { state = {error: null}; componentDidCatch(error) { this.setState({error}); } render() { if (this.state.error) { return <image {...expectSVG} />; } return this.props.children; } } assertNamespacesMatch( <svg {...expectSVG}> <ErrorBoundary> {usePortal( <div {...expectHTML}> <math {...expectMath}> <BrokenRender />) </math> </div>, )} </ErrorBoundary> {usePortal(<div {...expectHTML} />)} </svg>, ); }); it('should pass portal context when rendering subtree elsewhere', () => { const portalContainer = document.createElement('div'); class Component extends React.Component { static contextTypes = { foo: PropTypes.string.isRequired, }; render() { return <div>{this.context.foo}</div>; } } class Parent extends React.Component { static childContextTypes = { foo: PropTypes.string.isRequired, }; getChildContext() { return { foo: 'bar', }; } render() { return ReactDOM.createPortal(<Component />, portalContainer); } } ReactDOM.render(<Parent />, container); expect(container.innerHTML).toBe(''); expect(portalContainer.innerHTML).toBe('<div>bar</div>'); }); it('should update portal context if it changes due to setState', () => { const portalContainer = document.createElement('div'); class Component extends React.Component { static contextTypes = { foo: PropTypes.string.isRequired, getFoo: PropTypes.func.isRequired, }; render() { return <div>{this.context.foo + '-' + this.context.getFoo()}</div>; } } class Parent extends React.Component { static childContextTypes = { foo: PropTypes.string.isRequired, getFoo: PropTypes.func.isRequired, }; state = { bar: 'initial', }; getChildContext() { return { foo: this.state.bar, getFoo: () => this.state.bar, }; } render() { return ReactDOM.createPortal(<Component />, portalContainer); } } const instance = ReactDOM.render(<Parent />, container); expect(portalContainer.innerHTML).toBe('<div>initial-initial</div>'); expect(container.innerHTML).toBe(''); instance.setState({bar: 'changed'}); expect(portalContainer.innerHTML).toBe('<div>changed-changed</div>'); expect(container.innerHTML).toBe(''); }); it('should update portal context if it changes due to re-render', () => { const portalContainer = document.createElement('div'); class Component extends React.Component { static contextTypes = { foo: PropTypes.string.isRequired, getFoo: PropTypes.func.isRequired, }; render() { return <div>{this.context.foo + '-' + this.context.getFoo()}</div>; } } class Parent extends React.Component { static childContextTypes = { foo: PropTypes.string.isRequired, getFoo: PropTypes.func.isRequired, }; getChildContext() { return { foo: this.props.bar, getFoo: () => this.props.bar, }; } render() { return ReactDOM.createPortal(<Component />, portalContainer); } } ReactDOM.render(<Parent bar="initial" />, container); expect(portalContainer.innerHTML).toBe('<div>initial-initial</div>'); expect(container.innerHTML).toBe(''); ReactDOM.render(<Parent bar="changed" />, container); expect(portalContainer.innerHTML).toBe('<div>changed-changed</div>'); expect(container.innerHTML).toBe(''); }); it('findDOMNode should find dom element after expanding a fragment', () => { class MyNode extends React.Component { render() { return !this.props.flag ? [<div key="a" />] : [<span key="b" />, <div key="a" />]; } } const myNodeA = ReactDOM.render(<MyNode />, container); const a = ReactDOM.findDOMNode(myNodeA); expect(a.tagName).toBe('DIV'); const myNodeB = ReactDOM.render(<MyNode flag={true} />, container); expect(myNodeA === myNodeB).toBe(true); const b = ReactDOM.findDOMNode(myNodeB); expect(b.tagName).toBe('SPAN'); }); it('should bubble events from the portal to the parent', () => { const portalContainer = document.createElement('div'); document.body.appendChild(portalContainer); try { const ops = []; let portal = null; ReactDOM.render( <div onClick={() => ops.push('parent clicked')}> {ReactDOM.createPortal( <div onClick={() => ops.push('portal clicked')} ref={n => (portal = n)}> portal </div>, portalContainer, )} </div>, container, ); expect(portal.tagName).toBe('DIV'); portal.click(); expect(ops).toEqual(['portal clicked', 'parent clicked']); } finally { document.body.removeChild(portalContainer); } }); it('should not onMouseLeave when staying in the portal', () => { const portalContainer = document.createElement('div'); document.body.appendChild(portalContainer); let ops = []; let firstTarget = null; let secondTarget = null; let thirdTarget = null; function simulateMouseMove(from, to) { if (from) { from.dispatchEvent( new MouseEvent('mouseout', { bubbles: true, cancelable: true, relatedTarget: to, }), ); } if (to) { to.dispatchEvent( new MouseEvent('mouseover', { bubbles: true, cancelable: true, relatedTarget: from, }), ); } } try { ReactDOM.render( <div> <div onMouseEnter={() => ops.push('enter parent')} onMouseLeave={() => ops.push('leave parent')}> <div ref={n => (firstTarget = n)} /> {ReactDOM.createPortal( <div onMouseEnter={() => ops.push('enter portal')} onMouseLeave={() => ops.push('leave portal')} ref={n => (secondTarget = n)}> portal </div>, portalContainer, )} </div> <div ref={n => (thirdTarget = n)} /> </div>, container, ); simulateMouseMove(null, firstTarget); expect(ops).toEqual(['enter parent']); ops = []; simulateMouseMove(firstTarget, secondTarget); expect(ops).toEqual([ // Parent did not invoke leave because we're still inside the portal. 'enter portal', ]); ops = []; simulateMouseMove(secondTarget, thirdTarget); expect(ops).toEqual([ 'leave portal', 'leave parent', // Only when we leave the portal does onMouseLeave fire. ]); } finally { document.body.removeChild(portalContainer); } }); it('should throw on bad createPortal argument', () => { expect(() => { ReactDOM.createPortal(<div>portal</div>, null); }).toThrow('Target container is not a DOM element.'); expect(() => { ReactDOM.createPortal(<div>portal</div>, document.createTextNode('hi')); }).toThrow('Target container is not a DOM element.'); }); it('should warn for non-functional event listeners', () => { class Example extends React.Component { render() { return <div onClick="woops" />; } } expect(() => ReactDOM.render(<Example />, container)).toWarnDev( 'Expected `onClick` listener to be a function, instead got a value of `string` type.\n' + ' in div (at **)\n' + ' in Example (at **)', ); }); it('should warn with a special message for `false` event listeners', () => { class Example extends React.Component { render() { return <div onClick={false} />; } } expect(() => ReactDOM.render(<Example />, container)).toWarnDev( 'Expected `onClick` listener to be a function, instead got `false`.\n\n' + 'If you used to conditionally omit it with onClick={condition && value}, ' + 'pass onClick={condition ? value : undefined} instead.\n' + ' in div (at **)\n' + ' in Example (at **)', ); }); it('should not update event handlers until commit', () => { let ops = []; const handlerA = () => ops.push('A'); const handlerB = () => ops.push('B'); class Example extends React.Component { state = {flip: false, count: 0}; flip() { this.setState({flip: true, count: this.state.count + 1}); } tick() { this.setState({count: this.state.count + 1}); } render() { const useB = !this.props.forceA && this.state.flip; return <div onClick={useB ? handlerB : handlerA} />; } } class Click extends React.Component { constructor() { super(); node.click(); } render() { return null; } } let inst; ReactDOM.render([<Example key="a" ref={n => (inst = n)} />], container); const node = container.firstChild; expect(node.tagName).toEqual('DIV'); node.click(); expect(ops).toEqual(['A']); ops = []; // Render with the other event handler. inst.flip(); node.click(); expect(ops).toEqual(['B']); ops = []; // Rerender without changing any props. inst.tick(); node.click(); expect(ops).toEqual(['B']); ops = []; // Render a flip back to the A handler. The second component invokes the // click handler during render to simulate a click during an aborted // render. I use this hack because at current time we don't have a way to // test aborted ReactDOM renders. ReactDOM.render( [<Example key="a" forceA={true} />, <Click key="b" />], container, ); // Because the new click handler has not yet committed, we should still // invoke B. expect(ops).toEqual(['B']); ops = []; // Any click that happens after commit, should invoke A. node.click(); expect(ops).toEqual(['A']); }); it('should not crash encountering low-priority tree', () => { ReactDOM.render( <div hidden={true}> <div /> </div>, container, ); }); it('should not warn when rendering into an empty container', () => { ReactDOM.render(<div>foo</div>, container); expect(container.innerHTML).toBe('<div>foo</div>'); ReactDOM.render(null, container); expect(container.innerHTML).toBe(''); ReactDOM.render(<div>bar</div>, container); expect(container.innerHTML).toBe('<div>bar</div>'); }); it('should warn when replacing a container which was manually updated outside of React', () => { // when not messing with the DOM outside of React ReactDOM.render(<div key="1">foo</div>, container); ReactDOM.render(<div key="1">bar</div>, container); expect(container.innerHTML).toBe('<div>bar</div>'); // then we mess with the DOM before an update // we know this will error - that is expected right now // It's an error of type 'NotFoundError' with no message container.innerHTML = '<div>MEOW.</div>'; expect(() => { expect(() => ReactDOM.render(<div key="2">baz</div>, container), ).toWarnDev( 'render(...): ' + 'It looks like the React-rendered content of this container was ' + 'removed without using React. This is not supported and will ' + 'cause errors. Instead, call ReactDOM.unmountComponentAtNode ' + 'to empty a container.', {withoutStack: true}, ); }).toThrowError(); }); it('should warn when doing an update to a container manually updated outside of React', () => { // when not messing with the DOM outside of React ReactDOM.render(<div>foo</div>, container); ReactDOM.render(<div>bar</div>, container); expect(container.innerHTML).toBe('<div>bar</div>'); // then we mess with the DOM before an update container.innerHTML = '<div>MEOW.</div>'; expect(() => ReactDOM.render(<div>baz</div>, container)).toWarnDev( 'render(...): ' + 'It looks like the React-rendered content of this container was ' + 'removed without using React. This is not supported and will ' + 'cause errors. Instead, call ReactDOM.unmountComponentAtNode ' + 'to empty a container.', {withoutStack: true}, ); }); it('should warn when doing an update to a container manually cleared outside of React', () => { // when not messing with the DOM outside of React ReactDOM.render(<div>foo</div>, container); ReactDOM.render(<div>bar</div>, container); expect(container.innerHTML).toBe('<div>bar</div>'); // then we mess with the DOM before an update container.innerHTML = ''; expect(() => ReactDOM.render(<div>baz</div>, container)).toWarnDev( 'render(...): ' + 'It looks like the React-rendered content of this container was ' + 'removed without using React. This is not supported and will ' + 'cause errors. Instead, call ReactDOM.unmountComponentAtNode ' + 'to empty a container.', {withoutStack: true}, ); }); it('should render a text component with a text DOM node on the same document as the container', () => { // 1. Create a new document through the use of iframe // 2. Set up the spy to make asserts when a text component // is rendered inside the iframe container const textContent = 'Hello world'; const iframe = document.createElement('iframe'); document.body.appendChild(iframe); const iframeDocument = iframe.contentDocument; iframeDocument.write( '<!DOCTYPE html><html><head></head><body><div></div></body></html>', ); iframeDocument.close(); const iframeContainer = iframeDocument.body.firstChild; let actualDocument; let textNode; spyOnDevAndProd(iframeContainer, 'appendChild').and.callFake(node => { actualDocument = node.ownerDocument; textNode = node; }); ReactDOM.render(textContent, iframeContainer); expect(textNode.textContent).toBe(textContent); expect(actualDocument).not.toBe(document); expect(actualDocument).toBe(iframeDocument); expect(iframeContainer.appendChild).toHaveBeenCalledTimes(1); }); it('should mount into a document fragment', () => { const fragment = document.createDocumentFragment(); ReactDOM.render(<div>foo</div>, fragment); expect(container.innerHTML).toBe(''); container.appendChild(fragment); expect(container.innerHTML).toBe('<div>foo</div>'); }); // Regression test for https://github.com/facebook/react/issues/12643#issuecomment-413727104 it('should not diff memoized host components', () => { let inputRef = React.createRef(); let didCallOnChange = false; class Child extends React.Component { state = {}; componentDidMount() { document.addEventListener('click', this.update, true); } componentWillUnmount() { document.removeEventListener('click', this.update, true); } update = () => { // We're testing that this setState() // doesn't cause React to commit updates // to the input outside (which would itself // prevent the parent's onChange parent handler // from firing). this.setState({}); // Note that onChange was always broken when there was an // earlier setState() in a manual document capture phase // listener *in the same component*. But that's very rare. // Here we're testing that a *child* component doesn't break // the parent if this happens. }; render() { return <div />; } } class Parent extends React.Component { handleChange = val => { didCallOnChange = true; }; render() { return ( <div> <Child /> <input ref={inputRef} type="checkbox" checked={true} onChange={this.handleChange} /> </div> ); } } ReactDOM.render(<Parent />, container); inputRef.current.dispatchEvent( new MouseEvent('click', { bubbles: true, }), ); expect(didCallOnChange).toBe(true); }); });