Skip to content
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
11 changes: 8 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- All `assert.*` functions now validate their arguments strictly: wrong argument
count, wrong types, or a non-string `message` throw immediately with a
descriptive error pointing at the call site (#99).
- `test` and `test.*` now accept an optional `options` object as the second
argument — `test(name, options, fn)` — matching `node:test` call signature.
The only supported option is `timeout` (number, in milliseconds). Every valid
x-test call is a valid `node:test` call (#99).
- All public API functions (`assert`, `assert.deepEqual`, `assert.throws`,
`assert.rejects`, `load`, `suite`, `suite.*`, `test`, `test.*`) now validate
their arguments strictly: wrong argument count, wrong types, or invalid option
keys throw immediately with a descriptive error pointing at the call site.
- `assert.throws(fn, error, message?)` and `assert.rejects(fn, error, message?)`
for asserting that a function throws or an async function rejects. The `error`
argument is a `RegExp` tested against `String(thrown)` (consistent with
Expand Down
4 changes: 2 additions & 2 deletions test/test-scratch.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ suite.only('this wrapper exercises suite only logic', () => {
});

suite.only('interval', () => {
test.todo('times out after interval - this is supposed to fail', async () => {
test.todo('times out after interval - this is supposed to fail', { timeout: 0 }, async () => {
await new Promise(resolve => setTimeout(resolve, 1_000));
assert(true);
}, 0);
});
});
4 changes: 2 additions & 2 deletions test/test-suite.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { test, suite, assert } from '../x-test.js';

for (const [label, fn] of [
for (const [name, fn] of [
['suite', suite],
['suite.skip', suite.skip],
['suite.only', suite.only],
['suite.todo', suite.todo],
]) {
suite(label, () => {
suite(name, () => {
test('accepts valid arguments', () => {
fn('valid suite', () => {});
});
Expand Down
29 changes: 17 additions & 12 deletions test/test-test.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { test, suite, assert } from '../x-test.js';

for (const [label, fn] of [
for (const [name, fn] of [
['test', test],
['test.skip', test.skip],
['test.only', test.only],
['test.todo', test.todo],
]) {
suite(label, () => {
test('accepts valid arguments', () => {
suite(name, () => {
test('accepts (name, fn)', () => {
fn('valid test', () => {});
});

test('accepts valid arguments with timeout', () => {
fn('valid test with timeout', () => {}, 1000);
test('accepts (name, options, fn)', () => {
fn('valid test with options', { timeout: 1000 }, () => {});
fn('valid test with empty options', {}, () => {});
});

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

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

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

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

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

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

test('throws on extra arguments', () => {
assert.throws(() => fn('name', () => {}, 1000, 'extra'), /^Error: unexpected extra arguments$/);
assert.throws(() => fn('name', {}, () => {}, 'extra'), /^Error: unexpected extra arguments$/);
});
});
}
74 changes: 54 additions & 20 deletions types/x-test.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,36 +83,70 @@ export namespace suite {
function todo(name: string, fn: () => void, ...args: any[]): void;
}
/**
* Register an individual test case. Alternatively, mark with flags (.skip, .only, .todo).
* @param {string} name - The description of the test case
* @param {() => void | Promise<void>} fn - The test callback function
* @param {number} [timeout] - Optional timeout in milliseconds
* @overload
* @param {string} name
* @param {() => void | Promise<void>} fn
* @returns {void}
*/
export function test(name: string, fn: () => void | Promise<void>, timeout?: number, ...args: any[]): void;
export function test(name: string, fn: () => void | Promise<void>): void;
/**
* @overload
* @param {string} name
* @param {TestOptions} options
* @param {() => void | Promise<void>} fn
* @returns {void}
*/
export function test(name: string, options: TestOptions, fn: () => void | Promise<void>): void;
export namespace test {
/**
* Register a test case that will be skipped during execution.
* @param {string} name - The description of the test case
* @param {() => void | Promise<void>} fn - The test callback function
* @param {number} [timeout] - Optional timeout in milliseconds
* @overload
* @param {string} name
* @param {() => void | Promise<void>} fn
* @returns {void}
*/
function skip(name: string, fn: () => void | Promise<void>): void;
/**
* @overload
* @param {string} name
* @param {TestOptions} options
* @param {() => void | Promise<void>} fn
* @returns {void}
*/
function skip(name: string, options: TestOptions, fn: () => void | Promise<void>): void;
/**
* @overload
* @param {string} name
* @param {() => void | Promise<void>} fn
* @returns {void}
*/
function skip(name: string, fn: () => void | Promise<void>, timeout?: number, ...args: any[]): void;
function only(name: string, fn: () => void | Promise<void>): void;
/**
* Register a test case that will run exclusively (skips other non-only tests).
* @param {string} name - The description of the test case
* @param {() => void | Promise<void>} fn - The test callback function
* @param {number} [timeout] - Optional timeout in milliseconds
* @overload
* @param {string} name
* @param {TestOptions} options
* @param {() => void | Promise<void>} fn
* @returns {void}
*/
function only(name: string, fn: () => void | Promise<void>, timeout?: number, ...args: any[]): void;
function only(name: string, options: TestOptions, fn: () => void | Promise<void>): void;
/**
* Register a placeholder test case for future implementation.
* @param {string} name - The description of the test case
* @param {() => void | Promise<void>} fn - The test callback function
* @param {number} [timeout] - Optional timeout in milliseconds
* @overload
* @param {string} name
* @param {() => void | Promise<void>} fn
* @returns {void}
*/
function todo(name: string, fn: () => void | Promise<void>, timeout?: number, ...args: any[]): void;
function todo(name: string, fn: () => void | Promise<void>): void;
/**
* @overload
* @param {string} name
* @param {TestOptions} options
* @param {() => void | Promise<void>} fn
* @returns {void}
*/
function todo(name: string, options: TestOptions, fn: () => void | Promise<void>): void;
}
export type TestOptions = {
/**
* - Timeout in milliseconds for this test case.
*/
timeout?: number | undefined;
};
Loading