var React = require('react'); var ReactFeatureFlags = require('ReactFeatureFlags'); var ReactDOM; var AsyncComponent = React.unstable_AsyncComponent; describe('ReactDOMFiberAsync', () => { var container; beforeEach(() => { container = document.createElement('div'); ReactDOM = require('react-dom'); }); it('renders synchronously by default', () => { var ops = []; ReactDOM.render(
Hi
, container, () => { ops.push(container.textContent); }); ReactDOM.render(
Bye
, container, () => { ops.push(container.textContent); }); expect(ops).toEqual(['Hi', 'Bye']); }); describe('with feature flag disabled', () => { beforeEach(() => { jest.resetModules(); ReactFeatureFlags = require('ReactFeatureFlags'); container = document.createElement('div'); ReactFeatureFlags.enableAsyncSubtreeAPI = false; ReactDOM = require('react-dom'); }); it('renders synchronously', () => { ReactDOM.render(
Hi
, container, ); expect(container.textContent).toEqual('Hi'); ReactDOM.render(
Bye
, container, ); expect(container.textContent).toEqual('Bye'); }); }); describe('with feature flag enabled', () => { beforeEach(() => { jest.resetModules(); ReactFeatureFlags = require('ReactFeatureFlags'); container = document.createElement('div'); ReactFeatureFlags.enableAsyncSubtreeAPI = true; ReactDOM = require('react-dom'); }); it('AsyncComponent at the root makes the entire tree async', () => { ReactDOM.render(
Hi
, container, ); expect(container.textContent).toEqual(''); jest.runAllTimers(); expect(container.textContent).toEqual('Hi'); ReactDOM.render(
Bye
, container, ); expect(container.textContent).toEqual('Hi'); jest.runAllTimers(); expect(container.textContent).toEqual('Bye'); }); it('updates inside an async tree are async by default', () => { let instance; class Component extends React.Component { state = {step: 0}; render() { instance = this; return
{this.state.step}
; } } ReactDOM.render( , container, ); expect(container.textContent).toEqual(''); jest.runAllTimers(); expect(container.textContent).toEqual('0'); instance.setState({step: 1}); expect(container.textContent).toEqual('0'); jest.runAllTimers(); expect(container.textContent).toEqual('1'); }); it('AsyncComponent creates an async subtree', () => { let instance; class Component extends React.unstable_AsyncComponent { state = {step: 0}; render() { instance = this; return
{this.state.step}
; } } ReactDOM.render(
, container); jest.runAllTimers(); instance.setState({step: 1}); expect(container.textContent).toEqual('0'); jest.runAllTimers(); expect(container.textContent).toEqual('1'); }); it('updates inside an async subtree are async by default', () => { class Component extends React.unstable_AsyncComponent { render() { return ; } } let instance; class Child extends React.Component { state = {step: 0}; render() { instance = this; return
{this.state.step}
; } } ReactDOM.render(
, container); jest.runAllTimers(); instance.setState({step: 1}); expect(container.textContent).toEqual('0'); jest.runAllTimers(); expect(container.textContent).toEqual('1'); }); it('flushSync batches sync updates and flushes them at the end of the batch', () => { let ops = []; let instance; class Component extends React.Component { state = {text: ''}; push(val) { this.setState(state => ({text: state.text + val})); } componentDidUpdate() { ops.push(this.state.text); } render() { instance = this; return {this.state.text}; } } ReactDOM.render(, container); instance.push('A'); expect(ops).toEqual(['A']); expect(container.textContent).toEqual('A'); ReactDOM.flushSync(() => { instance.push('B'); instance.push('C'); // Not flushed yet expect(container.textContent).toEqual('A'); expect(ops).toEqual(['A']); }); expect(container.textContent).toEqual('ABC'); expect(ops).toEqual(['A', 'ABC']); instance.push('D'); expect(container.textContent).toEqual('ABCD'); expect(ops).toEqual(['A', 'ABC', 'ABCD']); }); it('flushSync flushes updates even if nested inside another flushSync', () => { let ops = []; let instance; class Component extends React.Component { state = {text: ''}; push(val) { this.setState(state => ({text: state.text + val})); } componentDidUpdate() { ops.push(this.state.text); } render() { instance = this; return {this.state.text}; } } ReactDOM.render(, container); instance.push('A'); expect(ops).toEqual(['A']); expect(container.textContent).toEqual('A'); ReactDOM.flushSync(() => { instance.push('B'); instance.push('C'); // Not flushed yet expect(container.textContent).toEqual('A'); expect(ops).toEqual(['A']); ReactDOM.flushSync(() => { instance.push('D'); }); // The nested flushSync caused everything to flush. expect(container.textContent).toEqual('ABCD'); expect(ops).toEqual(['A', 'ABCD']); }); expect(container.textContent).toEqual('ABCD'); expect(ops).toEqual(['A', 'ABCD']); }); it('flushSync throws if already performing work', () => { class Component extends React.Component { componentDidUpdate() { ReactDOM.flushSync(() => {}); } render() { return null; } } // Initial mount ReactDOM.render(, container); // Update expect(() => ReactDOM.render(, container)).toThrow( 'flushSync was called from inside a lifecycle method', ); }); it('flushSync flushes updates before end of the tick', () => { let ops = []; let instance; class Component extends React.unstable_AsyncComponent { state = {text: ''}; push(val) { this.setState(state => ({text: state.text + val})); } componentDidUpdate() { ops.push(this.state.text); } render() { instance = this; return {this.state.text}; } } ReactDOM.render(, container); jest.runAllTimers(); // Updates are async by default instance.push('A'); expect(ops).toEqual([]); expect(container.textContent).toEqual(''); ReactDOM.flushSync(() => { instance.push('B'); instance.push('C'); // Not flushed yet expect(container.textContent).toEqual(''); expect(ops).toEqual([]); }); // Only the active updates have flushed expect(container.textContent).toEqual('BC'); expect(ops).toEqual(['BC']); instance.push('D'); expect(container.textContent).toEqual('BC'); expect(ops).toEqual(['BC']); // Flush the async updates jest.runAllTimers(); expect(container.textContent).toEqual('ABCD'); expect(ops).toEqual(['BC', 'ABCD']); }); }); });