UNPKG

rax

Version:

A universal React-compatible render engine.

1,253 lines (1,107 loc) 32 kB
/* @jsx createElement */ import Component from '../component'; import createElement from '../../createElement'; import Host from '../host'; import render from '../../render'; import ServerDriver from 'driver-server'; describe('CompositeComponent', function() { function createNodeElement(tagName) { return { nodeType: 1, tagName: tagName.toUpperCase(), attributes: {}, style: {}, childNodes: [], parentNode: null }; } beforeEach(function() { Host.driver = ServerDriver; jest.useFakeTimers(); }); afterEach(function() { Host.driver = null; jest.useRealTimers(); }); it('should rewire refs when rendering to different child types', function() { class MyComponent extends Component { state = {activated: false}; toggleActivatedState = () => { this.setState({activated: !this.state.activated}); }; render() { return !this.state.activated ? <a ref="x" /> : <b ref="x" />; } } let component = render(<MyComponent />); expect(component.refs.x.tagName).toBe('A'); component.toggleActivatedState(); jest.runAllTimers(); expect(component.refs.x.tagName).toBe('B'); }); it('donot call render when setState in componentWillMount', function() { let container = createNodeElement('div'); let renderCounter = 0; class Foo extends Component { constructor() { super(); this.state = {}; } componentWillMount() { this.setState({ value: 'foo' }); } render() { ++renderCounter; return <span className={this.state.value} />; } } render(<Foo />, container); expect(renderCounter).toEqual(1); expect(container.childNodes[0].attributes.class).toBe('foo'); }); it('setState callback triggered', function() { let container = createNodeElement('div'); let triggered = false; class Foo extends Component { constructor() { super(); this.state = {}; } componentWillMount() { this.setState({ value: 'foo' }, () => { triggered = true; }); } componentWillReceiveProps() { this.setState({ value: 'foo' }, () => { triggered = true; }); } render() { return <span className={this.state.value} />; } } const instance = render(<Foo />, container); expect(triggered).toBe(true); triggered = false; instance.setState({}, () => triggered = true); jest.runAllTimers(); expect(triggered).toBe(true); triggered = false; render(<Foo />, container); expect(triggered).toBe(true); }); it('setState callback triggered in didMount or didUpdate should receive latest state', function() { let container = createNodeElement('div'); const logs = []; class Foo extends Component { constructor() { super(); this.state = { count: 1 }; } componentDidMount() { // eslint-disable-next-line react/no-did-mount-set-state this.setState({ count: 2 }, () => { logs.push(this.state.count); }); } componentDidUpdate() { if (this.state.count === 2) { // eslint-disable-next-line react/no-did-update-set-state this.setState({ count: 3 }, () => { logs.push(this.state.count); }); }; } render() { return <span className={this.state.value} />; } } render(<Foo />, container); jest.runAllTimers(); expect(logs).toEqual([2, 3]); }); it('will call all the normal life cycle methods', function() { var lifeCycles = []; let container = createNodeElement('div'); class Foo extends Component { constructor() { super(); this.state = {}; } componentWillMount() { this.setState({value: 'foo'}); lifeCycles.push('will-mount'); } componentDidMount() { lifeCycles.push('did-mount'); } componentWillReceiveProps(nextProps) { this.setState({value: 'bar'}, function() { lifeCycles.push('receive-props-callback'); }); lifeCycles.push('receive-props', nextProps); } shouldComponentUpdate(nextProps, nextState) { lifeCycles.push('should-update', nextProps, nextState); return true; } componentWillUpdate(nextProps, nextState) { lifeCycles.push('will-update', nextProps, nextState); } componentDidUpdate(prevProps, prevState) { lifeCycles.push('did-update', prevProps, prevState); } componentWillUnmount() { lifeCycles.push('will-unmount'); } render() { lifeCycles.push('render'); return <span className={this.props.value} />; } } render(<div><Foo value="foo" /></div>, container); expect(lifeCycles).toEqual([ 'will-mount', 'render', 'did-mount' ]); lifeCycles = []; // reset render(<div><Foo value="bar" /></div>, container); expect(lifeCycles).toEqual([ 'receive-props', {value: 'bar'}, 'should-update', {value: 'bar'}, {value: 'bar'}, 'will-update', {value: 'bar'}, {value: 'bar'}, 'render', 'did-update', {value: 'foo'}, {value: 'foo'}, 'receive-props-callback' ]); lifeCycles = []; // reset render(<div />, container); expect(lifeCycles).toEqual([ 'will-unmount', ]); }); it('not break other component render when one component rise an error', () => { let container = createNodeElement('div'); class MyComponent extends Component { render() { return [ <BrokenRender key="a" />, <NoBrokenRender key="b" /> ]; } } class BrokenRender extends Component { constructor() { throw new Error('Hello'); } render() { return ( <span>Hello 1</span> ); } } class NoBrokenRender extends Component { render() { return ( <span>Hello 2</span> ); } } expect(() => { render(<MyComponent />, container); jest.runAllTimers(); }).toThrowError(/Hello/); expect(container.childNodes[0].childNodes[0].data).toBe('Hello 2'); }); it('catches render error in a boundary', () => { let container = createNodeElement('div'); class ErrorBoundary extends Component { state = {error: null}; componentDidCatch(error) { this.setState({error}); } render() { if (this.state.error) { return ( <span>{`Caught an error: ${this.state.error.message}.`}</span> ); } return this.props.children; } } function BrokenRender(props) { throw new Error('Hello'); } render( <ErrorBoundary> <BrokenRender /> </ErrorBoundary>, container); jest.runAllTimers(); expect(container.childNodes[0].childNodes[0].data).toBe('Caught an error: Hello.'); }); it('catches component update error in a boundary', () => { let container = createNodeElement('div'); class ErrorBoundary extends Component { state = {error: null}; componentDidCatch(error) { this.setState({error}); } render() { if (this.state.error) { return ( <span>{`Caught an error: ${this.state.error.message}.`}</span> ); } return this.props.children; } } class BrokenRender extends Component { state = {foo: 'Hello'}; componentDidMount() { setTimeout(() => { this.setState({ foo: 'error' }); }); } render() { if (this.state.foo === 'error') { throw Error('foo'); } return ( <span>{this.state.foo}</span> ); } } render( <ErrorBoundary> <BrokenRender /> </ErrorBoundary>, container); jest.runAllTimers(); expect(container.childNodes[0].childNodes[0].data).toContain('Caught an error: foo'); }); it('catches lifeCycles errors in a boundary', () => { let container = createNodeElement('div'); class ErrorBoundary extends Component { state = {error: null}; componentDidCatch(error) { this.setState({error}); } render() { if (this.state.error) { return ( <span>{`Caught an error: ${this.state.error.message}.`}</span> ); } return this.props.children; } } class BrokenRender extends Component { componentDidMount() { throw new Error('Hello'); } render() { return ( <span>Hello</span> ); } } render( <ErrorBoundary> <BrokenRender /> </ErrorBoundary>, container); jest.runAllTimers(); expect(container.childNodes[0].childNodes[0].data).toBe('Caught an error: Hello.'); }); it('catches constructor errors in a boundary', () => { let container = createNodeElement('div'); class ErrorBoundary extends Component { state = {error: null}; componentDidCatch(error) { this.setState({error}); } render() { if (this.state.error) { return ( <span>{`Caught an error: ${this.state.error.message}.`}</span> ); } return this.props.children; } } class BrokenRender extends Component { constructor() { throw new Error('Hello'); } render() { return ( <span>Hello</span> ); } } render( <ErrorBoundary> <BrokenRender /> </ErrorBoundary>, container); jest.runAllTimers(); expect(container.childNodes[0].childNodes[0].data).toBe('Caught an error: Hello.'); }); it('catches render errors in a component', () => { let container = createNodeElement('div'); class BrokenRender extends Component { state = {error: null}; componentDidCatch(error) { this.setState({error}); } render() { if (this.state.error) { return ( <span>{`Caught an error: ${this.state.error.message}.`}</span> ); } throw new Error('Hello'); } } render(<BrokenRender />, container); jest.runAllTimers(); expect(container.childNodes[0].childNodes[0].data).toBe('Caught an error: Hello.'); }); it('should not attempt to recover an unmounting error boundary', () => { let container = createNodeElement('div'); let logs = []; class Parent extends Component { componentWillUnmount() { logs.push('Parent componentWillUnmount'); } render() { return <Boundary />; } } class Boundary extends Component { componentDidCatch(e) { logs.push(`Caught error: ${e.message}`); } render() { return <ThrowsOnUnmount />; } } class ThrowsOnUnmount extends Component { componentWillUnmount() { logs.push('ThrowsOnUnmount componentWillUnmount'); throw new Error('unmount error'); } render() { return null; } } render(<Parent />, container); render(<div />, container); expect(logs).toEqual([ // Parent unmounts before the error is thrown. 'Parent componentWillUnmount', 'ThrowsOnUnmount componentWillUnmount', ]); }); it('rendering correct on siblings of a component that throws', () => { let container = createNodeElement('div'); function BrokenRender() { throw new Error('Hello'); } class ErrorBoundary extends Component { state = {error: null}; componentDidCatch(error) { this.setState({error}); } render() { if (this.state.error) { return ( <div>{`Caught an error: ${this.state.error.message}.`}</div> ); } return ( <div> <span>siblings</span> <BrokenRender /> <span>siblings</span> </div> ); } } render(<ErrorBoundary />, container); jest.runAllTimers(); expect(container.childNodes.length).toBe(1); expect(container.childNodes[0].childNodes[0].data).toBe('Caught an error: Hello.'); }); it('working correct with fragment when a component that throw error', () => { let container = createNodeElement('div'); class ErrorBoundary extends Component { state = {error: null}; componentDidCatch(error) { this.setState({error}); } render() { if (this.state.error) { return ( <span>{`Caught an error: ${this.state.error.message}.`}</span> ); } return [ <span key={'1'}>siblings</span>, <BrokenRender key={'error'} />, <span key={'2'}>siblings</span> ]; } } function BrokenRender() { throw new Error('Hello'); } render(<ErrorBoundary />, container); jest.runAllTimers(); expect(container.childNodes.length).toBe(1); expect(container.childNodes[0].childNodes[0].data).toBe('Caught an error: Hello.'); }); it('Life cycle method invocation sequence should be correct', () => { let logs = []; let container = createNodeElement('div'); class ErrorBoundary extends Component { state = {error: null}; componentDidCatch(error) { this.setState({error}); } componentDidMount() { logs.push('componentDidMountErrorBoundary'); } componentDidUpdate() { logs.push('componentDidUpdateErrorBoundary'); } render() { if (this.state.error) { return ( <span>{`Caught an error: ${this.state.error.message}.`}</span> ); } return this.props.children; } } class Life1 extends Component { componentWillMount() { logs.push('componentWillMount1'); } render() { logs.push('render1'); return null; } componentDidMount() { logs.push('componentDidMount1'); } componentWillUpdate() { logs.push('componentWillUpdata1'); } componentDidUpdate() { logs.push('componentDidUpdate1'); } componentWillUnmount() { logs.push('componentWillUnmount1'); } } class Life2 extends Component { componentWillMount() { logs.push('componentWillMount2'); } render() { logs.push('render2'); throw new Error(); } componentDidMount() { logs.push('componentDidMount2'); } componentWillUpdate() { logs.push('componentWillUpdate2'); } componentDidUpdate() { logs.push('componentDidUpdate2'); } componentWillUnmount() { logs.push('componentWillUnmount2'); } } class Life3 extends Component { componentWillMount() { logs.push('componentWillMount3'); } render() { logs.push('render3'); return null; } componentDidMount() { logs.push('componentDidMount3'); } componentWillUpdata() { logs.push('componentWillUpdata3'); } componentDidUpdate() { logs.push('componentDidUpdate3'); } componentWillUnmount() { logs.push('componentWillUnmount3'); } } render( <ErrorBoundary> <Life1 /> <Life2 /> <Life3 /> </ErrorBoundary>, container); jest.runAllTimers(); expect(logs).toEqual([ 'componentWillMount1', 'render1', 'componentWillMount2', 'render2', 'componentWillMount3', 'render3', 'componentDidMount1', 'componentDidMount2', 'componentDidMount3', 'componentDidMountErrorBoundary', 'componentWillUnmount1', 'componentWillUnmount2', 'componentWillUnmount3', 'componentDidUpdateErrorBoundary' ]); }); it('should boundary exec componentDidCatch when child setState throw error', () => { let container = createNodeElement('div'); let child; class Child extends Component { state = { count: 1 } render() { child = this; if (this.state.count === 2) { throw new Error('Hello'); } return ( <span>Hello</span> ); } } class ErrorBoundary extends Component { state = {error: null}; componentDidCatch(error) { this.setState({error}); } render() { if (this.state.error) { return ( <div>{`Caught an error: ${this.state.error.message}.`}</div> ); } return ( <div> <Child /> </div> ); } } render(<ErrorBoundary><Child /></ErrorBoundary>, container); expect(container.childNodes[0].childNodes[0].childNodes[0].data).toBe('Hello'); child.setState({count: 2}); jest.runAllTimers(); expect(container.childNodes[0].childNodes[0].data).toBe('Caught an error: Hello.'); }); it('should update state to the next render when catch error.', () => { let container = createNodeElement('div'); class ErrorBoundary extends Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error) { return { hasError: true }; } componentDidCatch(error, errorInfo) { // log } render() { if (this.state.hasError) { return <h1>Something went wrong.</h1>; } return this.props.children; } } function BrokenRender(props) { throw new Error('Hello'); } render( <ErrorBoundary> <BrokenRender /> </ErrorBoundary>, container); jest.runAllTimers(); expect(container.childNodes[0].childNodes[0].data).toBe('Something went wrong.'); }); it('should catch error only with getDerivedStateFromError.', () => { let container = createNodeElement('div'); class ErrorBoundary extends Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error) { return { hasError: true }; } render() { if (this.state.hasError) { return <h1>Something went wrong.</h1>; } return this.props.children; } } function BrokenRender(props) { throw new Error('Hello'); } render( <ErrorBoundary> <BrokenRender /> </ErrorBoundary>, container); jest.runAllTimers(); expect(container.childNodes[0].childNodes[0].data).toBe('Something went wrong.'); }); it('should catch the exact error with getDerivedStateFromError.', () => { let caughtError; let exampleError = new Error('Example error message'); let container = createNodeElement('div'); class ErrorBoundary extends Component { constructor(props) { super(props); this.state = { hasError: false, error: null }; } static getDerivedStateFromError(error) { caughtError = error; return { hasError: true, error }; } render() { if (this.state.hasError) { return <h1>{this.state.error.message}</h1>; } return this.props.children; } } function BrokenRender(props) { throw exampleError; } render( <ErrorBoundary> <BrokenRender /> </ErrorBoundary>, container); jest.runAllTimers(); expect(caughtError).toBe(exampleError); expect(container.childNodes[0].childNodes[0].data).toBe('Example error message'); }); it('should render correct when prevRenderedComponent did not generate nodes', () => { let container = createNodeElement('div'); class Frag extends Component { render() { return []; } } class App extends Component { state = {count: 0}; render() { if (this.state.count === 0) { return <Frag />; } return <div />; } } const instance = render(<App />, container); expect(container.childNodes.length).toBe(0); instance.setState({count: 1}); jest.runAllTimers(); expect(container.childNodes[0].tagName).toBe('DIV'); }); it('render component that componentDidMount could get mounted DOM', () => { let container = createNodeElement('div'); class Child extends Component { componentDidMount() { expect(container.childNodes[0].childNodes[0].childNodes[0].tagName).toBe('DIV'); } render() { return <div />; } } class App extends Component { render() { return <div><Child /></div>; } } const instance = render(<div><App /></div>, container); jest.runAllTimers(); expect(container.childNodes[0].tagName).toBe('DIV'); }); it('render with fragment that componentDidMount could get mounted DOM', () => { let container = createNodeElement('div'); class Child extends Component { componentDidMount() { expect(container.childNodes[0].tagName).toBe('DIV'); } render() { return <div />; } } class App extends Component { render() { return [ <Child key="1" /> ]; } } const instance = render(<App />, container); jest.runAllTimers(); expect(container.childNodes[0].tagName).toBe('DIV'); }); it('schedules sync updates when inside componentDidMount/Update', () => { let container = createNodeElement('div'); let instance; let ops = []; class Foo extends Component { state = {tick: 0}; componentDidMount() { ops.push('componentDidMount (before setState): ' + this.state.tick); this.setState({tick: 1}); // eslint-disable-line // We're in a batch. Update hasn't flushed yet. ops.push('componentDidMount (after setState): ' + this.state.tick); } componentDidUpdate() { ops.push('componentDidUpdate: ' + this.state.tick); if (this.state.tick === 2) { ops.push('componentDidUpdate (before setState): ' + this.state.tick); this.setState({tick: 3}); // eslint-disable-line ops.push('componentDidUpdate (after setState): ' + this.state.tick); // We're in a batch. Update hasn't flushed yet. } } render() { ops.push('render: ' + this.state.tick); instance = this; return <span prop={this.state.tick} />; } } render(<Foo />, container); jest.runAllTimers(); expect(ops).toEqual([ 'render: 0', 'componentDidMount (before setState): 0', 'componentDidMount (after setState): 0', // If the setState inside componentDidMount were deferred, there would be // no more ops. Because it has Task priority, we get these ops, too: 'render: 1', 'componentDidUpdate: 1', ]); ops = []; instance.setState({tick: 2}); jest.runAllTimers(); expect(ops).toEqual([ 'render: 2', 'componentDidUpdate: 2', 'componentDidUpdate (before setState): 2', 'componentDidUpdate (after setState): 2', // If the setState inside componentDidUpdate were deferred, there would be // no more ops. Because it has Task priority, we get these ops, too: 'render: 3', 'componentDidUpdate: 3', ]); }); it('performs Task work in the callback', () => { let container = createNodeElement('div'); class Foo extends Component { state = {step: 1}; componentDidMount() { this.setState({step: 2}, () => { // eslint-disable-line this.setState({step: 3}, () => { this.setState({step: 4}, () => { this.setState({step: 5}); }); }); }); } render() { return <span>{this.state.step}</span>; } } render(<Foo />, container); jest.runAllTimers(); expect(container.childNodes[0].childNodes[0].data).toBe('5'); }); it('should batch child/parent state updates together', () => { let container = createNodeElement('div'); let container2 = createNodeElement('div'); var parentUpdateCount = 0; class Parent extends Component { state = {x: 0}; componentDidUpdate() { parentUpdateCount++; } render() { return <div><Child ref="child" x={this.state.x} /><Child2 ref="child2" x={this.state.x} /></div>; } } var childUpdateCount = 0; class Child extends Component { state = {y: 0}; componentDidUpdate() { childUpdateCount++; } render() { return <div>{this.props.x + this.state.y}</div>; } } var child2UpdateCount = 0; class Child2 extends Component { state = {y: 0}; componentDidUpdate() { child2UpdateCount++; } render() { return <div>{this.props.x + this.state.y}</div>; } } var instance = render(<Parent />, container); var child = instance.refs.child; var child2 = instance.refs.child2; expect(instance.state.x).toBe(0); expect(child.state.y).toBe(0); expect(child2.state.y).toBe(0); function Batch() { child.setState({y: 2}); instance.setState({x: 1}); child2.setState({y: 2}); expect(instance.state.x).toBe(0); expect(child.state.y).toBe(0); expect(child2.state.y).toBe(0); expect(parentUpdateCount).toBe(0); expect(childUpdateCount).toBe(0); expect(child2UpdateCount).toBe(0); return null; } render(<Batch />, container2); jest.runAllTimers(); expect(instance.state.x).toBe(1); expect(child.state.y).toBe(2); expect(child2.state.y).toBe(2); expect(parentUpdateCount).toBe(1); // Batching reduces the number of updates here to 1. expect(childUpdateCount).toBe(1); expect(child2UpdateCount).toBe(1); }); it('does not call render after a component as been deleted', () => { let container = createNodeElement('div'); let container2 = createNodeElement('div'); var renderCount = 0; var componentB = null; class B extends Component { state = {updates: 0}; componentDidMount() { componentB = this; } render() { renderCount++; return <div />; } } class A extends Component { state = {showB: true}; render() { return this.state.showB ? <B /> : <div />; } } var component = render(<A />, container); function Batch() { // B will have scheduled an update but the batching should ensure that its // update never fires. componentB.setState({updates: 1}); component.setState({showB: false}); } render(<Batch />, container2); expect(renderCount).toBe(1); }); it('does not update one component twice when schedule in the rendering phase', () => { let container = createNodeElement('div'); let logs = []; class Child extends Component { state = { count: 0 }; componentDidUpdate() { logs.push(this.props.child); } componentDidMount() { this.setState({count: 1}); // eslint-disable-line this.setState({count: 2}); // eslint-disable-line this.setState({count: 3}); // eslint-disable-line } render() { return ( [ <span key={'1'}>{this.props.count}</span>, <span key={'2'}>{this.state.count}</span> ] ); } } class Parent1 extends Component { state = { count: 0 } componentDidUpdate() { logs.push('Parent1'); } componentDidMount() { this.setState({count: 1}); // eslint-disable-line this.setState({count: 2}); // eslint-disable-line } render() { return <Child count={this.state.count} child="Child1" />; } } class Parent2 extends Component { state = { count: 0 } shouldComponentUpdate() { return false; } componentDidUpdate() { logs.push('Parent2'); } componentDidMount() { this.setState({count: 1}); // eslint-disable-line this.setState({count: 2}); // eslint-disable-line } render() { return <Child count={this.state.count} child="Child2" />; } } class App extends Component { render() { return [<Parent1 key={'a'} />, <Parent2 key={'b'} />]; } } render(<App />, container); jest.runAllTimers(); // Child1 Child2 appears only once expect(logs).toEqual(['Child1', 'Parent1', 'Child2']); expect(container.childNodes[0].childNodes[0].data).toBe('2'); expect(container.childNodes[1].childNodes[0].data).toBe('3'); expect(container.childNodes[2].childNodes[0].data).toBe('0'); expect(container.childNodes[3].childNodes[0].data).toBe('3'); }); it('should update fragment to right position', function() { let el = createNodeElement('div'); class Hello1 extends Component { render() { if (this.props.show) { return 'hello1'; } return null; } } class Text extends Component { render() { return this.props.children; } } class Hello2 extends Component { render() { if (this.props.show) { return [<Text key="1">1</Text>, <Text key="2">2</Text>, <Text key="3">3</Text>]; } else { return [<Text key="1">1</Text>, <Text key="2">2</Text>]; } } } class MyComponent extends Component { state = { show: false } render() { return ( <div> {'foo'} <Hello1 show={this.state.show} /> <Hello2 show={this.state.show} /> </div> ); } } let inst = render(<MyComponent />, el); let container = el.childNodes[0]; let childNodes = container.childNodes; expect(childNodes.length).toBe(4); expect(childNodes[0].data).toBe('foo'); expect(childNodes[1].data).toBe(' empty '); expect(childNodes[2].data).toBe('1'); expect(childNodes[3].data).toBe('2'); inst.setState({ show: true }); jest.runAllTimers(); childNodes = container.childNodes; expect(childNodes.length).toBe(5); expect(childNodes[0].data).toBe('foo'); expect(childNodes[1].data).toBe('hello1'); expect(childNodes[2].data).toBe('1'); expect(childNodes[3].data).toBe('2'); expect(childNodes[4].data).toBe('3'); }); it('unmount dirty component', function() { let el = createNodeElement('div'); let childInstance1 = null; let childInstance2 = null; class Child1 extends Component { render() { childInstance1 = this; return <div>child1</div>; } } class Child2 extends Component { render() { childInstance2 = this; return <div>child2</div>; } } class App extends Component { componentWillReceiveProps() { childInstance1.forceUpdate(); childInstance2.forceUpdate(); } render() { if (this.props.empty) return null; return [<Child1 key="1" />, <Child2 key="2" />]; } } render(<App />, el); expect(el.childNodes[0].childNodes[0].data).toBe('child1'); expect(el.childNodes[1].childNodes[0].data).toBe('child2'); render(<App empty />, el); jest.runAllTimers(); expect(el.childNodes[0].nodeType).toEqual(8); }); });