Skip to content

Commit c06d0f0

Browse files
committed
Validate all top-level interfaces.
This is a developer-experience thing. It’s nice when tools fail early, fast, and clearly. It’s some verbosity in the library, but it’s all very simple code.
1 parent 0aad35a commit c06d0f0

11 files changed

Lines changed: 408 additions & 80 deletions

test/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,8 @@ load('./test-root.html');
66
load('./test-tap.html');
77
load('./test-common.html');
88
load('./test-assert.html');
9+
load('./test-load.html');
10+
load('./test-suite.html');
11+
load('./test-test.html');
912
load('./test-boot.html');
1013
load('./test-scratch.html');

test/test-frame.js

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -362,19 +362,7 @@ suite('suite', () => {
362362
assert(context.publish.calls[1][1].error.message === 'test failure');
363363
});
364364

365-
test('throws if callback is not a function', () => {
366-
const { context } = getContext();
367-
const callback = null;
368-
const expected = 'Unexpected callback value "null".';
369-
let actual;
370-
try {
371-
XTestFrame.suite(context, 'description', callback);
372-
} catch (error) {
373-
actual = error.message;
374-
}
375-
assert(context.publish.calls.length === 0);
376-
assert(actual === expected, actual);
377-
});
365+
378366
});
379367

380368
suite('test', () => {
@@ -403,7 +391,7 @@ suite('test', () => {
403391
test('this will break', 'this is expected to fail');
404392
} catch (error) {
405393
message = error.message;
406-
passed = error.message === 'Unexpected callback value "this is expected to fail".';
394+
passed = error.message === 'unexpected fn, expected Function but got "this is expected to fail"';
407395
}
408396
test('throws if "test" is not given a function as a callback', () => {
409397
assert(passed, message);

test/test-load.html

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<meta http-equiv="content-security-policy" content="default-src 'self';">
6+
</head>
7+
<body>
8+
<h3>test-load</h3>
9+
<script type="module" src="test-load.js"></script>
10+
</body>
11+
</html>

test/test-load.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { test, suite, assert, load } from '../x-test.js';
2+
3+
suite('load', () => {
4+
test('throws with no arguments', () => {
5+
assert.throws(() => load(), /^Error: expected href argument, but got none$/);
6+
});
7+
8+
test('throws if href is not a string', () => {
9+
assert.throws(() => load(42), /^Error: unexpected href, expected string but got "42"$/);
10+
});
11+
12+
test('throws on extra arguments', () => {
13+
assert.throws(() => load('./foo.html', 'extra'), /^Error: unexpected extra arguments$/);
14+
});
15+
});

test/test-suite.html

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<meta http-equiv="content-security-policy" content="default-src 'self';">
6+
</head>
7+
<body>
8+
<h3>test-suite</h3>
9+
<script type="module" src="test-suite.js"></script>
10+
</body>
11+
</html>

test/test-suite.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { test, suite, assert } from '../x-test.js';
2+
3+
for (const [label, fn] of [
4+
['suite', suite],
5+
['suite.skip', suite.skip],
6+
['suite.only', suite.only],
7+
['suite.todo', suite.todo],
8+
]) {
9+
suite(label, () => {
10+
test('accepts valid arguments', () => {
11+
fn('valid suite', () => {});
12+
});
13+
14+
test('throws with too few arguments', () => {
15+
assert.throws(() => fn(), /^Error: expected name and fn arguments, but got too few arguments$/);
16+
assert.throws(() => fn('name'), /^Error: expected name and fn arguments, but got too few arguments$/);
17+
});
18+
19+
test('throws if name is not a string', () => {
20+
assert.throws(() => fn(42, () => {}), /^Error: unexpected name, expected string but got "42"$/);
21+
});
22+
23+
test('throws if fn is not a Function', () => {
24+
assert.throws(() => fn('name', 'not-a-function'), /^Error: unexpected fn, expected Function but got "not-a-function"$/);
25+
});
26+
27+
test('throws on extra arguments', () => {
28+
assert.throws(() => fn('name', () => {}, 'extra'), /^Error: unexpected extra arguments$/);
29+
});
30+
});
31+
}

test/test-test.html

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<meta http-equiv="content-security-policy" content="default-src 'self';">
6+
</head>
7+
<body>
8+
<h3>test-test</h3>
9+
<script type="module" src="test-test.js"></script>
10+
</body>
11+
</html>

test/test-test.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { test, suite, assert } from '../x-test.js';
2+
3+
for (const [label, fn] of [
4+
['test', test],
5+
['test.skip', test.skip],
6+
['test.only', test.only],
7+
['test.todo', test.todo],
8+
]) {
9+
suite(label, () => {
10+
test('accepts valid arguments', () => {
11+
fn('valid test', () => {});
12+
});
13+
14+
test('accepts valid arguments with timeout', () => {
15+
fn('valid test with timeout', () => {}, 1000);
16+
});
17+
18+
test('throws with too few arguments', () => {
19+
assert.throws(() => fn(), /^Error: expected name and fn arguments, but got too few arguments$/);
20+
assert.throws(() => fn('name'), /^Error: expected name and fn arguments, but got too few arguments$/);
21+
});
22+
23+
test('throws if name is not a string', () => {
24+
assert.throws(() => fn(42, () => {}), /^Error: unexpected name, expected string but got "42"$/);
25+
});
26+
27+
test('throws if fn is not a Function', () => {
28+
assert.throws(() => fn('name', 'not-a-function'), /^Error: unexpected fn, expected Function but got "not-a-function"$/);
29+
});
30+
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"$/);
33+
});
34+
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"$/);
37+
});
38+
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"$/);
41+
});
42+
43+
test('throws on extra arguments', () => {
44+
assert.throws(() => fn('name', () => {}, 1000, 'extra'), /^Error: unexpected extra arguments$/);
45+
});
46+
});
47+
}

types/x-test.d.ts

Lines changed: 44 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -44,55 +44,75 @@ export namespace assert {
4444
*/
4545
function rejects(fn: () => Promise<unknown>, error: RegExp, message?: string, ...args: any[]): Promise<void>;
4646
}
47-
export function load(href: string): void;
48-
export function suite(text: string, callback: () => void): void;
47+
/**
48+
* Load a new frame.
49+
* @example
50+
* load('./test-sibling.html');
51+
* @param {string} href - The URL/path to the test file to run
52+
* @returns {void}
53+
*/
54+
export function load(href: string, ...args: any[]): void;
55+
/**
56+
* Register a grouping of tests. Alternatively, mark with flags (.skip, .only, .todo).
57+
* @param {string} name - The description of the test group
58+
* @param {() => void} fn - The callback function containing nested tests
59+
* @returns {void}
60+
*/
61+
export function suite(name: string, fn: () => void, ...args: any[]): void;
4962
export namespace suite {
5063
/**
5164
* Register a test group that will be skipped during execution.
52-
* @param {string} text - The description of the test group
53-
* @param {() => void} callback - The callback function containing nested tests
65+
* @param {string} name - The description of the test group
66+
* @param {() => void} fn - The callback function containing nested tests
5467
* @returns {void}
5568
*/
56-
function skip(text: string, callback: () => void): void;
69+
function skip(name: string, fn: () => void, ...args: any[]): void;
5770
/**
5871
* Register a test group that will run exclusively (skips other non-only tests).
59-
* @param {string} text - The description of the test group
60-
* @param {() => void} callback - The callback function containing nested tests
72+
* @param {string} name - The description of the test group
73+
* @param {() => void} fn - The callback function containing nested tests
6174
* @returns {void}
6275
*/
63-
function only(text: string, callback: () => void): void;
76+
function only(name: string, fn: () => void, ...args: any[]): void;
6477
/**
6578
* Register a placeholder test group for future implementation.
66-
* @param {string} text - The description of the test group
67-
* @param {() => void} callback - The callback function containing nested tests
79+
* @param {string} name - The description of the test group
80+
* @param {() => void} fn - The callback function containing nested tests
6881
* @returns {void}
6982
*/
70-
function todo(text: string, callback: () => void): void;
83+
function todo(name: string, fn: () => void, ...args: any[]): void;
7184
}
72-
export function test(text: string, callback: () => void | Promise<void>, interval?: number): void;
85+
/**
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
90+
* @returns {void}
91+
*/
92+
export function test(name: string, fn: () => void | Promise<void>, timeout?: number, ...args: any[]): void;
7393
export namespace test {
7494
/**
7595
* Register a test case that will be skipped during execution.
76-
* @param {string} text - The description of the test case
77-
* @param {() => void | Promise<void>} callback - The test callback function
78-
* @param {number} [interval] - Optional timeout in milliseconds
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
7999
* @returns {void}
80100
*/
81-
function skip(text: string, callback: () => void | Promise<void>, interval?: number): void;
101+
function skip(name: string, fn: () => void | Promise<void>, timeout?: number, ...args: any[]): void;
82102
/**
83103
* Register a test case that will run exclusively (skips other non-only tests).
84-
* @param {string} text - The description of the test case
85-
* @param {() => void | Promise<void>} callback - The test callback function
86-
* @param {number} [interval] - Optional timeout in milliseconds
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
87107
* @returns {void}
88108
*/
89-
function only(text: string, callback: () => void | Promise<void>, interval?: number): void;
109+
function only(name: string, fn: () => void | Promise<void>, timeout?: number, ...args: any[]): void;
90110
/**
91111
* Register a placeholder test case for future implementation.
92-
* @param {string} text - The description of the test case
93-
* @param {() => void | Promise<void>} callback - The test callback function
94-
* @param {number} [interval] - Optional timeout in milliseconds
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
95115
* @returns {void}
96116
*/
97-
function todo(text: string, callback: () => void | Promise<void>, interval?: number): void;
117+
function todo(name: string, fn: () => void | Promise<void>, timeout?: number, ...args: any[]): void;
98118
}

x-test-frame.js

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -266,9 +266,6 @@ export class XTestFrame {
266266
*/
267267
static #suiteInner(context, text, callback, directive, only) {
268268
if (context && !context.state.bailed && !context.state.ready) {
269-
if (!(callback instanceof Function)) {
270-
throw new Error(`Unexpected callback value "${callback}".`);
271-
}
272269
const suiteId = context.uuid();
273270
const parents = [...context.state.parents];
274271
directive = directive ?? null;
@@ -328,15 +325,12 @@ export class XTestFrame {
328325
* @param {any} context
329326
* @param {any} text
330327
* @param {any} callback
331-
* @param {any} interval
332-
* @param {any} directive
333-
* @param {any} only
328+
* @param {any} [interval]
329+
* @param {any} [directive]
330+
* @param {any} [only]
334331
*/
335332
static #testInner(context, text, callback, interval, directive, only) {
336333
if (context && !context.state.bailed && !context.state.ready) {
337-
if (!(callback instanceof Function)) {
338-
throw new Error(`Unexpected callback value "${callback}".`);
339-
}
340334
const testId = context.uuid();
341335
const parents = [...context.state.parents];
342336
interval = interval ?? null;
@@ -354,7 +348,7 @@ export class XTestFrame {
354348
* @param {any} context
355349
* @param {any} text
356350
* @param {any} callback
357-
* @param {any} interval
351+
* @param {any} [interval]
358352
*/
359353
static test(context, text, callback, interval) {
360354
XTestFrame.#testInner(context, text, callback, interval, null, null);
@@ -364,7 +358,7 @@ export class XTestFrame {
364358
* @param {any} context
365359
* @param {any} text
366360
* @param {any} callback
367-
* @param {any} interval
361+
* @param {any} [interval]
368362
*/
369363
static testSkip(context, text, callback, interval) {
370364
XTestFrame.#testInner(context, text, callback, interval, 'SKIP', null);
@@ -374,7 +368,7 @@ export class XTestFrame {
374368
* @param {any} context
375369
* @param {any} text
376370
* @param {any} callback
377-
* @param {any} interval
371+
* @param {any} [interval]
378372
*/
379373
static testOnly(context, text, callback, interval) {
380374
XTestFrame.#testInner(context, text, callback, interval, null, true);
@@ -384,7 +378,7 @@ export class XTestFrame {
384378
* @param {any} context
385379
* @param {any} text
386380
* @param {any} callback
387-
* @param {any} interval
381+
* @param {any} [interval]
388382
*/
389383
static testTodo(context, text, callback, interval) {
390384
XTestFrame.#testInner(context, text, callback, interval, 'TODO', null);

0 commit comments

Comments
 (0)