Repository: sinonjs/fake-timers (js)

Issue:
Support util.promisify on installed functions
* FakeTimers version : 6.0.1
* Environment   : Node v15.5.1

**What did you expect to happen?**

[`util.promisify`](https://nodejs.org/api/util.html#util_util_promisify_original) would work on `setTimeout` installed by FakeTimers as it does on the global `setTimeout`.

**What actually happens**

The Promise returned by the installed `setTimeout` is never resolved because the installed `setTimeout` does not have a [`promisify.custom`](https://nodejs.org/api/util.html#util_custom_promisified_functions) symbol property.

**How to reproduce**

```js
const { promisify } = require('util');
const FakeTimers = require("@sinonjs/fake-timers");

console.log('initial global.setTimeout', setTimeout[promisify.custom]);

const target = { Date };
FakeTimers.install({ target });
console.log('installed target.setTimeout', target.setTimeout[promisify.custom]);

FakeTimers.install();
console.log('installed global.setTimeout', setTimeout[promisify.custom]);
```
prints

    initial global.setTimeout [Function]
    installed target.setTimeout undefined
    installed global.setTimeout undefined

**Discussion**

This issue was noted by @joshua-barnett in https://github.com/sinonjs/fake-timers/pull/292#issuecomment-744443087.  I'm opening this issue to track it.

Pull request description:
Copy all property descriptors to installed methods
#### Purpose (TL;DR) - mandatory

Fix #347 by copying symbol own-properties to installed methods.

#### Background (Problem in detail)  - optional

The `for...in` loop used to copy properties only copies string own-properties to installed methods, which omits [`util.promisify.custom`](https://nodejs.org/api/util.html#util_custom_promisified_functions) necessary for [`util.promisify`](https://nodejs.org/api/util.html#util_util_promisify_original) support on Node as added in #292.

#### Solution  - optional

This PR copies all own-property descriptors to get both `Symbol` own-properties and to copy any getters/setters that may be added in the future.

If there's a reason getters/setters should not be copied, let me know and I can add an `Object.assign` polyfill to [sinonjs/commons](https://github.com/sinonjs/commons) and use that instead.

Thanks for considering,
Kevin

Fixes: #347

Patch:
diff --git a/src/fake-timers-src.js b/src/fake-timers-src.js
index e55750b..5eaa30b 100644
--- a/src/fake-timers-src.js
+++ b/src/fake-timers-src.js
@@ -535,7 +535,6 @@ function withGlobal(_global) {
     }
 
     function hijackMethod(target, method, clock) {
-        var prop;
         clock[method].hadOwnProperty = Object.prototype.hasOwnProperty.call(
             target,
             method
@@ -575,11 +574,10 @@ function withGlobal(_global) {
                 return clock[method].apply(clock, arguments);
             };
 
-            for (prop in clock[method]) {
-                if (clock[method].hasOwnProperty(prop)) {
-                    target[method][prop] = clock[method][prop];
-                }
-            }
+            Object.defineProperties(
+                target[method],
+                Object.getOwnPropertyDescriptors(clock[method])
+            );
         }
 
         target[method].clock = clock;


Test patch:
diff --git a/test/fake-timers-test.js b/test/fake-timers-test.js
index d8dc43c..71ece42 100644
--- a/test/fake-timers-test.js
+++ b/test/fake-timers-test.js
@@ -4633,3 +4633,45 @@ describe("#187 - Support timeout.refresh in node environments", function() {
         clock.uninstall();
     });
 });
+
+describe("#347 - Support util.promisify once installed", function() {
+    before(function() {
+        if (typeof global.Promise === "undefined" || !utilPromisify) {
+            this.skip();
+        }
+    });
+
+    beforeEach(function() {
+        this.clock = FakeTimers.install();
+    });
+
+    afterEach(function() {
+        this.clock.uninstall();
+    });
+
+    it("setTimeout", function() {
+        var resolved = false;
+        utilPromisify(global.setTimeout)(100).then(function() {
+            resolved = true;
+        });
+
+        return this.clock.tickAsync(100).then(function() {
+            assert.isTrue(resolved);
+        });
+    });
+
+    it("setImmediate", function() {
+        if (!setImmediatePresent) {
+            this.skip();
+        }
+
+        var resolved = false;
+        utilPromisify(global.setImmediate)().then(function() {
+            resolved = true;
+        });
+
+        return this.clock.tickAsync(0).then(function() {
+            assert.isTrue(resolved);
+        });
+    });
+});