diff --git a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js index 18a8f751b8203..b5ce125f5cbb2 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js @@ -263,6 +263,98 @@ describe('ReactSuspense', () => { expect(root).toMatchRenderedOutput('AsyncAfter SuspenseSibling'); }); + it('interrupts current render if promise resolves between the render and commit phases', () => { + let didResolve = false; + let listeners = []; + + const thenable = { + then(resolve) { + if (!didResolve) { + listeners.push(resolve); + } else { + resolve(); + } + }, + }; + + function resolveThenable() { + didResolve = true; + listeners.forEach(l => l()); + } + + function Async() { + if (!didResolve) { + Scheduler.unstable_yieldValue('Suspend!'); + throw thenable; + } + Scheduler.unstable_yieldValue('Async'); + return 'Async'; + } + + const root = ReactTestRenderer.create( + <> + }> + + + + + , + { + unstable_isConcurrent: true, + }, + ); + expect(Scheduler).toFlushAndYieldThrough([ + 'Suspend!', + 'Loading...', + 'Initial', + 'Sibling', + ]); + expect(root).toMatchRenderedOutput(null); + + // this is really convoluted, but some way to interleave code between the + // continuation being yielded and it getting evaluated is required + // + // that is something that _can_ happen on the real scheduler, if: + // - the frame time budget expires while the last ReactFiberWorkLoop.workLoop + // is evaluated + // - the workLoop yielded its commitRoot continuation, which doesn't get + // evaluated as the Scheduler.workLoop ran out of time + // - _all_ pending thenables settled between this yielding and the next + // frame + // + // but there doesn't seem to be a way to simulate it on the mock scheduler, + // other than this brute force approach. + + // setting this to false makes the test pass + const interleave = true; + + const cb = Scheduler.unstable_getFirstCallbackNode().callback; + Scheduler.unstable_getFirstCallbackNode().callback = (...args) => { + const next = cb(...args); + return () => { + // the thenable resolves after reconciliation, but before commit + // the continuation should be bound to commitRoot but we can't + // assert on that + if (interleave) { + resolveThenable(); + Scheduler.unstable_yieldValue('Resolved'); + } + return next(); + }; + }; + // if this resolveThenable runs instead, all is fine + if (!interleave) { + // it's ok to have this run as well, to finish the first commit: + // expect(Scheduler).toFlushWithoutYielding(); + resolveThenable(); + } else { + expect(Scheduler).toFlushAndYieldThrough(['Resolved']); + } + + expect(Scheduler).toFlushAndYield(['Async']); + expect(root).toMatchRenderedOutput('AsyncInitialSibling'); + }); + it('mounts a lazy class component in non-concurrent mode', async () => { class Class extends React.Component { componentDidMount() {