Repository: sinonjs/fake-timers (js)

Issue:
Promisified (with util.promisify) `setTimeout` does not work
Node.js's setTimeout promisified [does](https://github.com/nodejs/node/blob/master/lib/timers.js#L459-L463):

```js
setTimeout[internalUtil.promisify.custom] = function(after, value) {
  return new Promise((resolve) => {
    active(new Timeout(resolve, after, [value], false));
  });
};
```

This means that it does not refer to setTimeout internally - we should override `setTimeout[internalUtil.promisify.custom]` if it is available to use our fake timers and revert it on uninstall.

If anyone wants to pick this up go ahead - otherwise I will work on a fix in ~2 weeks :)

Pull request:
support util.promisify in Node
#### Purpose (TL;DR) - mandatory
<!--
> give a concise (one or two short sentences) description of what problem is being solved by this PR
>
> Example: Fix issue #123456 by re-structuring the colour selection conditional in method `paintBlue`
-->
Fixes #223, similar to what we did for [`jest-fake-timers`](https://github.com/facebook/jest/pull/9180/files).
Noticed after starting there's already https://github.com/sinonjs/lolex/pull/227, but that seems stale and I'm not sure what some of the changes are for.
I'll do what I can to incorporate any review comments and get this through! :sweat_smile: 
If there's consensus that [storing a `global.Promise` backup](https://github.com/sinonjs/lolex/pull/227/files#r233684110) is a good idea, can do that, but would do another PR first, changing this in existing code.

<!--
#### Background (Problem in detail)  - optional
-->
<!--
> When relevant, give a more thorough description of what the problem the PR is trying to solve. Examples of good topics for this section are:
> * Link to an existing GitHub issue describing the problem
> * Describing the problem in greater detail than the TL;DR section above
> * How you discovered the issue if it's not already described as an issue on GitHub
> * Discussion of different approaches to solving this problem and why you chose your proposed solution
-->


<!--
#### Solution  - optional
-->
<!--
> When contributing code (and not just fixing typos, documentation and configuration), please describe why/how your solution works. This helps reviewers spot any mistakes in the implementation.
>
> Example:
> "This solution works by adding a `paintBlue()` method"
> Then your reviewer might spot a mistake in the implementation, if `paintBlue()` uses the colour red.
-->


Patch:
diff --git a/src/fake-timers-src.js b/src/fake-timers-src.js
index efd2981..f723db5 100644
--- a/src/fake-timers-src.js
+++ b/src/fake-timers-src.js
@@ -21,6 +21,7 @@ function withGlobal(_global) {
         hrtimePresent && typeof _global.process.hrtime.bigint === "function";
     var nextTickPresent =
         _global.process && typeof _global.process.nextTick === "function";
+    var utilPromisify = _global.process && require("util").promisify;
     var performancePresent =
         _global.performance && typeof _global.performance.now === "function";
     var hasPerformancePrototype =
@@ -743,6 +744,21 @@ function withGlobal(_global) {
                 delay: timeout
             });
         };
+        if (typeof _global.Promise !== "undefined" && utilPromisify) {
+            clock.setTimeout[
+                utilPromisify.custom
+            ] = function promisifiedSetTimeout(timeout, arg) {
+                return new _global.Promise(function setTimeoutExecutor(
+                    resolve
+                ) {
+                    addTimer(clock, {
+                        func: resolve,
+                        args: [arg],
+                        delay: timeout
+                    });
+                });
+            };
+        }
 
         clock.clearTimeout = function clearTimeout(timerId) {
             return clearTimer(clock, timerId, "Timeout");
@@ -783,6 +799,22 @@ function withGlobal(_global) {
                 });
             };
 
+            if (typeof _global.Promise !== "undefined" && utilPromisify) {
+                clock.setImmediate[
+                    utilPromisify.custom
+                ] = function promisifiedSetImmediate(arg) {
+                    return new _global.Promise(function setImmediateExecutor(
+                        resolve
+                    ) {
+                        addTimer(clock, {
+                            func: resolve,
+                            args: [arg],
+                            immediate: true
+                        });
+                    });
+                };
+            }
+
             clock.clearImmediate = function clearImmediate(timerId) {
                 return clearTimer(clock, timerId, "Immediate");
             };


Test patch:
diff --git a/test/fake-timers-test.js b/test/fake-timers-test.js
index 3459883..3bb4763 100644
--- a/test/fake-timers-test.js
+++ b/test/fake-timers-test.js
@@ -50,6 +50,7 @@ var performanceMarkPresent =
     global.performance && typeof global.performance.mark === "function";
 var setImmediatePresent =
     global.setImmediate && typeof global.setImmediate === "function";
+var utilPromisify = global.process && require("util").promisify;
 
 describe("issue #59", function() {
     var context = {
@@ -445,6 +446,36 @@ describe("FakeTimers", function() {
             this.clock.runAll();
             assert.equals(calls, ["NaN", "Infinity", "-Infinity"]);
         });
+
+        if (typeof global.Promise !== "undefined" && utilPromisify) {
+            describe("when util.promisified", function() {
+                it("sets timers on instance", function() {
+                    var resolved = false;
+                    utilPromisify(this.clock.setTimeout)(100).then(function() {
+                        resolved = true;
+                    });
+
+                    return this.clock.tickAsync(100).then(function() {
+                        assert.isTrue(resolved);
+                    });
+                });
+
+                it("resolves with the first additional argument to setTimeout", function() {
+                    var resolvedValue;
+                    utilPromisify(this.clock.setTimeout)(
+                        100,
+                        "the first",
+                        "the second"
+                    ).then(function(value) {
+                        resolvedValue = value;
+                    });
+
+                    return this.clock.tickAsync(100).then(function() {
+                        assert.equals(resolvedValue, "the first");
+                    });
+                });
+            });
+        }
     });
 
     describe("setImmediate", function() {
@@ -527,6 +558,35 @@ describe("FakeTimers", function() {
 
             clock.tick(0);
         });
+
+        if (typeof global.Promise !== "undefined" && utilPromisify) {
+            describe("when util.promisified", function() {
+                it("calls the given callback immediately", function() {
+                    var resolved = false;
+                    utilPromisify(this.clock.setImmediate)().then(function() {
+                        resolved = true;
+                    });
+
+                    return this.clock.tickAsync(0).then(function() {
+                        assert.isTrue(resolved);
+                    });
+                });
+
+                it("resolves with the first argument to setImmediate", function() {
+                    var resolvedValue;
+                    utilPromisify(this.clock.setImmediate)(
+                        "the first",
+                        "the second"
+                    ).then(function(value) {
+                        resolvedValue = value;
+                    });
+
+                    return this.clock.tickAsync(0).then(function() {
+                        assert.equals(resolvedValue, "the first");
+                    });
+                });
+            });
+        }
     });
 
     describe("clearImmediate", function() {