Skip to content

Commit 13bdd9c

Browse files
cjihrigaduh95
authored andcommitted
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: #56595 Reviewed-By: Pietro Marchini <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Chemi Atlow <[email protected]> Reviewed-By: Michaël Zasso <[email protected]>
1 parent 6a16012 commit 13bdd9c

File tree

3 files changed

+206
-1
lines changed

3 files changed

+206
-1
lines changed

doc/api/test.md

+21
Original file line numberDiff line numberDiff line change
@@ -3604,6 +3604,27 @@ test('top level test', async (t) => {
36043604
});
36053605
```
36063606

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

36093630
<!-- YAML

lib/internal/test_runner/test.js

+61-1
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,16 @@ 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+
clearTimeout,
70+
setTimeout,
71+
} = require('timers');
6672
const { TIMEOUT_MAX } = require('internal/timers');
6773
const { fileURLToPath } = require('internal/url');
6874
const { availableParallelism } = require('os');
@@ -340,6 +346,60 @@ class TestContext {
340346
loc: getCallerLocation(),
341347
});
342348
}
349+
350+
waitFor(condition, options = kEmptyObject) {
351+
validateFunction(condition, 'condition');
352+
validateObject(options, 'options');
353+
354+
const {
355+
interval = 50,
356+
timeout = 1000,
357+
} = options;
358+
359+
validateNumber(interval, 'options.interval', 0, TIMEOUT_MAX);
360+
validateNumber(timeout, 'options.timeout', 0, TIMEOUT_MAX);
361+
362+
const { promise, resolve, reject } = PromiseWithResolvers();
363+
const noError = Symbol();
364+
let cause = noError;
365+
let pollerId;
366+
let timeoutId;
367+
const done = (err, result) => {
368+
clearTimeout(pollerId);
369+
clearTimeout(timeoutId);
370+
371+
if (err === noError) {
372+
resolve(result);
373+
} else {
374+
reject(err);
375+
}
376+
};
377+
378+
timeoutId = setTimeout(() => {
379+
// eslint-disable-next-line no-restricted-syntax
380+
const err = new Error('waitFor() timed out');
381+
382+
if (cause !== noError) {
383+
err.cause = cause;
384+
}
385+
386+
done(err);
387+
}, timeout);
388+
389+
const poller = async () => {
390+
try {
391+
const result = await condition();
392+
393+
done(noError, result);
394+
} catch (err) {
395+
cause = err;
396+
pollerId = setTimeout(poller, interval);
397+
}
398+
};
399+
400+
poller();
401+
return promise;
402+
}
343403
}
344404

345405
class SuiteContext {

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

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

0 commit comments

Comments
 (0)