Skip to content

Commit 55c3944

Browse files
committed
Finalize “test” interface (adds “options”).
We aim to be a _strict subset_ of what `node:test` offers — no real reason to diverge there. As such, rather than take `interval` as an optional, positional argument, we should match Node’s _options_ interface which allows for `{ timeout?: number }` to be defined.
1 parent c06d0f0 commit 55c3944

6 files changed

Lines changed: 228 additions & 87 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88

99
### Added
1010

11-
- All `assert.*` functions now validate their arguments strictly: wrong argument
12-
count, wrong types, or a non-string `message` throw immediately with a
13-
descriptive error pointing at the call site (#99).
11+
- `test` and `test.*` now accept an optional `options` object as the second
12+
argument — `test(name, options, fn)` — matching `node:test` call signature.
13+
The only supported option is `timeout` (number, in milliseconds). Every valid
14+
x-test call is a valid `node:test` call (#99).
15+
- All public API functions (`assert`, `assert.deepEqual`, `assert.throws`,
16+
`assert.rejects`, `load`, `suite`, `suite.*`, `test`, `test.*`) now validate
17+
their arguments strictly: wrong argument count, wrong types, or invalid option
18+
keys throw immediately with a descriptive error pointing at the call site.
1419
- `assert.throws(fn, error, message?)` and `assert.rejects(fn, error, message?)`
1520
for asserting that a function throws or an async function rejects. The `error`
1621
argument is a `RegExp` tested against `String(thrown)` (consistent with

test/test-scratch.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ suite.only('this wrapper exercises suite only logic', () => {
2828
});
2929

3030
suite.only('interval', () => {
31-
test.todo('times out after interval - this is supposed to fail', async () => {
31+
test.todo('times out after interval - this is supposed to fail', { timeout: 0 }, async () => {
3232
await new Promise(resolve => setTimeout(resolve, 1_000));
3333
assert(true);
34-
}, 0);
34+
});
3535
});

test/test-suite.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { test, suite, assert } from '../x-test.js';
22

3-
for (const [label, fn] of [
3+
for (const [name, fn] of [
44
['suite', suite],
55
['suite.skip', suite.skip],
66
['suite.only', suite.only],
77
['suite.todo', suite.todo],
88
]) {
9-
suite(label, () => {
9+
suite(name, () => {
1010
test('accepts valid arguments', () => {
1111
fn('valid suite', () => {});
1212
});

test/test-test.js

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
import { test, suite, assert } from '../x-test.js';
22

3-
for (const [label, fn] of [
3+
for (const [name, fn] of [
44
['test', test],
55
['test.skip', test.skip],
66
['test.only', test.only],
77
['test.todo', test.todo],
88
]) {
9-
suite(label, () => {
10-
test('accepts valid arguments', () => {
9+
suite(name, () => {
10+
test('accepts (name, fn)', () => {
1111
fn('valid test', () => {});
1212
});
1313

14-
test('accepts valid arguments with timeout', () => {
15-
fn('valid test with timeout', () => {}, 1000);
14+
test('accepts (name, options, fn)', () => {
15+
fn('valid test with options', { timeout: 1000 }, () => {});
16+
fn('valid test with empty options', {}, () => {});
1617
});
1718

1819
test('throws with too few arguments', () => {
@@ -22,26 +23,30 @@ for (const [label, fn] of [
2223

2324
test('throws if name is not a string', () => {
2425
assert.throws(() => fn(42, () => {}), /^Error: unexpected name, expected string but got "42"$/);
26+
assert.throws(() => fn(42, {}, () => {}), /^Error: unexpected name, expected string but got "42"$/);
2527
});
2628

2729
test('throws if fn is not a Function', () => {
2830
assert.throws(() => fn('name', 'not-a-function'), /^Error: unexpected fn, expected Function but got "not-a-function"$/);
31+
assert.throws(() => fn('name', {}, 'not-a-function'), /^Error: unexpected fn, expected Function but got "not-a-function"$/);
2932
});
3033

31-
test('throws if name is not a string with timeout', () => {
32-
assert.throws(() => fn(42, () => {}, 1000), /^Error: unexpected name, expected string but got "42"$/);
34+
test('throws if options is not a plain object', () => {
35+
assert.throws(() => fn('name', null, () => {}), /^Error: unexpected options, expected object but got "null"$/);
36+
assert.throws(() => fn('name', 42, () => {}), /^Error: unexpected options, expected object but got "42"$/);
37+
assert.throws(() => fn('name', [], () => {}), /^Error: unexpected options, expected object but got ""$/);
3338
});
3439

35-
test('throws if fn is not a Function with timeout', () => {
36-
assert.throws(() => fn('name', 'not-a-function', 1000), /^Error: unexpected fn, expected Function but got "not-a-function"$/);
40+
test('throws if options has unexpected keys', () => {
41+
assert.throws(() => fn('name', { unknown: true }, () => {}), /^Error: unexpected options key "unknown"$/);
3742
});
3843

39-
test('throws if timeout is not a number', () => {
40-
assert.throws(() => fn('name', () => {}, 'not-a-number'), /^Error: unexpected timeout, expected number but got "not-a-number"$/);
44+
test('throws if options.timeout is not a number', () => {
45+
assert.throws(() => fn('name', { timeout: 'bad' }, () => {}), /^Error: unexpected options\.timeout, expected number but got "bad"$/);
4146
});
4247

4348
test('throws on extra arguments', () => {
44-
assert.throws(() => fn('name', () => {}, 1000, 'extra'), /^Error: unexpected extra arguments$/);
49+
assert.throws(() => fn('name', {}, () => {}, 'extra'), /^Error: unexpected extra arguments$/);
4550
});
4651
});
4752
}

types/x-test.d.ts

Lines changed: 54 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -83,36 +83,70 @@ export namespace suite {
8383
function todo(name: string, fn: () => void, ...args: any[]): void;
8484
}
8585
/**
86-
* Register an individual test case. Alternatively, mark with flags (.skip, .only, .todo).
87-
* @param {string} name - The description of the test case
88-
* @param {() => void | Promise<void>} fn - The test callback function
89-
* @param {number} [timeout] - Optional timeout in milliseconds
86+
* @overload
87+
* @param {string} name
88+
* @param {() => void | Promise<void>} fn
9089
* @returns {void}
9190
*/
92-
export function test(name: string, fn: () => void | Promise<void>, timeout?: number, ...args: any[]): void;
91+
export function test(name: string, fn: () => void | Promise<void>): void;
92+
/**
93+
* @overload
94+
* @param {string} name
95+
* @param {TestOptions} options
96+
* @param {() => void | Promise<void>} fn
97+
* @returns {void}
98+
*/
99+
export function test(name: string, options: TestOptions, fn: () => void | Promise<void>): void;
93100
export namespace test {
94101
/**
95-
* Register a test case that will be skipped during execution.
96-
* @param {string} name - The description of the test case
97-
* @param {() => void | Promise<void>} fn - The test callback function
98-
* @param {number} [timeout] - Optional timeout in milliseconds
102+
* @overload
103+
* @param {string} name
104+
* @param {() => void | Promise<void>} fn
105+
* @returns {void}
106+
*/
107+
function skip(name: string, fn: () => void | Promise<void>): void;
108+
/**
109+
* @overload
110+
* @param {string} name
111+
* @param {TestOptions} options
112+
* @param {() => void | Promise<void>} fn
113+
* @returns {void}
114+
*/
115+
function skip(name: string, options: TestOptions, fn: () => void | Promise<void>): void;
116+
/**
117+
* @overload
118+
* @param {string} name
119+
* @param {() => void | Promise<void>} fn
99120
* @returns {void}
100121
*/
101-
function skip(name: string, fn: () => void | Promise<void>, timeout?: number, ...args: any[]): void;
122+
function only(name: string, fn: () => void | Promise<void>): void;
102123
/**
103-
* Register a test case that will run exclusively (skips other non-only tests).
104-
* @param {string} name - The description of the test case
105-
* @param {() => void | Promise<void>} fn - The test callback function
106-
* @param {number} [timeout] - Optional timeout in milliseconds
124+
* @overload
125+
* @param {string} name
126+
* @param {TestOptions} options
127+
* @param {() => void | Promise<void>} fn
107128
* @returns {void}
108129
*/
109-
function only(name: string, fn: () => void | Promise<void>, timeout?: number, ...args: any[]): void;
130+
function only(name: string, options: TestOptions, fn: () => void | Promise<void>): void;
110131
/**
111-
* Register a placeholder test case for future implementation.
112-
* @param {string} name - The description of the test case
113-
* @param {() => void | Promise<void>} fn - The test callback function
114-
* @param {number} [timeout] - Optional timeout in milliseconds
132+
* @overload
133+
* @param {string} name
134+
* @param {() => void | Promise<void>} fn
115135
* @returns {void}
116136
*/
117-
function todo(name: string, fn: () => void | Promise<void>, timeout?: number, ...args: any[]): void;
137+
function todo(name: string, fn: () => void | Promise<void>): void;
138+
/**
139+
* @overload
140+
* @param {string} name
141+
* @param {TestOptions} options
142+
* @param {() => void | Promise<void>} fn
143+
* @returns {void}
144+
*/
145+
function todo(name: string, options: TestOptions, fn: () => void | Promise<void>): void;
118146
}
147+
export type TestOptions = {
148+
/**
149+
* - Timeout in milliseconds for this test case.
150+
*/
151+
timeout?: number | undefined;
152+
};

0 commit comments

Comments
 (0)