UNPKG

arkit

Version:

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

682 lines (592 loc) 20.6 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'); let ReactFeatureFlags = require('shared/ReactFeatureFlags'); let ReactDOM; const ConcurrentMode = React.unstable_ConcurrentMode; const setUntrackedInputValue = Object.getOwnPropertyDescriptor( HTMLInputElement.prototype, 'value', ).set; describe('ReactDOMFiberAsync', () => { let container; beforeEach(() => { // TODO pull this into helper method, reduce repetition. // mock the browser APIs which are used in schedule: // - requestAnimationFrame should pass the DOMHighResTimeStamp argument // - calling 'window.postMessage' should actually fire postmessage handlers global.requestAnimationFrame = function(cb) { return setTimeout(() => { cb(Date.now()); }); }; const originalAddEventListener = global.addEventListener; let postMessageCallback; global.addEventListener = function(eventName, callback, useCapture) { if (eventName === 'message') { postMessageCallback = callback; } else { originalAddEventListener(eventName, callback, useCapture); } }; global.postMessage = function(messageKey, targetOrigin) { const postMessageEvent = {source: window, data: messageKey}; if (postMessageCallback) { postMessageCallback(postMessageEvent); } }; jest.resetModules(); container = document.createElement('div'); ReactDOM = require('react-dom'); document.body.appendChild(container); }); afterEach(() => { document.body.removeChild(container); }); it('renders synchronously by default', () => { const ops = []; ReactDOM.render(<div>Hi</div>, container, () => { ops.push(container.textContent); }); ReactDOM.render(<div>Bye</div>, container, () => { ops.push(container.textContent); }); expect(ops).toEqual(['Hi', 'Bye']); }); it('does not perform deferred updates synchronously', () => { let inputRef = React.createRef(); let asyncValueRef = React.createRef(); let syncValueRef = React.createRef(); class Counter extends React.Component { state = {asyncValue: '', syncValue: ''}; handleChange = e => { const nextValue = e.target.value; requestIdleCallback(() => { this.setState({ asyncValue: nextValue, }); // It should not be flushed yet. expect(asyncValueRef.current.textContent).toBe(''); }); this.setState({ syncValue: nextValue, }); }; render() { return ( <div> <input ref={inputRef} onChange={this.handleChange} defaultValue="" /> <p ref={asyncValueRef}>{this.state.asyncValue}</p> <p ref={syncValueRef}>{this.state.syncValue}</p> </div> ); } } ReactDOM.render( <ConcurrentMode> <Counter /> </ConcurrentMode>, container, ); expect(asyncValueRef.current.textContent).toBe(''); expect(syncValueRef.current.textContent).toBe(''); setUntrackedInputValue.call(inputRef.current, 'hello'); inputRef.current.dispatchEvent(new MouseEvent('input', {bubbles: true})); // Should only flush non-deferred update. expect(asyncValueRef.current.textContent).toBe(''); expect(syncValueRef.current.textContent).toBe('hello'); // Should flush both updates now. jest.runAllTimers(); expect(asyncValueRef.current.textContent).toBe('hello'); expect(syncValueRef.current.textContent).toBe('hello'); }); describe('with feature flag disabled', () => { beforeEach(() => { jest.resetModules(); ReactFeatureFlags = require('shared/ReactFeatureFlags'); ReactDOM = require('react-dom'); }); it('renders synchronously', () => { ReactDOM.render( <ConcurrentMode> <div>Hi</div> </ConcurrentMode>, container, ); expect(container.textContent).toEqual('Hi'); ReactDOM.render( <ConcurrentMode> <div>Bye</div> </ConcurrentMode>, container, ); expect(container.textContent).toEqual('Bye'); }); }); describe('with feature flag enabled', () => { beforeEach(() => { jest.resetModules(); ReactFeatureFlags = require('shared/ReactFeatureFlags'); ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; ReactDOM = require('react-dom'); }); it('createRoot makes the entire tree async', () => { const root = ReactDOM.unstable_createRoot(container); root.render(<div>Hi</div>); expect(container.textContent).toEqual(''); jest.runAllTimers(); expect(container.textContent).toEqual('Hi'); root.render(<div>Bye</div>); 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 <div>{this.state.step}</div>; } } const root = ReactDOM.unstable_createRoot(container); root.render(<Component />); 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('ConcurrentMode creates an async subtree', () => { let instance; class Component extends React.Component { state = {step: 0}; render() { instance = this; return <div>{this.state.step}</div>; } } ReactDOM.render( <ConcurrentMode> <Component /> </ConcurrentMode>, 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', () => { let instance; class Child extends React.Component { state = {step: 0}; render() { instance = this; return <div>{this.state.step}</div>; } } ReactDOM.render( <div> <ConcurrentMode> <Child /> </ConcurrentMode> </div>, 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 <span>{this.state.text}</span>; } } ReactDOM.render(<Component />, 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 <span>{this.state.text}</span>; } } ReactDOM.render(<Component />, 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(<Component />, container); // Update expect(() => ReactDOM.render(<Component />, 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.Component { state = {text: ''}; push(val) { this.setState(state => ({text: state.text + val})); } componentDidUpdate() { ops.push(this.state.text); } render() { instance = this; return <span>{this.state.text}</span>; } } ReactDOM.render( <ConcurrentMode> <Component /> </ConcurrentMode>, 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']); }); it('flushControlled flushes updates before yielding to browser', () => { let inst; class Counter extends React.Component { state = {counter: 0}; increment = () => this.setState(state => ({counter: state.counter + 1})); render() { inst = this; return this.state.counter; } } ReactDOM.render( <ConcurrentMode> <Counter /> </ConcurrentMode>, container, ); expect(container.textContent).toEqual('0'); // Test that a normal update is async inst.increment(); expect(container.textContent).toEqual('0'); jest.runAllTimers(); expect(container.textContent).toEqual('1'); let ops = []; ReactDOM.unstable_flushControlled(() => { inst.increment(); ReactDOM.unstable_flushControlled(() => { inst.increment(); ops.push('end of inner flush: ' + container.textContent); }); ops.push('end of outer flush: ' + container.textContent); }); ops.push('after outer flush: ' + container.textContent); expect(ops).toEqual([ 'end of inner flush: 1', 'end of outer flush: 1', 'after outer flush: 3', ]); }); it('flushControlled does not flush until end of outermost batchedUpdates', () => { let inst; class Counter extends React.Component { state = {counter: 0}; increment = () => this.setState(state => ({counter: state.counter + 1})); render() { inst = this; return this.state.counter; } } ReactDOM.render(<Counter />, container); let ops = []; ReactDOM.unstable_batchedUpdates(() => { inst.increment(); ReactDOM.unstable_flushControlled(() => { inst.increment(); ops.push('end of flushControlled fn: ' + container.textContent); }); ops.push('end of batchedUpdates fn: ' + container.textContent); }); ops.push('after batchedUpdates: ' + container.textContent); expect(ops).toEqual([ 'end of flushControlled fn: 0', 'end of batchedUpdates fn: 0', 'after batchedUpdates: 2', ]); }); it('flushControlled returns nothing', () => { // In the future, we may want to return a thenable "work" object. let inst; class Counter extends React.Component { state = {counter: 0}; increment = () => this.setState(state => ({counter: state.counter + 1})); render() { inst = this; return this.state.counter; } } ReactDOM.render(<Counter />, container); expect(container.textContent).toEqual('0'); const returnValue = ReactDOM.unstable_flushControlled(() => { inst.increment(); return 'something'; }); expect(container.textContent).toEqual('1'); expect(returnValue).toBe(undefined); }); it('ignores discrete events on a pending removed element', () => { const disableButtonRef = React.createRef(); const submitButtonRef = React.createRef(); let formSubmitted = false; class Form extends React.Component { state = {active: true}; disableForm = () => { this.setState({active: false}); }; submitForm = () => { formSubmitted = true; // This should not get invoked }; render() { return ( <div> <button onClick={this.disableForm} ref={disableButtonRef}> Disable </button> {this.state.active ? ( <button onClick={this.submitForm} ref={submitButtonRef}> Submit </button> ) : null} </div> ); } } const root = ReactDOM.unstable_createRoot(container); root.render(<Form />); // Flush jest.runAllTimers(); let disableButton = disableButtonRef.current; expect(disableButton.tagName).toBe('BUTTON'); // Dispatch a click event on the Disable-button. let firstEvent = document.createEvent('Event'); firstEvent.initEvent('click', true, true); disableButton.dispatchEvent(firstEvent); // There should now be a pending update to disable the form. // This should not have flushed yet since it's in concurrent mode. let submitButton = submitButtonRef.current; expect(submitButton.tagName).toBe('BUTTON'); // In the meantime, we can dispatch a new client event on the submit button. let secondEvent = document.createEvent('Event'); secondEvent.initEvent('click', true, true); // This should force the pending update to flush which disables the submit button before the event is invoked. submitButton.dispatchEvent(secondEvent); // Therefore the form should never have been submitted. expect(formSubmitted).toBe(false); expect(submitButtonRef.current).toBe(null); }); it('ignores discrete events on a pending removed event listener', () => { const disableButtonRef = React.createRef(); const submitButtonRef = React.createRef(); let formSubmitted = false; class Form extends React.Component { state = {active: true}; disableForm = () => { this.setState({active: false}); }; submitForm = () => { formSubmitted = true; // This should not get invoked }; disabledSubmitForm = () => { // The form is disabled. }; render() { return ( <div> <button onClick={this.disableForm} ref={disableButtonRef}> Disable </button> <button onClick={ this.state.active ? this.submitForm : this.disabledSubmitForm } ref={submitButtonRef}> Submit </button>{' '} : null} </div> ); } } const root = ReactDOM.unstable_createRoot(container); root.render(<Form />); // Flush jest.runAllTimers(); let disableButton = disableButtonRef.current; expect(disableButton.tagName).toBe('BUTTON'); // Dispatch a click event on the Disable-button. let firstEvent = document.createEvent('Event'); firstEvent.initEvent('click', true, true); disableButton.dispatchEvent(firstEvent); // There should now be a pending update to disable the form. // This should not have flushed yet since it's in concurrent mode. let submitButton = submitButtonRef.current; expect(submitButton.tagName).toBe('BUTTON'); // In the meantime, we can dispatch a new client event on the submit button. let secondEvent = document.createEvent('Event'); secondEvent.initEvent('click', true, true); // This should force the pending update to flush which disables the submit button before the event is invoked. submitButton.dispatchEvent(secondEvent); // Therefore the form should never have been submitted. expect(formSubmitted).toBe(false); }); it('uses the newest discrete events on a pending changed event listener', () => { const enableButtonRef = React.createRef(); const submitButtonRef = React.createRef(); let formSubmitted = false; class Form extends React.Component { state = {active: false}; enableForm = () => { this.setState({active: true}); }; submitForm = () => { formSubmitted = true; // This should happen }; render() { return ( <div> <button onClick={this.enableForm} ref={enableButtonRef}> Enable </button> <button onClick={this.state.active ? this.submitForm : null} ref={submitButtonRef}> Submit </button>{' '} : null} </div> ); } } const root = ReactDOM.unstable_createRoot(container); root.render(<Form />); // Flush jest.runAllTimers(); let enableButton = enableButtonRef.current; expect(enableButton.tagName).toBe('BUTTON'); // Dispatch a click event on the Enable-button. let firstEvent = document.createEvent('Event'); firstEvent.initEvent('click', true, true); enableButton.dispatchEvent(firstEvent); // There should now be a pending update to enable the form. // This should not have flushed yet since it's in concurrent mode. let submitButton = submitButtonRef.current; expect(submitButton.tagName).toBe('BUTTON'); // In the meantime, we can dispatch a new client event on the submit button. let secondEvent = document.createEvent('Event'); secondEvent.initEvent('click', true, true); // This should force the pending update to flush which enables the submit button before the event is invoked. submitButton.dispatchEvent(secondEvent); // Therefore the form should have been submitted. expect(formSubmitted).toBe(true); }); }); });