From 59c71ee9b5bb284abaec5bb9da3896c43bba2796 Mon Sep 17 00:00:00 2001 From: cjihrig Date: Mon, 13 Jan 2025 21:02:41 -0500 Subject: [PATCH 1/3] test_runner: add TestContext.prototype.waitFor() This commit adds a waitFor() method to the TestContext class in the test runner. As the name implies, this method allows tests to more easily wait for things to happen. --- doc/api/test.md | 20 +++++ lib/internal/test_runner/test.js | 62 +++++++++++++++- test/parallel/test-runner-wait-for.js | 101 ++++++++++++++++++++++++++ 3 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 test/parallel/test-runner-wait-for.js diff --git a/doc/api/test.md b/doc/api/test.md index 6884e128033747..04806d64847d5e 100644 --- a/doc/api/test.md +++ b/doc/api/test.md @@ -3608,6 +3608,26 @@ test('top level test', async (t) => { }); ``` +### `context.waitFor(condition[, options])` + + + +* `condition` {Function|AsyncFunction} A function that is invoked periodically + until it completes successfully or the defined polling timeout elapses. This + function does not accept any arguments, and is allowed to return any value. +* `options` {Object} An optional configuration object for the polling operation. + The following properties are supported: + * `interval` {number} The polling period in milliseconds. The `condition` + function is invoked according to this interval. **Default:** `50`. + * `timeout` {number} The poll timeout in milliseconds. If `condition` has not + succeeded by the time this elapses, an error occurs. **Default:** `1000`. +* Returns: {Promise} Fulfilled with the value returned by `condition`. + +This method polls a `condition` function until that function either returns +successfully or the operation times out. + ## Class: `SuiteContext` -* `condition` {Function|AsyncFunction} A function that is invoked periodically - until it completes successfully or the defined polling timeout elapses. This +* `condition` {Function|AsyncFunction} An assertion function that is invoked + periodically until it completes successfully or the defined polling timeout + elapses. Successful completion is defined as not throwing or rejecting. This function does not accept any arguments, and is allowed to return any value. * `options` {Object} An optional configuration object for the polling operation. The following properties are supported: From 531921e91be0c68bef7833100a2b03cf6d872f0d Mon Sep 17 00:00:00 2001 From: cjihrig Date: Wed, 15 Jan 2025 10:30:03 -0500 Subject: [PATCH 3/3] nits --- doc/api/test.md | 4 +- lib/internal/test_runner/test.js | 12 ++-- test/parallel/test-runner-wait-for.js | 79 +++++++++++++++++---------- 3 files changed, 59 insertions(+), 36 deletions(-) diff --git a/doc/api/test.md b/doc/api/test.md index 7ce9fe1f9ec2bb..58a945df02b88f 100644 --- a/doc/api/test.md +++ b/doc/api/test.md @@ -3620,8 +3620,8 @@ added: REPLACEME function does not accept any arguments, and is allowed to return any value. * `options` {Object} An optional configuration object for the polling operation. The following properties are supported: - * `interval` {number} The polling period in milliseconds. The `condition` - function is invoked according to this interval. **Default:** `50`. + * `interval` {number} The number of milliseconds to wait after an unsuccessful + invocation of `condition` before trying again. **Default:** `50`. * `timeout` {number} The poll timeout in milliseconds. If `condition` has not succeeded by the time this elapses, an error occurs. **Default:** `1000`. * Returns: {Promise} Fulfilled with the value returned by `condition`. diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js index e56010a982934b..b13d13e105e9ba 100644 --- a/lib/internal/test_runner/test.js +++ b/lib/internal/test_runner/test.js @@ -66,9 +66,7 @@ const { validateUint32, } = require('internal/validators'); const { - clearInterval, clearTimeout, - setInterval, setTimeout, } = require('timers'); const { TIMEOUT_MAX } = require('internal/timers'); @@ -364,10 +362,10 @@ class TestContext { const { promise, resolve, reject } = PromiseWithResolvers(); const noError = Symbol(); let cause = noError; - let intervalId; + let pollerId; let timeoutId; const done = (err, result) => { - clearInterval(intervalId); + clearTimeout(pollerId); clearTimeout(timeoutId); if (err === noError) { @@ -388,16 +386,18 @@ class TestContext { done(err); }, timeout); - intervalId = setInterval(async () => { + const poller = async () => { try { const result = await condition(); done(noError, result); } catch (err) { cause = err; + pollerId = setTimeout(poller, interval); } - }, interval); + }; + poller(); return promise; } } diff --git a/test/parallel/test-runner-wait-for.js b/test/parallel/test-runner-wait-for.js index 79698b4c15de9a..8f1e28f12868e4 100644 --- a/test/parallel/test-runner-wait-for.js +++ b/test/parallel/test-runner-wait-for.js @@ -1,40 +1,42 @@ 'use strict'; require('../common'); -const { test } = require('node:test'); +const { suite, test } = require('node:test'); -test('throws if condition is not a function', (t) => { - t.assert.throws(() => { - t.waitFor(5); - }, { - code: 'ERR_INVALID_ARG_TYPE', - message: /The "condition" argument must be of type function/, +suite('input validation', () => { + test('throws if condition is not a function', (t) => { + t.assert.throws(() => { + t.waitFor(5); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "condition" argument must be of type function/, + }); }); -}); -test('throws if options is not an object', (t) => { - t.assert.throws(() => { - t.waitFor(() => {}, null); - }, { - code: 'ERR_INVALID_ARG_TYPE', - message: /The "options" argument must be of type object/, + test('throws if options is not an object', (t) => { + t.assert.throws(() => { + t.waitFor(() => {}, null); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "options" argument must be of type object/, + }); }); -}); -test('throws if options.interval is not a number', (t) => { - t.assert.throws(() => { - t.waitFor(() => {}, { interval: 'foo' }); - }, { - code: 'ERR_INVALID_ARG_TYPE', - message: /The "options\.interval" property must be of type number/, + test('throws if options.interval is not a number', (t) => { + t.assert.throws(() => { + t.waitFor(() => {}, { interval: 'foo' }); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "options\.interval" property must be of type number/, + }); }); -}); -test('throws if options.timeout is not a number', (t) => { - t.assert.throws(() => { - t.waitFor(() => {}, { timeout: 'foo' }); - }, { - code: 'ERR_INVALID_ARG_TYPE', - message: /The "options\.timeout" property must be of type number/, + test('throws if options.timeout is not a number', (t) => { + t.assert.throws(() => { + t.waitFor(() => {}, { timeout: 'foo' }); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "options\.timeout" property must be of type number/, + }); }); }); @@ -99,3 +101,24 @@ test('sets last failure as error cause on timeouts', async (t) => { return true; }); }); + +test('limits polling if condition takes longer than interval', async (t) => { + let count = 0; + + function condition() { + count++; + return new Promise((resolve) => { + setTimeout(() => { + resolve('success'); + }, 200); + }); + } + + const result = await t.waitFor(condition, { + interval: 1, + timeout: 60_000, + }); + + t.assert.strictEqual(result, 'success'); + t.assert.strictEqual(count, 1); +});