Repository: facebook/react
Pull request: Remove timers from ReactDOMSuspensePlaceholder tests

Our internal `act` implementation flushes Jest's fake timer queue as a way to force Suspense fallbacks to appear. So we should avoid using timers in our internal tests.

Changed files:
filename: packages/react-dom/src/__tests__/ReactDOMSuspensePlaceholder-test.js
patch: @@ -12,10 +12,9 @@
 let React;
 let ReactDOM;
 let Suspense;
-let ReactCache;
 let Scheduler;
-let TextResource;
 let act;
+let textCache;
 
 describe('ReactDOMSuspensePlaceholder', () => {
   let container;
@@ -24,48 +23,78 @@ describe('ReactDOMSuspensePlaceholder', () => {
     jest.resetModules();
     React = require('react');
     ReactDOM = require('react-dom');
-    ReactCache = require('react-cache');
     Scheduler = require('scheduler');
     act = require('internal-test-utils').act;
     Suspense = React.Suspense;
     container = document.createElement('div');
     document.body.appendChild(container);
 
-    TextResource = ReactCache.unstable_createResource(
-      ([text, ms = 0]) => {
-        return new Promise((resolve, reject) =>
-          setTimeout(() => {
-            resolve(text);
-          }, ms),
-        );
-      },
-      ([text, ms]) => text,
-    );
+    textCache = new Map();
   });
 
   afterEach(() => {
     document.body.removeChild(container);
   });
 
-  function advanceTimers(ms) {
-    // Note: This advances Jest's virtual time but not React's. Use
-    // ReactNoop.expire for that.
-    if (typeof ms !== 'number') {
-      throw new Error('Must specify ms');
+  function resolveText(text) {
+    const record = textCache.get(text);
+    if (record === undefined) {
+      const newRecord = {
+        status: 'resolved',
+        value: text,
+      };
+      textCache.set(text, newRecord);
+    } else if (record.status === 'pending') {
+      const thenable = record.value;
+      record.status = 'resolved';
+      record.value = text;
+      thenable.pings.forEach(t => t());
+    }
+  }
+
+  function readText(text) {
+    const record = textCache.get(text);
+    if (record !== undefined) {
+      switch (record.status) {
+        case 'pending':
+          Scheduler.log(`Suspend! [${text}]`);
+          throw record.value;
+        case 'rejected':
+          throw record.value;
+        case 'resolved':
+          return record.value;
+      }
+    } else {
+      Scheduler.log(`Suspend! [${text}]`);
+      const thenable = {
+        pings: [],
+        then(resolve) {
+          if (newRecord.status === 'pending') {
+            thenable.pings.push(resolve);
+          } else {
+            Promise.resolve().then(() => resolve(newRecord.value));
+          }
+        },
+      };
+
+      const newRecord = {
+        status: 'pending',
+        value: thenable,
+      };
+      textCache.set(text, newRecord);
+
+      throw thenable;
     }
-    jest.advanceTimersByTime(ms);
-    // Wait until the end of the current tick
-    // We cannot use a timer since we're faking them
-    return Promise.resolve().then(() => {});
   }
 
-  function Text(props) {
-    return props.text;
+  function Text({text}) {
+    Scheduler.log(text);
+    return text;
   }
 
-  function AsyncText(props) {
-    const text = props.text;
-    TextResource.read([props.text, props.ms]);
+  function AsyncText({text}) {
+    readText(text);
+    Scheduler.log(text);
     return text;
   }
 
@@ -82,7 +111,7 @@ describe('ReactDOMSuspensePlaceholder', () => {
             <Text text="A" />
           </div>
           <div ref={divs[1]}>
-            <AsyncText ms={500} text="B" />
+            <AsyncText text="B" />
           </div>
           <div style={{display: 'inline'}} ref={divs[2]}>
             <Text text="C" />
@@ -95,9 +124,9 @@ describe('ReactDOMSuspensePlaceholder', () => {
     expect(window.getComputedStyle(divs[1].current).display).toEqual('none');
     expect(window.getComputedStyle(divs[2].current).display).toEqual('none');
 
-    await advanceTimers(500);
-
-    Scheduler.unstable_flushAll();
+    await act(async () => {
+      await resolveText('B');
+    });
 
     expect(window.getComputedStyle(divs[0].current).display).toEqual('block');
     expect(window.getComputedStyle(divs[1].current).display).toEqual('block');
@@ -110,17 +139,17 @@ describe('ReactDOMSuspensePlaceholder', () => {
       return (
         <Suspense fallback={<Text text="Loading..." />}>
           <Text text="A" />
-          <AsyncText ms={500} text="B" />
+          <AsyncText text="B" />
           <Text text="C" />
         </Suspense>
       );
     }
     ReactDOM.render(<App />, container);
     expect(container.textContent).toEqual('Loading...');
 
-    await advanceTimers(500);
-
-    Scheduler.unstable_flushAll();
+    await act(async () => {
+      await resolveText('B');
+    });
 
     expect(container.textContent).toEqual('ABC');
   });
@@ -147,7 +176,7 @@ describe('ReactDOMSuspensePlaceholder', () => {
           <Suspense fallback={<Text text="Loading..." />}>
             <Sibling>Sibling</Sibling>
             <span>
-              <AsyncText ms={500} text="Async" />
+              <AsyncText text="Async" />
             </span>
           </Suspense>
         );
@@ -161,8 +190,16 @@ describe('ReactDOMSuspensePlaceholder', () => {
           '"display: none;"></span>Loading...',
       );
 
+      // Update the inline display style. It will be overridden because it's
+      // inside a hidden fallback.
       await act(async () => setIsVisible(true));
+      expect(container.innerHTML).toEqual(
+        '<span style="display: none;">Sibling</span><span style=' +
+          '"display: none;"></span>Loading...',
+      );
 
+      // Unsuspend. The style should now match the inline prop.
+      await act(async () => resolveText('Async'));
       expect(container.innerHTML).toEqual(
         '<span style="display: inline;">Sibling</span><span style="">Async</span>',
       );