Repository: sinonjs/lolex
Pull request #225 (2018-10-23 10:32:39)

[WIP] Issue #218 – `requestIdleCallback` implementation
#### First tests for requestIdleCallback and cancelIdleCallback added

For a start and be able to move on with your help I added this tests:

**requestIdleCallback**

- throws if no arguments
- returns numeric id
- returns unique id

**cancelIdleCallback**

- removes idle callback


#### requestIdleCallback behaviour

I tried figured out the behaviour and make busy system to see what is going on. This is what I got:

------------------ – new interval in setInterval (40ms)
idle without timeout - requestIdleCallback task without timeout option
idle with timeout - requestIdleCallback task with timeout option
timeout - three setTimeouts (10ms, 20ms, 30ms).
There are three idle tasks in each interval two without timeout and one with 15ms timeout.
Serial numbers used for the idle tasks.

Log:
```
requestIdleCallback.js:41 ------------------
requestIdleCallback.js:43 idle 1 without timeout
requestIdleCallback.js:49 idle 2 without timeout
requestIdleCallback.js:55 idle 3 with timeout
requestIdleCallback.js:46 timeout
requestIdleCallback.js:52 timeout 2
requestIdleCallback.js:58 timeout 3
requestIdleCallback.js:41 ------------------
requestIdleCallback.js:46 timeout
requestIdleCallback.js:55 idle 4 with timeout
requestIdleCallback.js:52 timeout 2
requestIdleCallback.js:58 timeout 3
requestIdleCallback.js:41 ------------------
requestIdleCallback.js:46 timeout
requestIdleCallback.js:55 idle 5 with timeout
requestIdleCallback.js:52 timeout 2
requestIdleCallback.js:58 timeout 3
...
requestIdleCallback.js:41 ------------------
requestIdleCallback.js:43 idle 597 without timeout
requestIdleCallback.js:49 idle 598 without timeout
requestIdleCallback.js:43 idle 599 without timeout
requestIdleCallback.js:49 idle 600 without timeout
...
requestIdleCallback.js:49 idle 620 without timeout
requestIdleCallback.js:43 idle 621 without timeout
requestIdleCallback.js:46 timeout
```

In first interval user agent had time to execute all three idle task (even earlier than timeout in third idle task) before timers, in next intervals idle tasks without timeout user agent delayed until indefinite time in the future. In log next idle task without timer was 597 after a lot of intervals.

With interval starting from 50ms it has time to put idle tasks before timers in each interval.

```
------------------
requestIdleCallback.js:43 idle 1 without timeout
requestIdleCallback.js:49 idle 2 without timeout
requestIdleCallback.js:55 idle 3 with timeout
requestIdleCallback.js:46 timeout
requestIdleCallback.js:52 timeout 2
requestIdleCallback.js:58 timeout 3
------------------
```
Seems like 20ms between timers is enough for user agent to estimate this as idle period and execute idle tasks. It is not precise, I think. What do you think? Or we can make it 50ms (max time for idle interval) to be 100% sure.

In this case behaviour for `requestIdleCallback` with timeout will be to execute in same queue with idle tasks without timeout option, but no later than timeout option time.

Any suggestions?

Also I need some explanations about testing workflow in a whole. Because there are a lot of things together as I understand (mocha, chai, sinon, referee).

Thank you for your time helping me.


Patch:
diff --git a/src/lolex-src.js b/src/lolex-src.js
index 5385abe..cba90a4 100644
--- a/src/lolex-src.js
+++ b/src/lolex-src.js
@@ -40,6 +40,12 @@ function withGlobal(_global) {
     var cancelAnimationFramePresent = (
         _global.cancelAnimationFrame && typeof _global.cancelAnimationFrame === "function"
     );
+    var requestIdleCallbackPresent = (
+        _global.requestIdleCallback && typeof _global.requestIdleCallback === "function"
+    );
+    var cancelIdleCallbackPresent = (
+        _global.cancelIdleCallback && typeof _global.cancelIdleCallback === "function"
+    );
 
     _global.clearTimeout(timeoutResult);
 
@@ -188,7 +194,6 @@ function withGlobal(_global) {
         return mirrorDateProperties(ClockDate, NativeDate);
     }
 
-
     function enqueueJob(clock, job) {
         // enqueues a microtick-deferred task - ecma262/#sec-enqueuejob
         if (!clock.jobs) {
@@ -496,6 +501,14 @@ function withGlobal(_global) {
         timers.cancelAnimationFrame = _global.cancelAnimationFrame;
     }
 
+    if (requestIdleCallbackPresent) {
+        timers.requestIdleCallback = _global.requestIdleCallback;
+    }
+
+    if (cancelIdleCallbackPresent) {
+        timers.cancelIdleCallback = _global.cancelIdleCallback;
+    }
+
     var keys = Object.keys || function (obj) {
         var ks = [];
         var key;
@@ -561,6 +574,26 @@ function withGlobal(_global) {
             return [secsSinceStart, remainderInNanos];
         }
 
+        clock.requestIdleCallback = function requestIdleCallback(func, timeout) {
+            var timeToNextIdlePeriod = 0;
+
+            if (clock.countTimers() > 0) {
+                timeToNextIdlePeriod = 50; // const for now
+            }
+
+            var result = addTimer(clock, {
+                func: func,
+                args: Array.prototype.slice.call(arguments, 2),
+                delay: typeof timeout === "undefined" ? timeToNextIdlePeriod : Math.min(timeout, timeToNextIdlePeriod)
+            });
+
+            return result.id || result;
+        };
+
+        clock.cancelIdleCallback = function cancelIdleCallback(timerId) {
+            return clearTimer(clock, timerId, "Timeout");
+        };
+
         clock.setTimeout = function setTimeout(func, timeout) {
             return addTimer(clock, {
                 func: func,
@@ -572,15 +605,18 @@ function withGlobal(_global) {
         clock.clearTimeout = function clearTimeout(timerId) {
             return clearTimer(clock, timerId, "Timeout");
         };
+
         clock.nextTick = function nextTick(func) {
             return enqueueJob(clock, {
                 func: func,
                 args: Array.prototype.slice.call(arguments, 1)
             });
         };
+
         clock.queueMicrotask = function queueMicrotask(func) {
             return clock.nextTick(func); // explicitly drop additional arguments
         };
+
         clock.setInterval = function setInterval(func, timeout) {
             timeout = parseInt(timeout, 10);
             return addTimer(clock, {


Test patch:
diff --git a/test/lolex-test.js b/test/lolex-test.js
index a877de9..b99ae93 100644
--- a/test/lolex-test.js
+++ b/test/lolex-test.js
@@ -2769,4 +2769,80 @@ describe("lolex", function () {
             }, true);
         });
     });
+
+    describe("requestIdleCallback", function () {
+        beforeEach(function () {
+            this.clock = lolex.createClock();
+        });
+
+        it("throws if no arguments", function () {
+            var clock = this.clock;
+
+            assert.exception(function () { clock.requestIdleCallback(); });
+        });
+
+        it("returns numeric id", function () {
+            var result = this.clock.requestIdleCallback(NOOP);
+
+            assert.isNumber(result);
+        });
+
+        it("returns unique id", function () {
+            var id1 = this.clock.requestIdleCallback(NOOP);
+            var id2 = this.clock.requestIdleCallback(NOOP);
+            this.clock.runAll();
+
+            refute.equals(id2, id1);
+        });
+
+        it("runs after all timers", function () {
+            var spy = sinon.spy();
+            this.clock.requestIdleCallback(spy);
+            this.clock.runAll();
+
+            assert(spy.called);
+        });
+
+        it("runs immediately with timeout option if there isn't any timer", function () {
+            var spy = sinon.spy();
+            this.clock.requestIdleCallback(spy, 20);
+            this.clock.tick(1);
+
+            assert(spy.called);
+        });
+
+        it("runs no later than timeout option even if there are any timers", function () {
+            var spy = sinon.spy();
+            this.clock.setTimeout(NOOP, 10);
+            this.clock.setTimeout(NOOP, 30);
+            this.clock.requestIdleCallback(spy, 20);
+            this.clock.tick(20);
+
+            assert(spy.called);
+        });
+
+        it("doesn't runs if there are any timers and no timeout option", function () {
+            var spy = sinon.spy();
+            this.clock.setTimeout(NOOP, 30);
+            this.clock.requestIdleCallback(spy);
+            this.clock.tick(35);
+
+            assert.isFalse(spy.called);
+        });
+    });
+
+    describe("cancelIdleCallback", function () {
+        beforeEach(function () {
+            this.clock = lolex.createClock();
+        });
+
+        it("removes idle callback", function () {
+            var stub = sinon.stub();
+            var callbackId = this.clock.requestIdleCallback(stub, 0);
+            this.clock.cancelIdleCallback(callbackId);
+            this.clock.runAll();
+
+            assert.isFalse(stub.called);
+        });
+    });
 });