mirror of
https://github.com/nodejs/node.git
synced 2026-10-14 10:26:26 -04:00
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. PR-URL: https://github.com/nodejs/node/pull/56595 Reviewed-By: Pietro Marchini <pietro.marchini94@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Chemi Atlow <chemi@atlow.co.il> Reviewed-By: Michaël Zasso <targos@protonmail.com>
This commit is contained in:
1 parent
e96072ad57
commit
0e7ec5e7a1
3 files changed
+206
-1
No files matched your search
@@ -3608,6 +3608,27 @@ test('top level test', async (t) => {
|
||||
});
|
||||
```
|
||||
|
||||
### `context.waitFor(condition[, options])`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
* `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:
|
||||
* `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`.
|
||||
|
||||
This method polls a `condition` function until that function either returns
|
||||
successfully or the operation times out.
|
||||
|
||||
## Class: `SuiteContext`
|
||||
|
||||
<!-- YAML
|
||||
|
||||
@@ -8,6 +8,7 @@ const {
|
||||
ArrayPrototypeSplice,
|
||||
ArrayPrototypeUnshift,
|
||||
ArrayPrototypeUnshiftApply,
|
||||
Error,
|
||||
FunctionPrototype,
|
||||
MathMax,
|
||||
Number,
|
||||
@@ -58,11 +59,16 @@ const {
|
||||
const { isPromise } = require('internal/util/types');
|
||||
const {
|
||||
validateAbortSignal,
|
||||
validateFunction,
|
||||
validateNumber,
|
||||
validateObject,
|
||||
validateOneOf,
|
||||
validateUint32,
|
||||
} = require('internal/validators');
|
||||
const { setTimeout } = require('timers');
|
||||
const {
|
||||
clearTimeout,
|
||||
setTimeout,
|
||||
} = require('timers');
|
||||
const { TIMEOUT_MAX } = require('internal/timers');
|
||||
const { fileURLToPath } = require('internal/url');
|
||||
const { availableParallelism } = require('os');
|
||||
@@ -340,6 +346,60 @@ class TestContext {
|
||||
loc: getCallerLocation(),
|
||||
});
|
||||
}
|
||||
|
||||
waitFor(condition, options = kEmptyObject) {
|
||||
validateFunction(condition, 'condition');
|
||||
validateObject(options, 'options');
|
||||
|
||||
const {
|
||||
interval = 50,
|
||||
timeout = 1000,
|
||||
} = options;
|
||||
|
||||
validateNumber(interval, 'options.interval', 0, TIMEOUT_MAX);
|
||||
validateNumber(timeout, 'options.timeout', 0, TIMEOUT_MAX);
|
||||
|
||||
const { promise, resolve, reject } = PromiseWithResolvers();
|
||||
const noError = Symbol();
|
||||
let cause = noError;
|
||||
let pollerId;
|
||||
let timeoutId;
|
||||
const done = (err, result) => {
|
||||
clearTimeout(pollerId);
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (err === noError) {
|
||||
resolve(result);
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
};
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const err = new Error('waitFor() timed out');
|
||||
|
||||
if (cause !== noError) {
|
||||
err.cause = cause;
|
||||
}
|
||||
|
||||
done(err);
|
||||
}, timeout);
|
||||
|
||||
const poller = async () => {
|
||||
try {
|
||||
const result = await condition();
|
||||
|
||||
done(noError, result);
|
||||
} catch (err) {
|
||||
cause = err;
|
||||
pollerId = setTimeout(poller, interval);
|
||||
}
|
||||
};
|
||||
|
||||
poller();
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
|
||||
class SuiteContext {
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
'use strict';
|
||||
require('../common');
|
||||
const { suite, test } = require('node:test');
|
||||
|
||||
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.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('returns the result of the condition function', async (t) => {
|
||||
const result = await t.waitFor(() => {
|
||||
return 42;
|
||||
});
|
||||
|
||||
t.assert.strictEqual(result, 42);
|
||||
});
|
||||
|
||||
test('returns the result of an async condition function', async (t) => {
|
||||
const result = await t.waitFor(async () => {
|
||||
return 84;
|
||||
});
|
||||
|
||||
t.assert.strictEqual(result, 84);
|
||||
});
|
||||
|
||||
test('errors if the condition times out', async (t) => {
|
||||
await t.assert.rejects(async () => {
|
||||
await t.waitFor(() => {
|
||||
return new Promise(() => {});
|
||||
}, {
|
||||
interval: 60_000,
|
||||
timeout: 1,
|
||||
});
|
||||
}, {
|
||||
message: /waitFor\(\) timed out/,
|
||||
});
|
||||
});
|
||||
|
||||
test('polls until the condition returns successfully', async (t) => {
|
||||
let count = 0;
|
||||
const result = await t.waitFor(() => {
|
||||
++count;
|
||||
if (count < 4) {
|
||||
throw new Error('resource is not ready yet');
|
||||
}
|
||||
|
||||
return 'success';
|
||||
}, {
|
||||
interval: 1,
|
||||
timeout: 60_000,
|
||||
});
|
||||
|
||||
t.assert.strictEqual(result, 'success');
|
||||
t.assert.strictEqual(count, 4);
|
||||
});
|
||||
|
||||
test('sets last failure as error cause on timeouts', async (t) => {
|
||||
const error = new Error('boom');
|
||||
await t.assert.rejects(async () => {
|
||||
await t.waitFor(() => {
|
||||
return new Promise((_, reject) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}, (err) => {
|
||||
t.assert.match(err.message, /timed out/);
|
||||
t.assert.strictEqual(err.cause, error);
|
||||
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);
|
||||
});
|
||||
Reference in new issue
Block a user