UNPKG

react-native

Version:

A framework for building native apps using React

1,177 lines (1,014 loc) • 28.7 kB
/** * Copyright 2013-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * * @emails react-core */ 'use strict'; var React; var ReactNoop; var ReactFeatureFlags; describe('ReactIncrementalSideEffects', () => { beforeEach(() => { jest.resetModules(); React = require('React'); ReactNoop = require('ReactNoop'); ReactFeatureFlags = require('ReactFeatureFlags'); ReactFeatureFlags.disableNewFiberFeatures = false; }); function normalizeCodeLocInfo(str) { return str && str.replace(/\(at .+?:\d+\)/g, '(at **)'); } function div(...children) { children = children.map(c => typeof c === 'string' ? { text: c } : c); return { type: 'div', children, prop: undefined }; } function span(prop) { return { type: 'span', children: [], prop }; } it('can update child nodes of a host instance', () => { function Bar(props) { return <span>{props.text}</span>; } function Foo(props) { return ( <div> <Bar text={props.text} /> {props.text === 'World' ? <Bar text={props.text} /> : null} </div> ); } ReactNoop.render(<Foo text="Hello" />); ReactNoop.flush(); expect(ReactNoop.getChildren()).toEqual([ div(span()), ]); ReactNoop.render(<Foo text="World" />); ReactNoop.flush(); expect(ReactNoop.getChildren()).toEqual([ div(span(), span()), ]); }); it('can update child nodes of a fragment', function() { function Bar(props) { return <span>{props.text}</span>; } function Foo(props) { return ( <div> <Bar text={props.text} /> {props.text === 'World' ? [ <Bar key="a" text={props.text} />, <div key="b" />, ] : props.text === 'Hi' ? [ <div key="b" />, <Bar key="a" text={props.text} />, ] : null} <span prop="test" /> </div> ); } ReactNoop.render(<Foo text="Hello" />); ReactNoop.flush(); expect(ReactNoop.getChildren()).toEqual([ div(span(), span('test')), ]); ReactNoop.render(<Foo text="World" />); ReactNoop.flush(); expect(ReactNoop.getChildren()).toEqual([ div(span(), span(), div(), span('test')), ]); ReactNoop.render(<Foo text="Hi" />); ReactNoop.flush(); expect(ReactNoop.getChildren()).toEqual([ div(span(), div(), span(), span('test')), ]); }); it('can update child nodes rendering into text nodes', function() { function Bar(props) { return props.text; } function Foo(props) { return ( <div> <Bar text={props.text} /> {props.text === 'World' ? [ <Bar key="a" text={props.text} />, '!', ] : null} </div> ); } ReactNoop.render(<Foo text="Hello" />); ReactNoop.flush(); expect(ReactNoop.getChildren()).toEqual([ div('Hello'), ]); ReactNoop.render(<Foo text="World" />); ReactNoop.flush(); expect(ReactNoop.getChildren()).toEqual([ div('World', 'World', '!'), ]); }); it('can deletes children either components, host or text', function() { function Bar(props) { return <span prop={props.children} />; } function Foo(props) { return ( <div> {props.show ? [ <div key="a" />, <Bar key="b">Hello</Bar>, 'World', ] : []} </div> ); } ReactNoop.render(<Foo show={true} />); ReactNoop.flush(); expect(ReactNoop.getChildren()).toEqual([ div(div(), span('Hello'), 'World'), ]); ReactNoop.render(<Foo show={false} />); ReactNoop.flush(); expect(ReactNoop.getChildren()).toEqual([ div(), ]); }); it('can delete a child that changes type - implicit keys', function() { let unmounted = false; class ClassComponent extends React.Component { componentWillUnmount() { unmounted = true; } render() { return <span prop="Class" />; } } function FunctionalComponent(props) { return <span prop="Function" />; } function Foo(props) { return ( <div> {props.useClass ? <ClassComponent /> : props.useFunction ? <FunctionalComponent /> : props.useText ? 'Text' : null } Trail </div> ); } ReactNoop.render(<Foo useClass={true} />); ReactNoop.flush(); expect(ReactNoop.getChildren()).toEqual([ div(span('Class'), 'Trail'), ]); expect(unmounted).toBe(false); ReactNoop.render(<Foo useFunction={true} />); ReactNoop.flush(); expect(ReactNoop.getChildren()).toEqual([ div(span('Function'), 'Trail'), ]); expect(unmounted).toBe(true); ReactNoop.render(<Foo useText={true} />); ReactNoop.flush(); expect(ReactNoop.getChildren()).toEqual([ div('Text', 'Trail'), ]); ReactNoop.render(<Foo />); ReactNoop.flush(); expect(ReactNoop.getChildren()).toEqual([ div('Trail'), ]); }); it('can delete a child that changes type - explicit keys', function() { let unmounted = false; class ClassComponent extends React.Component { componentWillUnmount() { unmounted = true; } render() { return <span prop="Class" />; } } function FunctionalComponent(props) { return <span prop="Function" />; } function Foo(props) { return ( <div> {props.useClass ? <ClassComponent key="a" /> : props.useFunction ? <FunctionalComponent key="a" /> : null } Trail </div> ); } ReactNoop.render(<Foo useClass={true} />); ReactNoop.flush(); expect(ReactNoop.getChildren()).toEqual([ div(span('Class'), 'Trail'), ]); expect(unmounted).toBe(false); ReactNoop.render(<Foo useFunction={true} />); ReactNoop.flush(); expect(ReactNoop.getChildren()).toEqual([ div(span('Function'), 'Trail'), ]); expect(unmounted).toBe(true); ReactNoop.render(<Foo />); ReactNoop.flush(); expect(ReactNoop.getChildren()).toEqual([ div('Trail'), ]); }); it('does not update child nodes if a flush is aborted', () => { function Bar(props) { return <span prop={props.text} />; } function Foo(props) { return ( <div> <div> <Bar text={props.text} /> {props.text === 'Hello' ? <Bar text={props.text} /> : null} </div> <Bar text="Yo" /> </div> ); } ReactNoop.render(<Foo text="Hello" />); ReactNoop.flush(); expect(ReactNoop.getChildren()).toEqual([ div(div(span('Hello'), span('Hello')), span('Yo')), ]); ReactNoop.render(<Foo text="World" />); ReactNoop.flushDeferredPri(35); expect(ReactNoop.getChildren()).toEqual([ div(div(span('Hello'), span('Hello')), span('Yo')), ]); }); it('preserves a previously rendered node when deprioritized', () => { function Middle(props) { return <span prop={props.children} />; } function Foo(props) { return ( <div> <div hidden={true}> <Middle>{props.text}</Middle> </div> </div> ); } ReactNoop.render(<Foo text="foo" />); ReactNoop.flush(); expect(ReactNoop.getChildren()).toEqual([ div(div(span('foo'))), ]); ReactNoop.render(<Foo text="bar" />); ReactNoop.flushDeferredPri(20); expect(ReactNoop.getChildren()).toEqual([ div(div(span('foo'))), ]); ReactNoop.flush(); expect(ReactNoop.getChildren()).toEqual([ div(div(span('bar'))), ]); }); it('can reuse side-effects after being preempted', () => { function Bar(props) { return <span prop={props.children} />; } var middleContent = ( <div> <Bar>Hello</Bar> <Bar>World</Bar> </div> ); function Foo(props) { return ( <div hidden={true}> { props.step === 0 ? <div> <Bar>Hi</Bar> <Bar>{props.text}</Bar> </div> : middleContent } </div> ); } // Init ReactNoop.render(<Foo text="foo" step={0} />); ReactNoop.flush(); expect(ReactNoop.getChildren()).toEqual([ div(div(span('Hi'), span('foo'))), ]); // Make a quick update which will schedule low priority work to // update the middle content. ReactNoop.render(<Foo text="bar" step={1} />); ReactNoop.flushDeferredPri(30); // The tree remains unchanged. expect(ReactNoop.getChildren()).toEqual([ div(div(span('Hi'), span('foo'))), ]); // The first Bar has already completed its update but we'll interrupt it to // render some higher priority work. The middle content will bailout so // it remains untouched which means that it should reuse it next time. ReactNoop.render(<Foo text="foo" step={1} />); ReactNoop.flush(); // Since we did nothing to the middle subtree during the interuption, // we should be able to reuse the reconciliation work that we already did // without restarting. The side-effects should still be replayed. expect(ReactNoop.getChildren()).toEqual([ div(div(span('Hello'), span('World'))), ]); }); it('can reuse side-effects after being preempted, if shouldComponentUpdate is false', () => { class Bar extends React.Component { shouldComponentUpdate(nextProps) { return this.props.children !== nextProps.children; } render() { return <span prop={this.props.children} />; } } class Content extends React.Component { shouldComponentUpdate(nextProps) { return this.props.step !== nextProps.step; } render() { return ( <div> <Bar>{this.props.step === 0 ? 'Hi' : 'Hello'}</Bar> <Bar>{this.props.step === 0 ? this.props.text : 'World'}</Bar> </div> ); } } function Foo(props) { return ( <div hidden={true}> <Content step={props.step} text={props.text} /> </div> ); } // Init ReactNoop.render(<Foo text="foo" step={0} />); ReactNoop.flush(); expect(ReactNoop.getChildren()).toEqual([ div(div(span('Hi'), span('foo'))), ]); // Make a quick update which will schedule low priority work to // update the middle content. ReactNoop.render(<Foo text="bar" step={1} />); ReactNoop.flushDeferredPri(35); // The tree remains unchanged. expect(ReactNoop.getChildren()).toEqual([ div(div(span('Hi'), span('foo'))), ]); // The first Bar has already completed its update but we'll interrupt it to // render some higher priority work. The middle content will bailout so // it remains untouched which means that it should reuse it next time. ReactNoop.render(<Foo text="foo" step={1} />); ReactNoop.flush(30); // Since we did nothing to the middle subtree during the interuption, // we should be able to reuse the reconciliation work that we already did // without restarting. The side-effects should still be replayed. expect(ReactNoop.getChildren()).toEqual([ div(div(span('Hello'), span('World'))), ]); }); it('can update a completed tree before it has a chance to commit', () => { function Foo(props) { return <span prop={props.step}/>; } ReactNoop.render(<Foo step={1} />); // This should be just enough to complete the tree without committing it ReactNoop.flushDeferredPri(20); expect(ReactNoop.getChildren()).toEqual([]); // To confirm, perform one more unit of work. The tree should now be flushed. // (ReactNoop decrements the time remaining by 5 *before* returning it from // the deadline, so to perform n units of work, you need to give it 5n + 5. // TODO: This is confusing. Decrement it after.) ReactNoop.flushDeferredPri(10); expect(ReactNoop.getChildren()).toEqual([span(1)]); ReactNoop.render(<Foo step={2} />); // This should be just enough to complete the tree without committing it ReactNoop.flushDeferredPri(20); expect(ReactNoop.getChildren()).toEqual([span(1)]); // This time, before we commit the tree, we update the root component with // new props ReactNoop.render(<Foo step={3} />); // Now let's commit. We already had a commit that was pending, which will // render 2. ReactNoop.flushDeferredPri(10); expect(ReactNoop.getChildren()).toEqual([span(2)]); // If we flush the rest of the work, we should get another commit that // renders 3. If it renders 2 again, that means an update was dropped. ReactNoop.flush(); expect(ReactNoop.getChildren()).toEqual([span(3)]); }); it('updates a child even though the old props is empty', () => { function Foo(props) { return ( <div hidden={true}> <span prop={1} /> </div> ); } ReactNoop.render(<Foo />); ReactNoop.flush(); expect(ReactNoop.getChildren()).toEqual([ div(span(1)), ]); }); it('can defer side-effects and resume them later on', function() { class Bar extends React.Component { shouldComponentUpdate(nextProps) { return this.props.idx !== nextProps.idx; } render() { return <span prop={this.props.idx} />; } } function Foo(props) { return ( <div> <span prop={props.tick} /> <div hidden={true}> <Bar idx={props.idx} /> <Bar idx={props.idx + 1} /> </div> </div> ); } ReactNoop.render(<Foo tick={0} idx={0} />); ReactNoop.flushDeferredPri(40 + 25); expect(ReactNoop.getChildren()).toEqual([ div( span(0), div(/*the spans are down-prioritized and not rendered yet*/) ), ]); ReactNoop.render(<Foo tick={1} idx={0} />); ReactNoop.flushDeferredPri(35 + 25); expect(ReactNoop.getChildren()).toEqual([ div( span(1), div(/*still not rendered yet*/) ), ]); ReactNoop.flushDeferredPri(30 + 25); expect(ReactNoop.getChildren()).toEqual([ div( span(1), div( // Now we had enough time to finish the spans. span(0), span(1) ) ), ]); var innerSpanA = ReactNoop.getChildren()[0].children[1].children[1]; ReactNoop.render(<Foo tick={2} idx={1} />); ReactNoop.flushDeferredPri(30 + 25); expect(ReactNoop.getChildren()).toEqual([ div( span(2), div( // Still same old numbers. span(0), span(1) ) ), ]); ReactNoop.render(<Foo tick={3} idx={1} />); ReactNoop.flush(); expect(ReactNoop.getChildren()).toEqual([ div( span(3), div( // New numbers. span(1), span(2) ) ), ]); var innerSpanB = ReactNoop.getChildren()[0].children[1].children[1]; // This should have been an update to an existing instance, not recreation. // We verify that by ensuring that the child instance was the same as // before. expect(innerSpanA).toBe(innerSpanB); }); it('can defer side-effects and reuse them later - complex', function() { var ops = []; class Bar extends React.Component { shouldComponentUpdate(nextProps) { return this.props.idx !== nextProps.idx; } render() { ops.push('Bar'); return <span prop={this.props.idx} />; } } class Baz extends React.Component { shouldComponentUpdate(nextProps) { return this.props.idx !== nextProps.idx; } render() { ops.push('Baz'); return [<Bar idx={this.props.idx} />, <Bar idx={this.props.idx} />]; } } function Foo(props) { ops.push('Foo'); return ( <div> <span prop={props.tick} /> <div hidden={true}> <Baz idx={props.idx} /> <Baz idx={props.idx} /> <Baz idx={props.idx} /> </div> </div> ); } ReactNoop.render(<Foo tick={0} idx={0} />); ReactNoop.flushDeferredPri(65 + 5); expect(ReactNoop.getChildren()).toEqual([ div( span(0), div(/*the spans are down-prioritized and not rendered yet*/) ), ]); expect(ops).toEqual(['Foo', 'Baz', 'Bar']); ops = []; ReactNoop.render(<Foo tick={1} idx={0} />); ReactNoop.flushDeferredPri(70); expect(ReactNoop.getChildren()).toEqual([ div( span(1), div(/*still not rendered yet*/) ), ]); expect(ops).toEqual(['Foo']); ops = []; ReactNoop.flush(); expect(ReactNoop.getChildren()).toEqual([ div( span(1), div( // Now we had enough time to finish the spans. span(0), span(0), span(0), span(0), span(0), span(0) ) ), ]); expect(ops).toEqual(['Bar', 'Baz', 'Bar', 'Bar', 'Baz', 'Bar', 'Bar']); ops = []; // Now we're going to update the index but we'll only let it finish half // way through. ReactNoop.render(<Foo tick={2} idx={1} />); ReactNoop.flushDeferredPri(95); expect(ReactNoop.getChildren()).toEqual([ div( span(2), div( // Still same old numbers. span(0), span(0), span(0), span(0), span(0), span(0) ) ), ]); // We let it finish half way through. That means we'll have one fully // completed Baz, one half-way completed Baz and one fully incomplete Baz. expect(ops).toEqual(['Foo', 'Baz', 'Bar', 'Bar', 'Baz', 'Bar']); ops = []; // We'll update again, without letting the new index update yet. Only half // way through. ReactNoop.render(<Foo tick={3} idx={1} />); ReactNoop.flushDeferredPri(50); expect(ReactNoop.getChildren()).toEqual([ div( span(3), div( // Old numbers. span(0), span(0), span(0), span(0), span(0), span(0) ) ), ]); expect(ops).toEqual(['Foo']); ops = []; // We should now be able to reuse some of the work we've already done // and replay those side-effects. ReactNoop.flush(); expect(ReactNoop.getChildren()).toEqual([ div( span(3), div( // New numbers. span(1), span(1), span(1), span(1), span(1), span(1) ) ), ]); expect(ops).toEqual(['Bar', 'Baz', 'Bar', 'Bar']); }); it('deprioritizes setStates that happens within a deprioritized tree', () => { var ops = []; var barInstances = []; class Bar extends React.Component { constructor() { super(); this.state = { active: false }; barInstances.push(this); } activate() { this.setState({ active: true }); } render() { ops.push('Bar'); return <span prop={this.state.active ? 'X' : this.props.idx} />; } } function Foo(props) { ops.push('Foo'); return ( <div> <span prop={props.tick} /> <div hidden={true}> <Bar idx={props.idx} /> <Bar idx={props.idx} /> <Bar idx={props.idx} /> </div> </div> ); } ReactNoop.render(<Foo tick={0} idx={0} />); ReactNoop.flush(); expect(ReactNoop.getChildren()).toEqual([ div( span(0), div( span(0), span(0), span(0) ) ), ]); expect(ops).toEqual(['Foo', 'Bar', 'Bar', 'Bar']); ops = []; ReactNoop.render(<Foo tick={1} idx={1} />); ReactNoop.flushDeferredPri(70 + 5); expect(ReactNoop.getChildren()).toEqual([ div( // Updated. span(1), div( // Still not updated. span(0), span(0), span(0) ) ), ]); expect(ops).toEqual(['Foo', 'Bar', 'Bar']); ops = []; barInstances[0].activate(); // This should not be enough time to render the content of all the hidden // items. Including the set state since that is deprioritized. // TODO: The cycles it takes to do this could be lowered with further // optimizations. ReactNoop.flushDeferredPri(60 + 5); expect(ReactNoop.getChildren()).toEqual([ div( // Updated. span(1), div( // Still not updated. span(0), span(0), span(0) ) ), ]); expect(ops).toEqual(['Bar']); ops = []; // However, once we render fully, we will have enough time to finish it all // at once. ReactNoop.flush(); expect(ReactNoop.getChildren()).toEqual([ div( span(1), div( // Now we had enough time to finish the spans. span('X'), span(1), span(1), ) ), ]); expect(ops).toEqual(['Bar']); }); // TODO: Test that side-effects are not cut off when a work in progress node // moves to "current" without flushing due to having lower priority. Does this // even happen? Maybe a child doesn't get processed because it is lower prio? it('calls callback after update is flushed', () => { let instance; class Foo extends React.Component { constructor() { super(); instance = this; this.state = { text: 'foo' }; } render() { return <span prop={this.state.text} />; } } ReactNoop.render(<Foo />); ReactNoop.flush(); expect(ReactNoop.getChildren()).toEqual([ span('foo'), ]); let called = false; instance.setState({ text: 'bar' }, () => { expect(ReactNoop.getChildren()).toEqual([ span('bar'), ]); called = true; }); ReactNoop.flush(); expect(called).toBe(true); }); it('calls setState callback even if component bails out', () => { let instance; class Foo extends React.Component { constructor() { super(); instance = this; this.state = { text: 'foo' }; } shouldComponentUpdate(nextProps, nextState) { return this.state.text !== nextState.text; } render() { return <span prop={this.state.text} />; } } ReactNoop.render(<Foo />); ReactNoop.flush(); expect(ReactNoop.getChildren()).toEqual([ span('foo'), ]); let called = false; instance.setState({}, () => { called = true; }); ReactNoop.flush(); expect(called).toBe(true); }); // TODO: Test that callbacks are not lost if an update is preempted. it('calls componentWillUnmount after a deletion, even if nested', () => { var ops = []; class Bar extends React.Component { componentWillUnmount() { ops.push(this.props.name); } render() { return <span />; } } class Wrapper extends React.Component { componentWillUnmount() { ops.push('Wrapper'); } render() { return <Bar name={this.props.name} />; } } function Foo(props) { return ( <div> {props.show ? [ <Bar key="a" name="A" />, <Wrapper key="b" name="B" />, <div key="cd"> <Bar name="C" /> <Wrapper name="D" />, </div>, [ <Bar key="e" name="E" />, <Bar key="f" name="F" />, ], ] : []} <div> {props.show ? <Bar key="g" name="G" /> : null} </div> <Bar name="this should not unmount" /> </div> ); } ReactNoop.render(<Foo show={true} />); ReactNoop.flush(); expect(ops).toEqual([]); ReactNoop.render(<Foo show={false} />); ReactNoop.flush(); expect(ops).toEqual([ 'A', 'Wrapper', 'B', 'C', 'Wrapper', 'D', 'E', 'F', 'G', ]); }); it('calls componentDidMount/Update after insertion/update', () => { var ops = []; class Bar extends React.Component { componentDidMount() { ops.push('mount:' + this.props.name); } componentDidUpdate() { ops.push('update:' + this.props.name); } render() { return <span />; } } class Wrapper extends React.Component { componentDidMount() { ops.push('mount:wrapper-' + this.props.name); } componentDidUpdate() { ops.push('update:wrapper-' + this.props.name); } render() { return <Bar name={this.props.name} />; } } function Foo(props) { return ( <div> <Bar key="a" name="A" /> <Wrapper key="b" name="B" /> <div key="cd"> <Bar name="C" /> <Wrapper name="D" /> </div> {[ <Bar key="e" name="E" />, <Bar key="f" name="F" />, ]} <div> <Bar key="g" name="G" /> </div> </div> ); } ReactNoop.render(<Foo />); ReactNoop.flush(); expect(ops).toEqual([ 'mount:A', 'mount:B', 'mount:wrapper-B', 'mount:C', 'mount:D', 'mount:wrapper-D', 'mount:E', 'mount:F', 'mount:G', ]); ops = []; ReactNoop.render(<Foo />); ReactNoop.flush(); expect(ops).toEqual([ 'update:A', 'update:B', 'update:wrapper-B', 'update:C', 'update:D', 'update:wrapper-D', 'update:E', 'update:F', 'update:G', ]); }); it('invokes ref callbacks after insertion/update/unmount', () => { spyOn(console, 'error'); var classInstance = null; var ops = []; class ClassComponent extends React.Component { render() { classInstance = this; return <span />; } } function FunctionalComponent(props) { return <span />; } function Foo(props) { return ( props.show ? <div> <ClassComponent ref={n => ops.push(n)} /> <FunctionalComponent ref={n => ops.push(n)} /> <div ref={n => ops.push(n)} /> </div> : null ); } ReactNoop.render(<Foo show={true} />); ReactNoop.flush(); expect(ops).toEqual([ classInstance, // no call for functional components div(), ]); ops = []; // Refs that switch function instances get reinvoked ReactNoop.render(<Foo show={true} />); ReactNoop.flush(); expect(ops).toEqual([ // detach all refs that switched handlers first. null, null, // reattach as a separate phase classInstance, div(), ]); ops = []; ReactNoop.render(<Foo show={false} />); ReactNoop.flush(); expect(ops).toEqual([ // unmount null, null, ]); expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe( 'Warning: Stateless function components cannot be given refs. ' + 'Attempts to access this ref will fail.\n\nCheck the render method ' + 'of `Foo`.\n' + ' in FunctionalComponent (at **)\n' + ' in div (at **)\n' + ' in Foo (at **)' ); }); // TODO: Test that mounts, updates, refs, unmounts and deletions happen in the // expected way for aborted and resumed render life-cycles. it('supports string refs', () => { var fooInstance = null; class Bar extends React.Component { componentDidMount() { this.test = 'test'; } render() { return <div />; } } class Foo extends React.Component { render() { fooInstance = this; return <Bar ref="bar" />; } } ReactNoop.render(<Foo />); ReactNoop.flush(); expect(fooInstance.refs.bar.test).toEqual('test'); }); });