Repository: nodejs/node
Pull request: test_runner: add mock timers for nodeTimers.promises.scheduler.wait

add mock timers for nodeTimers.promises.scheduler.wait

<!--
Before submitting a pull request, please read:

- the CONTRIBUTING guide at https://github.com/nodejs/node/blob/HEAD/CONTRIBUTING.md
- the commit message formatting guidelines at
  https://github.com/nodejs/node/blob/HEAD/doc/contributing/pull-requests.md#commit-message-guidelines

For code changes:
1. Include tests for any bug fixes or new features.
2. Update documentation if relevant.
3. Ensure that `make -j4 test` (UNIX), or `vcbuild test` (Windows) passes.

If you believe this PR should be highlighted in the Node.js CHANGELOG
please add the `notable-change` label.

Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I
    have the right to submit it under the open source license
    indicated in the file; or

(b) The contribution is based upon previous work that, to the best
    of my knowledge, is covered under an appropriate open source
    license and I have the right under that license to submit that
    work with modifications, whether created in whole or in part
    by me, under the same open source license (unless I am
    permitted to submit under a different license), as indicated
    in the file; or

(c) The contribution was provided directly to me by some other
    person who certified (a), (b) or (c) and I have not modified
    it.

(d) I understand and agree that this project and the contribution
    are public and that a record of the contribution (including all
    personal information I submit with it, including my sign-off) is
    maintained indefinitely and may be redistributed consistent with
    this project or the open source license(s) involved.
-->


Changed files:
filename: lib/internal/test_runner/mock/mock_timers.js
patch: @@ -64,9 +64,9 @@ function abortIt(signal) {
 }
 
 /**
- * @enum {('setTimeout'|'setInterval'|'setImmediate'|'Date')[]} Supported timers
+ * @enum {('setTimeout'|'setInterval'|'setImmediate'|'Date', 'scheduler.wait')[]} Supported timers
  */
-const SUPPORTED_APIS = ['setTimeout', 'setInterval', 'setImmediate', 'Date'];
+const SUPPORTED_APIS = ['setTimeout', 'setInterval', 'setImmediate', 'Date', 'scheduler.wait'];
 const TIMERS_DEFAULT_INTERVAL = {
   __proto__: null,
   setImmediate: -1,
@@ -108,6 +108,7 @@ class MockTimers {
 
   #realPromisifiedSetTimeout;
   #realPromisifiedSetInterval;
+  #realTimersPromisifiedSchedulerWait;
 
   #realTimersSetTimeout;
   #realTimersClearTimeout;
@@ -192,6 +193,13 @@ class MockTimers {
     );
   }
 
+  #restoreOriginalSchedulerWait() {
+    nodeTimersPromises.scheduler.wait = FunctionPrototypeBind(
+      this.#realTimersPromisifiedSchedulerWait,
+      this,
+    );
+  }
+
   #restoreOriginalSetTimeout() {
     ObjectDefineProperty(
       globalThis,
@@ -266,6 +274,14 @@ class MockTimers {
     );
   }
 
+  #storeOriginalSchedulerWait() {
+
+    this.#realTimersPromisifiedSchedulerWait = FunctionPrototypeBind(
+      nodeTimersPromises.scheduler.wait,
+      this,
+    );
+  }
+
   #storeOriginalSetTimeout() {
     this.#realSetTimeout = ObjectGetOwnPropertyDescriptor(
       globalThis,
@@ -562,8 +578,14 @@ class MockTimers {
     const options = {
       __proto__: null,
       toFake: {
-        __proto__: null,
-        setTimeout: () => {
+        '__proto__': null,
+        'scheduler.wait': () => {
+          this.#storeOriginalSchedulerWait();
+
+          nodeTimersPromises.scheduler.wait = (delay, options) =>
+            this.#setTimeoutPromisified(delay, undefined, options);
+        },
+        'setTimeout': () => {
           this.#storeOriginalSetTimeout();
 
           globalThis.setTimeout = this.#setTimeout;
@@ -577,7 +599,7 @@ class MockTimers {
             this,
           );
         },
-        setInterval: () => {
+        'setInterval': () => {
           this.#storeOriginalSetInterval();
 
           globalThis.setInterval = this.#setInterval;
@@ -591,7 +613,7 @@ class MockTimers {
             this,
           );
         },
-        setImmediate: () => {
+        'setImmediate': () => {
           this.#storeOriginalSetImmediate();
 
           // setImmediate functions needs to bind MockTimers
@@ -615,23 +637,26 @@ class MockTimers {
             this,
           );
         },
-        Date: () => {
+        'Date': () => {
           this.#nativeDateDescriptor = ObjectGetOwnPropertyDescriptor(globalThis, 'Date');
           globalThis.Date = this.#createDate();
         },
       },
       toReal: {
-        __proto__: null,
-        setTimeout: () => {
+        '__proto__': null,
+        'scheduler.wait': () => {
+          this.#restoreOriginalSchedulerWait();
+        },
+        'setTimeout': () => {
           this.#restoreOriginalSetTimeout();
         },
-        setInterval: () => {
+        'setInterval': () => {
           this.#restoreOriginalSetInterval();
         },
-        setImmediate: () => {
+        'setImmediate': () => {
           this.#restoreSetImmediate();
         },
-        Date: () => {
+        'Date': () => {
           ObjectDefineProperty(globalThis, 'Date', this.#nativeDateDescriptor);
         },
       },

filename: test/parallel/test-runner-mock-timers.js
patch: @@ -791,6 +791,126 @@ describe('Mock Timers Test Suite', () => {
     });
   });
 
+  describe('scheduler Suite', () => {
+    describe('scheduler.wait', () => {
+      it('should advance in time and trigger timers when calling the .tick function', (t) => {
+        t.mock.timers.enable({ apis: ['scheduler.wait'] });
+
+        const now = Date.now();
+        const durationAtMost = 100;
+
+        const p = nodeTimersPromises.scheduler.wait(4000);
+        t.mock.timers.tick(4000);
+
+        return p.then(common.mustCall((result) => {
+          assert.strictEqual(result, undefined);
+          assert.ok(
+            Date.now() - now < durationAtMost,
+            `time should be advanced less than the ${durationAtMost}ms`
+          );
+        }));
+      });
+
+      it('should advance in time and trigger timers when calling the .tick function multiple times', async (t) => {
+        t.mock.timers.enable({ apis: ['scheduler.wait'] });
+
+        const fn = t.mock.fn();
+
+        nodeTimersPromises.scheduler.wait(9999).then(fn);
+
+        t.mock.timers.tick(8999);
+        assert.strictEqual(fn.mock.callCount(), 0);
+        t.mock.timers.tick(500);
+
+        await nodeTimersPromises.setImmediate();
+
+        assert.strictEqual(fn.mock.callCount(), 0);
+        t.mock.timers.tick(500);
+
+        await nodeTimersPromises.setImmediate();
+        assert.strictEqual(fn.mock.callCount(), 1);
+      });
+
+      it('should work with the same params as the original timers/promises/scheduler.wait', async (t) => {
+        t.mock.timers.enable({ apis: ['scheduler.wait'] });
+        const controller = new AbortController();
+        const p = nodeTimersPromises.scheduler.wait(2000, {
+          ref: true,
+          signal: controller.signal,
+        });
+
+        t.mock.timers.tick(1000);
+        t.mock.timers.tick(500);
+        t.mock.timers.tick(500);
+        t.mock.timers.tick(500);
+
+        const result = await p;
+        assert.strictEqual(result, undefined);
+      });
+
+      it('should abort operation if timers/promises/scheduler.wait received an aborted signal', async (t) => {
+        t.mock.timers.enable({ apis: ['scheduler.wait'] });
+        const controller = new AbortController();
+        const p = nodeTimersPromises.scheduler.wait(2000, {
+          ref: true,
+          signal: controller.signal,
+        });
+
+        t.mock.timers.tick(1000);
+        controller.abort();
+        t.mock.timers.tick(500);
+        t.mock.timers.tick(500);
+        t.mock.timers.tick(500);
+
+        await assert.rejects(() => p, {
+          name: 'AbortError',
+        });
+      });
+      it('should abort operation even if the .tick was not called', async (t) => {
+        t.mock.timers.enable({ apis: ['scheduler.wait'] });
+        const controller = new AbortController();
+        const p = nodeTimersPromises.scheduler.wait(2000, {
+          ref: true,
+          signal: controller.signal,
+        });
+
+        controller.abort();
+
+        await assert.rejects(() => p, {
+          name: 'AbortError',
+        });
+      });
+
+      it('should abort operation when .abort is called before calling setInterval', async (t) => {
+        t.mock.timers.enable({ apis: ['scheduler.wait'] });
+        const controller = new AbortController();
+        controller.abort();
+        const p = nodeTimersPromises.scheduler.wait(2000, {
+          ref: true,
+          signal: controller.signal,
+        });
+
+        await assert.rejects(() => p, {
+          name: 'AbortError',
+        });
+      });
+
+      it('should reject given an an invalid signal instance', async (t) => {
+        t.mock.timers.enable({ apis: ['scheduler.wait'] });
+        const p = nodeTimersPromises.scheduler.wait(2000, {
+          ref: true,
+          signal: {},
+        });
+
+        await assert.rejects(() => p, {
+          name: 'TypeError',
+          code: 'ERR_INVALID_ARG_TYPE',
+        });
+      });
+
+    });
+  });
+
   describe('Date Suite', () => {
     it('should return the initial UNIX epoch if not specified', (t) => {
       t.mock.timers.enable({ apis: ['Date'] });