Repository: facebook/react
Pull request: [Scheduler] Add tests for isInputPending

This feature was already implemented but we didn't have any tests. So I wrote some.

Changed files:
filename: packages/scheduler/src/__tests__/Scheduler-test.js
patch: @@ -17,6 +17,7 @@ let runtime;
 let performance;
 let cancelCallback;
 let scheduleCallback;
+let requestPaint;
 let NormalPriority;
 
 // The Scheduler implementation uses browser APIs like `MessageChannel` and
@@ -40,6 +41,7 @@ describe('SchedulerBrowser', () => {
     cancelCallback = Scheduler.unstable_cancelCallback;
     scheduleCallback = Scheduler.unstable_scheduleCallback;
     NormalPriority = Scheduler.unstable_NormalPriority;
+    requestPaint = Scheduler.unstable_requestPaint;
   });
 
   afterEach(() => {
@@ -52,6 +54,9 @@ describe('SchedulerBrowser', () => {
 
   function installMockBrowserRuntime() {
     let hasPendingMessageEvent = false;
+    let isFiringMessageEvent = false;
+    let hasPendingDiscreteEvent = false;
+    let hasPendingContinuousEvent = false;
 
     let timerIDCounter = 0;
     // let timerIDs = new Map();
@@ -94,6 +99,23 @@ describe('SchedulerBrowser', () => {
       this.port2 = port2;
     };
 
+    const scheduling = {
+      isInputPending(options) {
+        if (this !== scheduling) {
+          throw new Error(
+            'isInputPending called with incorrect `this` context',
+          );
+        }
+
+        return (
+          hasPendingDiscreteEvent ||
+          (options && options.includeContinuous && hasPendingContinuousEvent)
+        );
+      },
+    };
+
+    global.navigator = {scheduling};
+
     function ensureLogIsEmpty() {
       if (eventLog.length !== 0) {
         throw Error('Log is not empty. Call assertLog before continuing.');
@@ -102,6 +124,9 @@ describe('SchedulerBrowser', () => {
     function advanceTime(ms) {
       currentTime += ms;
     }
+    function resetTime() {
+      currentTime = 0;
+    }
     function fireMessageEvent() {
       ensureLogIsEmpty();
       if (!hasPendingMessageEvent) {
@@ -110,7 +135,35 @@ describe('SchedulerBrowser', () => {
       hasPendingMessageEvent = false;
       const onMessage = port1.onmessage;
       log('Message Event');
-      onMessage();
+
+      isFiringMessageEvent = true;
+      try {
+        onMessage();
+      } finally {
+        isFiringMessageEvent = false;
+        if (hasPendingDiscreteEvent) {
+          log('Discrete Event');
+          hasPendingDiscreteEvent = false;
+        }
+        if (hasPendingContinuousEvent) {
+          log('Continuous Event');
+          hasPendingContinuousEvent = false;
+        }
+      }
+    }
+    function scheduleDiscreteEvent() {
+      if (isFiringMessageEvent) {
+        hasPendingDiscreteEvent = true;
+      } else {
+        log('Discrete Event');
+      }
+    }
+    function scheduleContinuousEvent() {
+      if (isFiringMessageEvent) {
+        hasPendingContinuousEvent = true;
+      } else {
+        log('Continuous Event');
+      }
     }
     function log(val) {
       eventLog.push(val);
@@ -125,10 +178,13 @@ describe('SchedulerBrowser', () => {
     }
     return {
       advanceTime,
+      resetTime,
       fireMessageEvent,
       log,
       isLogEmpty,
       assertLog,
+      scheduleDiscreteEvent,
+      scheduleContinuousEvent,
     };
   }
 
@@ -144,6 +200,8 @@ describe('SchedulerBrowser', () => {
   it('task with continuation', () => {
     scheduleCallback(NormalPriority, () => {
       runtime.log('Task');
+      // Request paint so that we yield at the end of the frame interval
+      requestPaint();
       while (!Scheduler.unstable_shouldYield()) {
         runtime.advanceTime(1);
       }
@@ -259,4 +317,162 @@ describe('SchedulerBrowser', () => {
     runtime.fireMessageEvent();
     runtime.assertLog(['Message Event', 'B']);
   });
+
+  it('when isInputPending is available, we can wait longer before yielding', () => {
+    function blockUntilSchedulerAsksToYield() {
+      while (!Scheduler.unstable_shouldYield()) {
+        runtime.advanceTime(1);
+      }
+      runtime.log(`Yield at ${performance.now()}ms`);
+    }
+
+    // First show what happens when we don't request a paint
+    scheduleCallback(NormalPriority, () => {
+      runtime.log('Task with no pending input');
+      blockUntilSchedulerAsksToYield();
+    });
+    runtime.assertLog(['Post Message']);
+
+    runtime.fireMessageEvent();
+    runtime.assertLog([
+      'Message Event',
+      'Task with no pending input',
+      // Even though there's no input, eventually Scheduler will yield
+      // regardless in case there's a pending main thread task we don't know
+      // about, like a network event.
+      gate(flags =>
+        flags.enableIsInputPending
+          ? 'Yield at 300ms'
+          : // When isInputPending is disabled, we always yield quickly
+            'Yield at 5ms',
+      ),
+    ]);
+
+    runtime.resetTime();
+
+    // Now do the same thing, but while the task is running, simulate an
+    // input event.
+    scheduleCallback(NormalPriority, () => {
+      runtime.log('Task with pending input');
+      runtime.scheduleDiscreteEvent();
+      blockUntilSchedulerAsksToYield();
+    });
+    runtime.assertLog(['Post Message']);
+
+    runtime.fireMessageEvent();
+    runtime.assertLog([
+      'Message Event',
+      'Task with pending input',
+      // This time we yielded quickly to unblock the discrete event.
+      'Yield at 5ms',
+      'Discrete Event',
+    ]);
+  });
+
+  it(
+    'isInputPending will also check for continuous inputs, but after a ' +
+      'slightly larger threshold',
+    () => {
+      function blockUntilSchedulerAsksToYield() {
+        while (!Scheduler.unstable_shouldYield()) {
+          runtime.advanceTime(1);
+        }
+        runtime.log(`Yield at ${performance.now()}ms`);
+      }
+
+      // First show what happens when we don't request a paint
+      scheduleCallback(NormalPriority, () => {
+        runtime.log('Task with no pending input');
+        blockUntilSchedulerAsksToYield();
+      });
+      runtime.assertLog(['Post Message']);
+
+      runtime.fireMessageEvent();
+      runtime.assertLog([
+        'Message Event',
+        'Task with no pending input',
+        // Even though there's no input, eventually Scheduler will yield
+        // regardless in case there's a pending main thread task we don't know
+        // about, like a network event.
+        gate(flags =>
+          flags.enableIsInputPending
+            ? 'Yield at 300ms'
+            : // When isInputPending is disabled, we always yield quickly
+              'Yield at 5ms',
+        ),
+      ]);
+
+      runtime.resetTime();
+
+      // Now do the same thing, but while the task is running, simulate a
+      // continuous input event.
+      scheduleCallback(NormalPriority, () => {
+        runtime.log('Task with continuous input');
+        runtime.scheduleContinuousEvent();
+        blockUntilSchedulerAsksToYield();
+      });
+      runtime.assertLog(['Post Message']);
+
+      runtime.fireMessageEvent();
+      runtime.assertLog([
+        'Message Event',
+        'Task with continuous input',
+        // This time we yielded quickly to unblock the continuous event. But not
+        // as quickly as for a discrete event.
+        gate(flags =>
+          flags.enableIsInputPending
+            ? 'Yield at 50ms'
+            : // When isInputPending is disabled, we always yield quickly
+              'Yield at 5ms',
+        ),
+        'Continuous Event',
+      ]);
+    },
+  );
+
+  it('requestPaint forces a yield at the end of the next frame interval', () => {
+    function blockUntilSchedulerAsksToYield() {
+      while (!Scheduler.unstable_shouldYield()) {
+        runtime.advanceTime(1);
+      }
+      runtime.log(`Yield at ${performance.now()}ms`);
+    }
+
+    // First show what happens when we don't request a paint
+    scheduleCallback(NormalPriority, () => {
+      runtime.log('Task with no paint');
+      blockUntilSchedulerAsksToYield();
+    });
+    runtime.assertLog(['Post Message']);
+
+    runtime.fireMessageEvent();
+    runtime.assertLog([
+      'Message Event',
+      'Task with no paint',
+      gate(flags =>
+        flags.enableIsInputPending
+          ? 'Yield at 300ms'
+          : // When isInputPending is disabled, we always yield quickly
+            'Yield at 5ms',
+      ),
+    ]);
+
+    runtime.resetTime();
+
+    // Now do the same thing, but call requestPaint inside the task
+    scheduleCallback(NormalPriority, () => {
+      runtime.log('Task with paint');
+      requestPaint();
+      blockUntilSchedulerAsksToYield();
+    });
+    runtime.assertLog(['Post Message']);
+
+    runtime.fireMessageEvent();
+    runtime.assertLog([
+      'Message Event',
+      'Task with paint',
+      // This time we yielded quickly (5ms) because we requested a paint.
+      'Yield at 5ms',
+    ]);
+  });
 });