| 1 | let React; |
| 2 | let ReactNoop; |
| 3 | let Scheduler; |
| 4 | let waitForAll; |
| 5 | let assertLog; |
| 6 | let ReactCache; |
| 7 | let Suspense; |
| 8 | let TextResource; |
| 9 | let act; |
| 10 | |
| 11 | describe('ReactBlockingMode', () => { |
| 12 | beforeEach(() => { |
| 13 | jest.resetModules(); |
| 14 | React = require('react'); |
| 15 | ReactNoop = require('react-noop-renderer'); |
| 16 | Scheduler = require('scheduler'); |
| 17 | ReactCache = require('react-cache'); |
| 18 | Suspense = React.Suspense; |
| 19 | |
| 20 | const InternalTestUtils = require('internal-test-utils'); |
| 21 | waitForAll = InternalTestUtils.waitForAll; |
| 22 | assertLog = InternalTestUtils.assertLog; |
| 23 | act = InternalTestUtils.act; |
| 24 | |
| 25 | TextResource = ReactCache.unstable_createResource( |
| 26 | ([text, ms = 0]) => { |
| 27 | return new Promise((resolve, reject) => |
| 28 | setTimeout(() => { |
| 29 | Scheduler.log(`Promise resolved [${text}]`); |
| 30 | resolve(text); |
| 31 | }, ms), |
| 32 | ); |
| 33 | }, |
| 34 | ([text, ms]) => text, |
| 35 | ); |
| 36 | }); |
| 37 | |
| 38 | function Text(props) { |
| 39 | Scheduler.log(props.text); |
| 40 | return props.text; |
| 41 | } |
| 42 | |
| 43 | function AsyncText(props) { |
| 44 | const text = props.text; |
| 45 | try { |
| 46 | TextResource.read([props.text, props.ms]); |
| 47 | Scheduler.log(text); |
| 48 | return props.text; |
| 49 | } catch (promise) { |
| 50 | if (typeof promise.then === 'function') { |
| 51 | Scheduler.log(`Suspend! [${text}]`); |
| 52 | } else { |
| 53 | Scheduler.log(`Error! [${text}]`); |
| 54 | } |
| 55 | throw promise; |
| 56 | } |
| 57 | } |
| 58 | |
| 59 | it('updates flush without yielding in the next event', async () => { |
| 60 | const root = ReactNoop.createRoot(); |
| 61 | |
| 62 | root.render( |
| 63 | <> |
| 64 | <Text text="A" /> |
| 65 | <Text text="B" /> |
| 66 | <Text text="C" /> |
| 67 | </>, |
| 68 | ); |
| 69 | |
| 70 | // Nothing should have rendered yet |
| 71 | expect(root).toMatchRenderedOutput(null); |
| 72 | |
| 73 | await waitForAll(['A', 'B', 'C']); |
| 74 | expect(root).toMatchRenderedOutput('ABC'); |
| 75 | }); |
| 76 | |
| 77 | it('layout updates flush synchronously in same event', async () => { |
| 78 | const {useLayoutEffect} = React; |
| 79 | |
| 80 | function App() { |
| 81 | useLayoutEffect(() => { |
| 82 | Scheduler.log('Layout effect'); |
| 83 | }); |
| 84 | return <Text text="Hi" />; |
| 85 | } |
| 86 | |
| 87 | const root = ReactNoop.createRoot(); |
| 88 | root.render(<App />); |
| 89 | expect(root).toMatchRenderedOutput(null); |
| 90 | assertLog([]); |
| 91 | |
| 92 | await waitForAll(['Hi', 'Layout effect']); |
| 93 | expect(root).toMatchRenderedOutput('Hi'); |
| 94 | }); |
| 95 | |
| 96 | it('uses proper Suspense semantics, not legacy ones', async () => { |
| 97 | const root = ReactNoop.createRoot(); |
| 98 | root.render( |
| 99 | <Suspense fallback={<Text text="Loading..." />}> |
| 100 | <span> |
| 101 | <Text text="A" /> |
| 102 | </span> |
| 103 | <span> |
| 104 | <AsyncText text="B" /> |
| 105 | </span> |
| 106 | <span> |
| 107 | <Text text="C" /> |
| 108 | </span> |
| 109 | </Suspense>, |
| 110 | ); |
| 111 | |
| 112 | await waitForAll([ |
| 113 | 'A', |
| 114 | 'Suspend! [B]', |
| 115 | 'Loading...', |
| 116 | // pre-warming |
| 117 | 'A', |
| 118 | 'Suspend! [B]', |
| 119 | 'C', |
| 120 | ]); |
| 121 | // In Legacy Mode, A and B would mount in a hidden primary tree. In |
| 122 | // Concurrent Mode, nothing in the primary tree should mount. But the |
| 123 | // fallback should mount immediately. |
| 124 | expect(root).toMatchRenderedOutput('Loading...'); |
| 125 | |
| 126 | await act(() => jest.advanceTimersByTime(1000)); |
| 127 | assertLog(['Promise resolved [B]', 'A', 'B', 'C']); |
| 128 | expect(root).toMatchRenderedOutput( |
| 129 | <> |
| 130 | <span>A</span> |
| 131 | <span>B</span> |
| 132 | <span>C</span> |
| 133 | </>, |
| 134 | ); |
| 135 | }); |
| 136 | |
| 137 | it('flushSync does not flush batched work', async () => { |
| 138 | const {useState, forwardRef, useImperativeHandle} = React; |
| 139 | const root = ReactNoop.createRoot(); |
| 140 | |
| 141 | const Foo = forwardRef(({label}, ref) => { |
| 142 | const [step, setStep] = useState(0); |
| 143 | useImperativeHandle(ref, () => ({setStep})); |
| 144 | return <Text text={label + step} />; |
| 145 | }); |
| 146 | |
| 147 | const foo1 = React.createRef(null); |
| 148 | const foo2 = React.createRef(null); |
| 149 | root.render( |
| 150 | <> |
| 151 | <Foo label="A" ref={foo1} /> |
| 152 | <Foo label="B" ref={foo2} /> |
| 153 | </>, |
| 154 | ); |
| 155 | |
| 156 | await waitForAll(['A0', 'B0']); |
| 157 | expect(root).toMatchRenderedOutput('A0B0'); |
| 158 | |
| 159 | // Schedule a batched update to the first sibling |
| 160 | ReactNoop.batchedUpdates(() => foo1.current.setStep(1)); |
| 161 | |
| 162 | // Before it flushes, update the second sibling inside flushSync |
| 163 | ReactNoop.batchedUpdates(() => |
| 164 | ReactNoop.flushSync(() => { |
| 165 | foo2.current.setStep(1); |
| 166 | }), |
| 167 | ); |
| 168 | |
| 169 | // Now flush the first update |
| 170 | assertLog(['A1', 'B1']); |
| 171 | expect(root).toMatchRenderedOutput('A1B1'); |
| 172 | }); |
| 173 | }); |