From 45b9398fa01afe5fd2280913f80d562bc41c14c6 Mon Sep 17 00:00:00 2001 From: Nicolas DUBIEN Date: Thu, 23 Apr 2020 09:51:28 +0200 Subject: [PATCH] Detect #18657 thanks to fuzzing The current commit is totally work in progress but it already found back the issue by reporting the following counterexample: ``` [Scheduler` -> [task#2] sequence::Scheduling "8" with priority 3 resolved -> [task#1] promise::Request for "447b0ed" resolved with value "resolved 447b0ed!"`,"447b0ed",[{"priority":3,"text":"8"}], true, ["8"] => true>] ``` Reproduced by https://codesandbox.io/s/strange-frost-d4ujl?file=/src/App.js Related to #18669 --- .../__tests__/ReactSuspense-test.property.js | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 packages/react-reconciler/src/__tests__/ReactSuspense-test.property.js diff --git a/packages/react-reconciler/src/__tests__/ReactSuspense-test.property.js b/packages/react-reconciler/src/__tests__/ReactSuspense-test.property.js new file mode 100644 index 000000000000..5697ecc6fa0b --- /dev/null +++ b/packages/react-reconciler/src/__tests__/ReactSuspense-test.property.js @@ -0,0 +1,144 @@ +let React; +let ReactFeatureFlags; +let ReactNoop; +let Scheduler; +let Suspense; +let fc; + +const beforeEachAction = () => { + jest.resetModules(); + ReactFeatureFlags = require('shared/ReactFeatureFlags'); + ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false; + ReactFeatureFlags.enableSuspenseServerRenderer = true; + React = require('react'); + ReactNoop = require('react-noop-renderer'); + Scheduler = require('scheduler'); + Suspense = React.Suspense; + fc = require('fast-check'); +}; + +describe('ReactSuspense', () => { + beforeEach(beforeEachAction); + + function Text({text}) { + return {text}; + } + + function AsyncText({text, readOrThrow}) { + readOrThrow(text); + return {text}; + } + + function flushAndYieldScheduler() { + Scheduler.unstable_flushAllWithoutAsserting(); + Scheduler.unstable_clearYields(); + } + + it('should display components up-to the first unresolved one as resolved, next ones should be considered unresolved in "forward" mode', async () => { + await fc.assert( + fc + .asyncProperty( + // Scheduler able to re-order operations + fc.scheduler(), + // The initial text defined in the App component + fc.stringOf(fc.hexa()), + // Array of updates with the associated priority + fc.array( + fc.record({ + // Priority of the task + priority: fc.constantFrom( + Scheduler.unstable_ImmediatePriority, + Scheduler.unstable_UserBlockingPriority, + Scheduler.unstable_NormalPriority, + Scheduler.unstable_IdlePriority, + Scheduler.unstable_LowPriority, + ), + // Value to set for text + text: fc.stringOf(fc.hexa()), + }), + ), + // The code under test + async (s, initialText, textUpdates) => { + // We simulate a cache: string -> Promise + // It may contain successes and rejections + const cache = new Map(); + const readOrThrow = text => { + if (cache.has(text)) { + // The text has already been queried + const {promise, resolvedWith} = cache.get(text); + // Not resolved yet? + if (resolvedWith === null) throw promise; + // Resolved with error? + if (resolvedWith.error) throw resolvedWith.error; + // Success + return text; + } else { + // Not yet queried + const promise = s.schedule( + Promise.resolve(), + `Request for ${JSON.stringify(text)}`, + ); + const cachedValue = {promise, resolvedWith: null}; + promise.then( + success => (cachedValue.resolvedWith = {success}), + error => (cachedValue.resolvedWith = {error}), + ); + cache.set(text, cachedValue); + throw promise; + } + }; + + let setText; + function App() { + const [text, _setText] = React.useState(initialText); + setText = _setText; + return ; + } + + // Initial render + ReactNoop.render( + }> + + , + ); + flushAndYieldScheduler(); + expect(ReactNoop).toMatchRenderedOutput(Loading...); + + // Schedule updates into the scheduler + // Updates will not be reordered + // BUT promises that they may trigger may be scheduled in-between + s.scheduleSequence( + textUpdates.map(update => { + return { + label: `Scheduling ${JSON.stringify( + update.text, + )} with priority ${update.priority}`, + builder: async () => + Scheduler.unstable_runWithPriority(update.priority, () => { + setText(update.text); + }), + }; + }), + ); + + // Exhaust the queue of scheduled tasks + while (s.count() !== 0) { + await ReactNoop.act(async () => { + await s.waitOne(); + flushAndYieldScheduler(); + }); + } + + // Check the final value is the expected one + const lastText = + textUpdates.length > 0 + ? textUpdates[textUpdates.length - 1].text + : initialText; + expect(ReactNoop).toMatchRenderedOutput({lastText}); + }, + ) + .beforeEach(beforeEachAction), + {verbose: 2}, + ); + }); +});