Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test_runner: add TestContext.prototype.waitFor() #56595

Merged
merged 3 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions doc/api/test.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
62 changes: 61 additions & 1 deletion lib/internal/test_runner/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const {
ArrayPrototypeSplice,
ArrayPrototypeUnshift,
ArrayPrototypeUnshiftApply,
Error,
FunctionPrototype,
MathMax,
Number,
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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 {
Expand Down
124 changes: 124 additions & 0 deletions test/parallel/test-runner-wait-for.js
Original file line number Diff line number Diff line change
@@ -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);
});
Loading