Skip to content

Commit 59c71ee

Browse files
committed
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.
1 parent e6a988d commit 59c71ee

File tree

3 files changed

+182
-1
lines changed

3 files changed

+182
-1
lines changed

doc/api/test.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3608,6 +3608,26 @@ test('top level test', async (t) => {
36083608
});
36093609
```
36103610

3611+
### `context.waitFor(condition[, options])`
3612+
3613+
<!-- YAML
3614+
added: REPLACEME
3615+
-->
3616+
3617+
* `condition` {Function|AsyncFunction} A function that is invoked periodically
3618+
until it completes successfully or the defined polling timeout elapses. This
3619+
function does not accept any arguments, and is allowed to return any value.
3620+
* `options` {Object} An optional configuration object for the polling operation.
3621+
The following properties are supported:
3622+
* `interval` {number} The polling period in milliseconds. The `condition`
3623+
function is invoked according to this interval. **Default:** `50`.
3624+
* `timeout` {number} The poll timeout in milliseconds. If `condition` has not
3625+
succeeded by the time this elapses, an error occurs. **Default:** `1000`.
3626+
* Returns: {Promise} Fulfilled with the value returned by `condition`.
3627+
3628+
This method polls a `condition` function until that function either returns
3629+
successfully or the operation times out.
3630+
36113631
## Class: `SuiteContext`
36123632

36133633
<!-- YAML

lib/internal/test_runner/test.js

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const {
88
ArrayPrototypeSplice,
99
ArrayPrototypeUnshift,
1010
ArrayPrototypeUnshiftApply,
11+
Error,
1112
FunctionPrototype,
1213
MathMax,
1314
Number,
@@ -58,11 +59,18 @@ const {
5859
const { isPromise } = require('internal/util/types');
5960
const {
6061
validateAbortSignal,
62+
validateFunction,
6163
validateNumber,
64+
validateObject,
6265
validateOneOf,
6366
validateUint32,
6467
} = require('internal/validators');
65-
const { setTimeout } = require('timers');
68+
const {
69+
clearInterval,
70+
clearTimeout,
71+
setInterval,
72+
setTimeout,
73+
} = require('timers');
6674
const { TIMEOUT_MAX } = require('internal/timers');
6775
const { fileURLToPath } = require('internal/url');
6876
const { availableParallelism } = require('os');
@@ -340,6 +348,58 @@ class TestContext {
340348
loc: getCallerLocation(),
341349
});
342350
}
351+
352+
waitFor(condition, options = kEmptyObject) {
353+
validateFunction(condition, 'condition');
354+
validateObject(options, 'options');
355+
356+
const {
357+
interval = 50,
358+
timeout = 1000,
359+
} = options;
360+
361+
validateNumber(interval, 'options.interval', 0, TIMEOUT_MAX);
362+
validateNumber(timeout, 'options.timeout', 0, TIMEOUT_MAX);
363+
364+
const { promise, resolve, reject } = PromiseWithResolvers();
365+
const noError = Symbol();
366+
let cause = noError;
367+
let intervalId;
368+
let timeoutId;
369+
const done = (err, result) => {
370+
clearInterval(intervalId);
371+
clearTimeout(timeoutId);
372+
373+
if (err === noError) {
374+
resolve(result);
375+
} else {
376+
reject(err);
377+
}
378+
};
379+
380+
timeoutId = setTimeout(() => {
381+
// eslint-disable-next-line no-restricted-syntax
382+
const err = new Error('waitFor() timed out');
383+
384+
if (cause !== noError) {
385+
err.cause = cause;
386+
}
387+
388+
done(err);
389+
}, timeout);
390+
391+
intervalId = setInterval(async () => {
392+
try {
393+
const result = await condition();
394+
395+
done(noError, result);
396+
} catch (err) {
397+
cause = err;
398+
}
399+
}, interval);
400+
401+
return promise;
402+
}
343403
}
344404

345405
class SuiteContext {

test/parallel/test-runner-wait-for.js

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
'use strict';
2+
require('../common');
3+
const { test } = require('node:test');
4+
5+
test('throws if condition is not a function', (t) => {
6+
t.assert.throws(() => {
7+
t.waitFor(5);
8+
}, {
9+
code: 'ERR_INVALID_ARG_TYPE',
10+
message: /The "condition" argument must be of type function/,
11+
});
12+
});
13+
14+
test('throws if options is not an object', (t) => {
15+
t.assert.throws(() => {
16+
t.waitFor(() => {}, null);
17+
}, {
18+
code: 'ERR_INVALID_ARG_TYPE',
19+
message: /The "options" argument must be of type object/,
20+
});
21+
});
22+
23+
test('throws if options.interval is not a number', (t) => {
24+
t.assert.throws(() => {
25+
t.waitFor(() => {}, { interval: 'foo' });
26+
}, {
27+
code: 'ERR_INVALID_ARG_TYPE',
28+
message: /The "options\.interval" property must be of type number/,
29+
});
30+
});
31+
32+
test('throws if options.timeout is not a number', (t) => {
33+
t.assert.throws(() => {
34+
t.waitFor(() => {}, { timeout: 'foo' });
35+
}, {
36+
code: 'ERR_INVALID_ARG_TYPE',
37+
message: /The "options\.timeout" property must be of type number/,
38+
});
39+
});
40+
41+
test('returns the result of the condition function', async (t) => {
42+
const result = await t.waitFor(() => {
43+
return 42;
44+
});
45+
46+
t.assert.strictEqual(result, 42);
47+
});
48+
49+
test('returns the result of an async condition function', async (t) => {
50+
const result = await t.waitFor(async () => {
51+
return 84;
52+
});
53+
54+
t.assert.strictEqual(result, 84);
55+
});
56+
57+
test('errors if the condition times out', async (t) => {
58+
await t.assert.rejects(async () => {
59+
await t.waitFor(() => {
60+
return new Promise(() => {});
61+
}, {
62+
interval: 60_000,
63+
timeout: 1,
64+
});
65+
}, {
66+
message: /waitFor\(\) timed out/,
67+
});
68+
});
69+
70+
test('polls until the condition returns successfully', async (t) => {
71+
let count = 0;
72+
const result = await t.waitFor(() => {
73+
++count;
74+
if (count < 4) {
75+
throw new Error('resource is not ready yet');
76+
}
77+
78+
return 'success';
79+
}, {
80+
interval: 1,
81+
timeout: 60_000,
82+
});
83+
84+
t.assert.strictEqual(result, 'success');
85+
t.assert.strictEqual(count, 4);
86+
});
87+
88+
test('sets last failure as error cause on timeouts', async (t) => {
89+
const error = new Error('boom');
90+
await t.assert.rejects(async () => {
91+
await t.waitFor(() => {
92+
return new Promise((_, reject) => {
93+
reject(error);
94+
});
95+
});
96+
}, (err) => {
97+
t.assert.match(err.message, /timed out/);
98+
t.assert.strictEqual(err.cause, error);
99+
return true;
100+
});
101+
});

0 commit comments

Comments
 (0)