qunitx 1.2.19 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +388 -198
- package/dist/.build-hash +1 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/browser/index.d.ts +33 -0
- package/dist/browser/index.js +10 -3
- package/dist/deno/index.d.ts +97 -3
- package/dist/deno/index.js +10 -1
- package/dist/node/index.d.ts +4 -0
- package/dist/node/index.js +15 -4
- package/package.json +1 -1
- package/shims/deno/index.ts +105 -4
package/README.md
CHANGED
|
@@ -2,90 +2,117 @@
|
|
|
2
2
|
|
|
3
3
|
# QUnitX
|
|
4
4
|
|
|
5
|
+
**One test file. Node.js, Deno, and the browser. No config.**
|
|
6
|
+
|
|
5
7
|
[](https://github.com/izelnakri/qunitx/actions/workflows/ci.yml)
|
|
6
8
|
[](https://codecov.io/gh/izelnakri/qunitx)
|
|
7
9
|
[](https://www.npmjs.com/package/qunitx)
|
|
10
|
+
[](https://jsr.io/@izelnakri/qunitx)
|
|
11
|
+
[](https://jsr.io/@izelnakri/qunitx)
|
|
8
12
|
[](https://www.npmjs.com/package/qunitx)
|
|
9
13
|
[](LICENSE)
|
|
10
|
-
[](https://github.com/izelnakri/qunitx/issues)
|
|
11
14
|
[](https://github.com/sponsors/izelnakri)
|
|
12
15
|
|
|
13
|
-
|
|
16
|
+

|
|
14
17
|
|
|
15
|
-
|
|
18
|
+
<sub>Left: `node --test` and `deno test` on the same file, including watch mode going red → green.<br>
|
|
19
|
+
Right: the same file in real Chrome, with QUnit's filterable, shareable UI.</sub>
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
Zero dependencies. No config needed for Node. TypeScript works out of the box.
|
|
21
|
+
</div>
|
|
19
22
|
|
|
20
23
|
---
|
|
21
24
|
|
|
22
|
-
|
|
25
|
+
Write a test once. Run it under Node's built-in runner, Deno's built-in runner, and a
|
|
26
|
+
real browser — the **same file**, unchanged, each runtime its usual way.
|
|
23
27
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
tools are still catching up to. Its assertion API is the most mature in the JavaScript
|
|
27
|
-
ecosystem:
|
|
28
|
-
|
|
29
|
-
- **`assert.deepEqual`** — handles circular references, prototype chains, Sets, Maps,
|
|
30
|
-
typed arrays, Dates, RegExps, and getters correctly
|
|
31
|
-
- **`assert.throws` / `assert.rejects`** — match errors by constructor, regex, or custom validator
|
|
32
|
-
- **`assert.step` / `assert.verifySteps`** — declarative execution-order verification;
|
|
33
|
-
catches missing async callbacks that other frameworks silently swallow
|
|
34
|
-
- **`assert.expect(n)`** — fails the test if exactly _n_ assertions didn't run;
|
|
35
|
-
invaluable for async code where missing assertions would otherwise pass silently
|
|
36
|
-
- **`assert.timeout(ms)`** — hard deadline per test; the test fails with a descriptive
|
|
37
|
-
message if it doesn't complete in time (all three runtimes)
|
|
38
|
-
- **Hooks** — `before`, `beforeEach`, `afterEach`, `after` with correct FIFO/LIFO ordering,
|
|
39
|
-
properly scoped across nested modules
|
|
40
|
-
- **Shareable browser URLs** — the QUnit browser UI filters tests via query params, so you can
|
|
41
|
-
share `https://yourapp.test/?moduleId=abc123` with a colleague and they see exactly the same view
|
|
42
|
-
|
|
43
|
-
QUnitX wraps this API to work with **Node.js's built-in `node:test` runner** and
|
|
44
|
-
**Deno's native test runner** — no Jest, Vitest, or other framework needed.
|
|
45
|
-
|
|
46
|
-
QUnit includes the fastest assertion and test runtime in JS world. I've previously contributed to some [speed optimizations](https://qunitjs.com/blog/2022/02/15/qunit-2-18-0/) to QUnit, we benchmark every possible thing to make it the fastest test
|
|
47
|
-
runtime, faster than node.js and deno default assertions in most cases. Therefore I consider myself very objective
|
|
48
|
-
when I say QUnit(X) is the best JS/TS testing tool out there.
|
|
28
|
+
```ts
|
|
29
|
+
import { module, test } from 'qunitx';
|
|
49
30
|
|
|
50
|
-
|
|
31
|
+
module('Math', () => {
|
|
32
|
+
test('addition', (assert) => {
|
|
33
|
+
assert.equal(2 + 2, 4);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
```
|
|
51
37
|
|
|
52
|
-
|
|
38
|
+
```sh
|
|
39
|
+
node --test math-test.ts # Node's runner
|
|
40
|
+
deno test math-test.ts # Deno's runner
|
|
41
|
+
qunitx math-test.ts # real browser, TAP in your terminal
|
|
42
|
+
```
|
|
53
43
|
|
|
54
|
-
|
|
55
|
-
|
|
44
|
+
No `jest.config.js`. No `vitest.config.ts`. No setup files, no transform pipeline, no
|
|
45
|
+
globals to inject. TypeScript runs as-is. **Zero runtime dependencies.**
|
|
56
46
|
|
|
57
|
-
|
|
58
|
-
For the combined terminal + browser recording, see "Recording the demo" below. -->
|
|
59
|
-

|
|
47
|
+
---
|
|
60
48
|
|
|
61
|
-
|
|
49
|
+
## Why QUnit?
|
|
62
50
|
|
|
51
|
+
QUnit was created in 2008 by the jQuery team. Newer frameworks come and go; QUnit has
|
|
52
|
+
quietly accumulated 16+ years of real-world edge-case handling that younger tools are
|
|
53
|
+
still catching up to. Its assertion API is the most mature in the JavaScript ecosystem:
|
|
54
|
+
|
|
55
|
+
- **`assert.deepEqual`** handles circular references, prototype chains, Sets, Maps,
|
|
56
|
+
typed arrays, Dates, RegExps and getters correctly — the cases that quietly return
|
|
57
|
+
the wrong answer elsewhere.
|
|
58
|
+
- **`assert.throws` / `assert.rejects`** match errors by constructor, regex, error
|
|
59
|
+
instance, or a custom validator.
|
|
60
|
+
- **`assert.step` / `verifySteps`** turn execution order into a declarative assertion,
|
|
61
|
+
and catch missing async callbacks that other frameworks silently swallow.
|
|
62
|
+
- **`assert.expect(n)`** fails the test when exactly _n_ assertions did not run —
|
|
63
|
+
invaluable for async code, where a missing assertion otherwise looks like a pass.
|
|
64
|
+
- **`assert.timeout(ms)`** is a hard per-test deadline, on all three runtimes.
|
|
65
|
+
- **Hooks** — `before`, `beforeEach`, `afterEach`, `after` with correct FIFO/LIFO
|
|
66
|
+
ordering, properly scoped across nested modules.
|
|
67
|
+
- **Shareable browser URLs** — the QUnit UI keeps filter state in the query string, so
|
|
68
|
+
`?moduleId=abc123` reproduces exactly what you were looking at for a colleague.
|
|
69
|
+
|
|
70
|
+
It is also fast. I've contributed [speed optimisations to QUnit itself](https://qunitjs.com/blog/2022/02/15/qunit-2-18-0/),
|
|
71
|
+
and we benchmark every part of this wrapper — in most cases the assertions beat Node's
|
|
72
|
+
and Deno's built-in ones. So I am not especially objective when I say QUnit(X) is the
|
|
73
|
+
best testing tool in JS/TS. But I have the benchmarks.
|
|
74
|
+
|
|
75
|
+
QUnitX wraps that API over **`node:test`** and **Deno's native runner**. No Jest, no
|
|
76
|
+
Vitest, no second framework underneath.
|
|
77
|
+
|
|
78
|
+
**Live example** — a real suite in the browser UI, filtered and linkable:
|
|
63
79
|
[objectmodel.js.org/test/?moduleId=6e15ed5f](https://objectmodel.js.org/test/?moduleId=6e15ed5f&moduleId=950ec9c5)
|
|
64
80
|
|
|
65
81
|
---
|
|
66
82
|
|
|
67
|
-
##
|
|
83
|
+
## Install
|
|
68
84
|
|
|
69
85
|
```sh
|
|
70
86
|
npm install qunitx --save-dev
|
|
71
87
|
```
|
|
72
88
|
|
|
73
|
-
|
|
89
|
+
Deno users can install from JSR instead — same library, published from CI with
|
|
90
|
+
[provenance](https://jsr.io/@izelnakri/qunitx):
|
|
74
91
|
|
|
75
|
-
|
|
76
|
-
|
|
92
|
+
```sh
|
|
93
|
+
deno add jsr:@izelnakri/qunitx
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Requires **Node.js >= 22** (LTS) or **Deno >= 2**. For a Deno project with no
|
|
97
|
+
`package.json`, an import map is enough:
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
echo '{"imports": {"qunitx": "npm:qunitx"}}' > deno.json
|
|
101
|
+
```
|
|
77
102
|
|
|
78
103
|
---
|
|
79
104
|
|
|
80
105
|
## Quick start
|
|
81
106
|
|
|
107
|
+
One file, every runtime:
|
|
108
|
+
|
|
82
109
|
```ts
|
|
83
|
-
// math-test.ts
|
|
110
|
+
// math-test.ts
|
|
84
111
|
import { module, test } from 'qunitx';
|
|
85
112
|
|
|
86
113
|
module('Math utilities', (hooks) => {
|
|
87
|
-
hooks.
|
|
88
|
-
|
|
114
|
+
hooks.beforeEach(function () {
|
|
115
|
+
this.numbers = [1, 2, 3];
|
|
89
116
|
});
|
|
90
117
|
|
|
91
118
|
test('addition', (assert) => {
|
|
@@ -93,14 +120,17 @@ module('Math utilities', (hooks) => {
|
|
|
93
120
|
assert.notEqual(2 + 2, 5);
|
|
94
121
|
});
|
|
95
122
|
|
|
96
|
-
test('deepEqual', (assert) => {
|
|
97
|
-
assert.deepEqual(
|
|
123
|
+
test('deepEqual understands Sets and Maps', (assert) => {
|
|
124
|
+
assert.deepEqual(
|
|
125
|
+
{ tags: new Set(['js']), scores: new Map([['a', 1]]) },
|
|
126
|
+
{ tags: new Set(['js']), scores: new Map([['a', 1]]) },
|
|
127
|
+
);
|
|
98
128
|
});
|
|
99
129
|
|
|
100
130
|
module('Async', () => {
|
|
101
|
-
test('resolves
|
|
102
|
-
const
|
|
103
|
-
assert.
|
|
131
|
+
test('resolves', async (assert) => {
|
|
132
|
+
const user = await Promise.resolve({ id: 1, name: 'Alice' });
|
|
133
|
+
assert.propContains(user, { name: 'Alice' });
|
|
104
134
|
});
|
|
105
135
|
});
|
|
106
136
|
});
|
|
@@ -109,55 +139,34 @@ module('Math utilities', (hooks) => {
|
|
|
109
139
|
### Node.js
|
|
110
140
|
|
|
111
141
|
```sh
|
|
112
|
-
|
|
113
|
-
node --test math-test.ts
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
node --test --watch math-test.ts
|
|
117
|
-
|
|
118
|
-
# Glob pattern
|
|
119
|
-
node --test --watch 'test/**/*.ts'
|
|
120
|
-
|
|
121
|
-
# Code coverage
|
|
122
|
-
npx c8 node --test math-test.ts
|
|
142
|
+
node --test math-test.ts # built-in runner, no extra deps
|
|
143
|
+
node --test --watch math-test.ts # rerun on save
|
|
144
|
+
node --test --watch 'test/**/*.ts' # globs
|
|
145
|
+
npx c8 node --test math-test.ts # coverage
|
|
123
146
|
```
|
|
124
147
|
|
|
125
148
|
### Deno
|
|
126
149
|
|
|
127
150
|
```sh
|
|
128
|
-
# Install qunitx once (same command as Node)
|
|
129
|
-
npm install qunitx --save-dev
|
|
130
|
-
|
|
131
|
-
# Run
|
|
132
151
|
deno test math-test.ts
|
|
133
|
-
|
|
134
|
-
# With explicit permissions
|
|
135
152
|
deno test --allow-read --allow-env math-test.ts
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
For Deno-only projects without a `package.json`, use an import map instead:
|
|
139
|
-
|
|
140
|
-
```sh
|
|
141
|
-
echo '{"imports": {"qunitx": "npm:qunitx"}}' > deno.json
|
|
142
|
-
deno test math-test.ts
|
|
153
|
+
deno test --coverage=cov math-test.ts
|
|
143
154
|
```
|
|
144
155
|
|
|
145
156
|
### Browser
|
|
146
157
|
|
|
147
|
-
|
|
148
|
-
|
|
158
|
+
[qunitx-cli](https://github.com/izelnakri/qunitx-cli) bundles your test file, runs it in
|
|
159
|
+
a real browser, and streams TAP back to your terminal:
|
|
149
160
|
|
|
150
161
|
```sh
|
|
151
162
|
npm install -g qunitx-cli
|
|
152
163
|
|
|
153
|
-
#
|
|
154
|
-
qunitx math-test.ts
|
|
155
|
-
|
|
156
|
-
# Open QUnit browser UI alongside terminal output
|
|
157
|
-
qunitx math-test.ts --debug
|
|
164
|
+
qunitx math-test.ts # headless — TAP on stdout, CI-friendly
|
|
165
|
+
qunitx math-test.ts --debug # opens the live QUnit UI too
|
|
158
166
|
```
|
|
159
167
|
|
|
160
168
|
The browser UI lets you:
|
|
169
|
+
|
|
161
170
|
- Filter by module or test name (filter state is preserved in the URL)
|
|
162
171
|
- Share a link that reproduces the exact filtered view with a colleague
|
|
163
172
|
- Re-run individual tests by clicking them
|
|
@@ -170,206 +179,387 @@ The browser UI lets you:
|
|
|
170
179
|
|
|
171
180
|
---
|
|
172
181
|
|
|
173
|
-
##
|
|
182
|
+
## Assertions
|
|
183
|
+
|
|
184
|
+
Every assertion takes an optional trailing `message` used in the failure output. This is
|
|
185
|
+
the complete list.
|
|
186
|
+
|
|
187
|
+
### Truthiness
|
|
188
|
+
|
|
189
|
+
| | |
|
|
190
|
+
|---|---|
|
|
191
|
+
| `assert.ok(value)` | passes if `value` is truthy |
|
|
192
|
+
| `assert.notOk(value)` | passes if `value` is falsy |
|
|
193
|
+
| `assert.true(value)` | passes only for exactly `true` |
|
|
194
|
+
| `assert.false(value)` | passes only for exactly `false` |
|
|
174
195
|
|
|
175
|
-
|
|
196
|
+
### Equality
|
|
197
|
+
|
|
198
|
+
| | |
|
|
199
|
+
|---|---|
|
|
200
|
+
| `assert.equal(actual, expected)` | loose `==`, after QUnit's coercion rules |
|
|
201
|
+
| `assert.notEqual(actual, expected)` | loose `!=` |
|
|
202
|
+
| `assert.strictEqual(actual, expected)` | strict `===` — the one you usually want |
|
|
203
|
+
| `assert.notStrictEqual(actual, expected)` | strict `!==` |
|
|
204
|
+
| `assert.deepEqual(actual, expected)` | deep structural equality: nested objects, arrays, `Set`, `Map`, typed arrays, `Date`, `RegExp`, circular references |
|
|
205
|
+
| `assert.notDeepEqual(actual, expected)` | inverse of `deepEqual` |
|
|
206
|
+
| `assert.propEqual(actual, expected)` | compares **own enumerable properties only** — ignores prototype and class identity, so a class instance can equal a plain object |
|
|
207
|
+
| `assert.notPropEqual(actual, expected)` | inverse of `propEqual` |
|
|
208
|
+
| `assert.propContains(actual, expected)` | **partial** match: every property in `expected` must match, extras in `actual` are ignored |
|
|
209
|
+
| `assert.notPropContains(actual, expected)` | inverse of `propContains` |
|
|
210
|
+
|
|
211
|
+
`propContains` is the one to reach for when asserting on an API response you only partly
|
|
212
|
+
care about:
|
|
176
213
|
|
|
177
214
|
```js
|
|
178
|
-
|
|
179
|
-
|
|
215
|
+
test('api response', async (assert) => {
|
|
216
|
+
const user = await fetchUser(1);
|
|
217
|
+
// passes regardless of createdAt, updatedAt, avatarUrl, …
|
|
218
|
+
assert.propContains(user, { id: 1, role: 'admin' });
|
|
219
|
+
});
|
|
220
|
+
```
|
|
180
221
|
|
|
181
|
-
|
|
182
|
-
|
|
222
|
+
### Errors
|
|
223
|
+
|
|
224
|
+
| | |
|
|
225
|
+
|---|---|
|
|
226
|
+
| `assert.throws(fn, expected?)` | `fn` must throw |
|
|
227
|
+
| `await assert.rejects(promise, expected?)` | `promise` must reject |
|
|
228
|
+
|
|
229
|
+
`expected` matches by constructor, by `RegExp` against the message, against another
|
|
230
|
+
`Error` instance (same constructor, name and message), or by a custom validator:
|
|
231
|
+
|
|
232
|
+
```js
|
|
233
|
+
test('error matching', (assert) => {
|
|
234
|
+
assert.throws(() => JSON.parse('{'), SyntaxError); // constructor
|
|
235
|
+
assert.throws(() => parse(''), /unexpected end/i); // message regex
|
|
236
|
+
assert.throws(() => parse(''), new SyntaxError('unexpected')); // Error instance
|
|
237
|
+
assert.throws(() => run(), (err) => err.code === 'EAGAIN'); // validator
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test('async errors', async (assert) => {
|
|
241
|
+
await assert.rejects(fetchMissing(), TypeError);
|
|
242
|
+
});
|
|
183
243
|
```
|
|
184
244
|
|
|
185
|
-
|
|
245
|
+
> Passing an `async` function to `assert.throws` is a mistake that silently passes in
|
|
246
|
+
> most frameworks — it returns a rejected promise rather than throwing. QUnitX detects
|
|
247
|
+
> it and tells you to use `assert.rejects`.
|
|
186
248
|
|
|
187
|
-
|
|
249
|
+
### Execution order
|
|
188
250
|
|
|
189
|
-
|
|
251
|
+
| | |
|
|
252
|
+
|---|---|
|
|
253
|
+
| `assert.step(label)` | records a step |
|
|
254
|
+
| `assert.verifySteps(labels)` | asserts the recorded steps equal `labels` in order, then clears them |
|
|
190
255
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
- **Hook ordering** — `before`/`beforeEach` run FIFO; `afterEach`/`after` run LIFO, exactly as in QUnit.
|
|
256
|
+
```js
|
|
257
|
+
test('callbacks fire in order', (assert) => {
|
|
258
|
+
emitter.on('start', () => assert.step('start'));
|
|
259
|
+
emitter.on('done', () => assert.step('done'));
|
|
196
260
|
|
|
197
|
-
|
|
261
|
+
emitter.run();
|
|
198
262
|
|
|
199
|
-
|
|
263
|
+
assert.verifySteps(['start', 'done']);
|
|
264
|
+
});
|
|
265
|
+
```
|
|
200
266
|
|
|
201
|
-
|
|
267
|
+
### Async control
|
|
202
268
|
|
|
203
|
-
|
|
269
|
+
| | |
|
|
270
|
+
|---|---|
|
|
271
|
+
| `assert.expect(n)` | the test fails unless exactly `n` assertions ran |
|
|
272
|
+
| `assert.timeout(ms)` | hard deadline for this test; calling again resets it |
|
|
273
|
+
| `assert.async()` | returns a `done` callback; the test waits until every `done` is called |
|
|
204
274
|
|
|
205
275
|
```js
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
hooks.afterEach(function (assert) { this.db.reset(); });
|
|
276
|
+
test('callback-style async', (assert) => {
|
|
277
|
+
const done = assert.async();
|
|
278
|
+
assert.expect(1);
|
|
210
279
|
|
|
211
|
-
|
|
212
|
-
assert.ok(
|
|
280
|
+
fs.readFile('data.json', (err, buf) => {
|
|
281
|
+
assert.ok(buf.length > 0);
|
|
282
|
+
done();
|
|
213
283
|
});
|
|
214
284
|
});
|
|
215
285
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
286
|
+
test('deadline', async (assert) => {
|
|
287
|
+
assert.timeout(500);
|
|
288
|
+
assert.ok(await fetchSomething());
|
|
289
|
+
});
|
|
290
|
+
```
|
|
219
291
|
|
|
220
|
-
|
|
221
|
-
|
|
292
|
+
`assert.expect()` is what turns "the callback never fired" from a silent pass into a
|
|
293
|
+
failure. For `async`/`await` tests just use an `async` callback — `assert.async()` is for
|
|
294
|
+
callback APIs.
|
|
222
295
|
|
|
223
|
-
|
|
224
|
-
|
|
296
|
+
### Custom assertions
|
|
297
|
+
|
|
298
|
+
`assert.pushResult` is the building block every other assertion is made of. The plain
|
|
299
|
+
version is a function that takes `assert`:
|
|
300
|
+
|
|
301
|
+
```js
|
|
302
|
+
function assertIsEven(assert, value) {
|
|
303
|
+
assert.pushResult({
|
|
304
|
+
result: value % 2 === 0,
|
|
305
|
+
actual: value,
|
|
306
|
+
expected: 'an even number',
|
|
307
|
+
message: `${value} is even`,
|
|
225
308
|
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
test('even numbers', (assert) => {
|
|
312
|
+
assertIsEven(assert, 4);
|
|
226
313
|
});
|
|
227
314
|
```
|
|
228
315
|
|
|
229
|
-
|
|
316
|
+
Better: put it **on `assert` itself**, so it reads like every built-in assertion and
|
|
317
|
+
needs no import at the call site. The assertion class is exported from `qunitx/assert`,
|
|
318
|
+
and adding to its prototype once — in a setup file, or at the top of your test helper —
|
|
319
|
+
makes it available in every test:
|
|
320
|
+
|
|
321
|
+
```js
|
|
322
|
+
// test-helpers.js
|
|
323
|
+
import Assert from 'qunitx/assert';
|
|
324
|
+
|
|
325
|
+
Assert.prototype.isEven = function (value, message) {
|
|
326
|
+
this.pushResult({
|
|
327
|
+
result: value % 2 === 0,
|
|
328
|
+
actual: value,
|
|
329
|
+
expected: 'an even number',
|
|
330
|
+
message: message ?? `${value} is even`,
|
|
331
|
+
});
|
|
332
|
+
};
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
```js
|
|
336
|
+
import './test-helpers.js';
|
|
337
|
+
|
|
338
|
+
test('even numbers', (assert) => {
|
|
339
|
+
assert.isEven(4); // passes
|
|
340
|
+
assert.isEven(3, 'three is even'); // fails, with your message
|
|
341
|
+
});
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
In TypeScript, declare the extended shape and assign through it:
|
|
345
|
+
|
|
346
|
+
```ts
|
|
347
|
+
import BaseAssert from 'qunitx/assert';
|
|
348
|
+
|
|
349
|
+
export interface Assert extends BaseAssert {
|
|
350
|
+
isEven(value: number, message?: string): void;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
(BaseAssert.prototype as Assert).isEven = function (value, message) {
|
|
354
|
+
this.pushResult({
|
|
355
|
+
result: value % 2 === 0,
|
|
356
|
+
actual: value,
|
|
357
|
+
expected: 'an even number',
|
|
358
|
+
message: message ?? `${value} is even`,
|
|
359
|
+
});
|
|
360
|
+
};
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
> `qunitx/assert` default-exports the class, so `declare module 'qunitx/assert'` cannot
|
|
364
|
+
> merge an interface into it — TypeScript can only augment *named* exports. Hence the
|
|
365
|
+
> explicit `extends` above. Test callbacks still receive the base type, so annotate the
|
|
366
|
+
> parameter (`(assert: Assert) => …`) where you call a custom assertion.
|
|
367
|
+
|
|
368
|
+
`qunitx/assert` touches no runner API, so it resolves the same way in Node, Deno and the
|
|
369
|
+
browser — a helper written against it is as portable as the tests that use it.
|
|
230
370
|
|
|
231
371
|
---
|
|
232
372
|
|
|
233
|
-
##
|
|
373
|
+
## Modules and hooks
|
|
234
374
|
|
|
235
|
-
|
|
375
|
+
`module()` groups tests and can nest. Hooks are declared on the `hooks` argument:
|
|
236
376
|
|
|
237
377
|
```js
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
378
|
+
module('Database', (hooks) => {
|
|
379
|
+
hooks.before(async function () { this.db = await connect(); }); // once, first
|
|
380
|
+
hooks.beforeEach(function () { this.tx = this.db.begin(); }); // before each test
|
|
381
|
+
hooks.afterEach(function () { this.tx.rollback(); }); // after each test
|
|
382
|
+
hooks.after(async function () { await this.db.close(); }); // once, last
|
|
383
|
+
|
|
384
|
+
test('insert', function (assert) {
|
|
385
|
+
assert.ok(this.tx.insert({ id: 1 }));
|
|
244
386
|
});
|
|
245
387
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
388
|
+
module('Nested', () => {
|
|
389
|
+
// parent hooks still apply, outermost first
|
|
390
|
+
test('inherits the transaction', function (assert) {
|
|
391
|
+
assert.ok(this.tx);
|
|
392
|
+
});
|
|
249
393
|
});
|
|
394
|
+
});
|
|
395
|
+
```
|
|
250
396
|
|
|
251
|
-
|
|
252
|
-
|
|
397
|
+
`before` / `beforeEach` run FIFO (outermost first); `afterEach` / `after` run LIFO
|
|
398
|
+
(innermost first) — exactly as in QUnit.
|
|
253
399
|
|
|
254
|
-
|
|
255
|
-
test('flaky on CI', { skip: true }, (assert) => { assert.ok(true); });
|
|
256
|
-
test('not ready', { todo: 'needs implementation' }, (assert) => { assert.ok(false); });
|
|
257
|
-
});
|
|
400
|
+
### `describe` / `it` — BDD aliases
|
|
258
401
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
402
|
+
If you prefer BDD naming, `describe` and `it` are exported as aliases of `module` and
|
|
403
|
+
`test`. They are the *same function objects*, so everything that works on one works on
|
|
404
|
+
the other — options, `.skip`, `.todo`, nesting, and mixing the two styles in one file:
|
|
405
|
+
|
|
406
|
+
```js
|
|
407
|
+
import { describe, it } from 'qunitx';
|
|
408
|
+
|
|
409
|
+
describe('Math utilities', (hooks) => {
|
|
410
|
+
hooks.beforeEach(function () {
|
|
411
|
+
this.numbers = [1, 2, 3];
|
|
412
|
+
});
|
|
263
413
|
|
|
264
|
-
|
|
265
|
-
|
|
414
|
+
it('adds', (assert) => {
|
|
415
|
+
assert.equal(2 + 2, 4);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it.skip('not yet implemented', (assert) => { /* never runs */ });
|
|
419
|
+
it('flaky on CI', { skip: true }, (assert) => {});
|
|
420
|
+
|
|
421
|
+
describe('Nested', () => {
|
|
422
|
+
it('inherits parent hooks', function (assert) {
|
|
423
|
+
assert.deepEqual(this.numbers, [1, 2, 3]);
|
|
424
|
+
});
|
|
425
|
+
});
|
|
266
426
|
});
|
|
267
427
|
```
|
|
268
428
|
|
|
269
|
-
|
|
429
|
+
### `context` — for arrow functions
|
|
430
|
+
|
|
431
|
+
QUnit exposes shared state as `this`, which arrow functions cannot see. QUnitX passes the
|
|
432
|
+
same object as `context` in the second argument of every hook and test, so both styles
|
|
433
|
+
work:
|
|
270
434
|
|
|
271
435
|
```js
|
|
272
|
-
|
|
436
|
+
// QUnit style — regular functions, `this`
|
|
437
|
+
module('Suite', function (hooks) {
|
|
438
|
+
hooks.before(function () { this.db = createDb(); });
|
|
439
|
+
test('query', function (assert) { assert.ok(this.db.query('SELECT 1')); });
|
|
440
|
+
});
|
|
273
441
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
442
|
+
// QUnitX style — arrow functions, `context`
|
|
443
|
+
module('Suite', (hooks, { context }) => {
|
|
444
|
+
hooks.before((assert, { context }) => { context.db = createDb(); });
|
|
445
|
+
test('query', (assert, { context }) => { assert.ok(context.db.query('SELECT 1')); });
|
|
277
446
|
});
|
|
278
447
|
```
|
|
279
448
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
449
|
+
Either way the state follows QUnit's prototype-chain model: each test gets a fresh object
|
|
450
|
+
inheriting from the module's, so `before()` writes are visible everywhere and a test's own
|
|
451
|
+
writes never leak into its siblings.
|
|
283
452
|
|
|
284
453
|
---
|
|
285
454
|
|
|
286
|
-
##
|
|
455
|
+
## Skip and todo
|
|
287
456
|
|
|
288
457
|
```js
|
|
289
|
-
test('
|
|
290
|
-
|
|
458
|
+
test.skip('not yet implemented', (assert) => { /* never runs */ });
|
|
459
|
+
test.todo('known broken', (assert) => { /* runs; failures do not fail the suite */ });
|
|
460
|
+
test.todo('not even started'); // no body — registers as pending
|
|
291
461
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
});
|
|
462
|
+
test('flaky on CI', { skip: true }, (assert) => {}); // conditional, via options
|
|
463
|
+
test('waiting on API', { todo: 'needs endpoint' }, (assert) => {});
|
|
295
464
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
465
|
+
module.skip('Legacy API', () => { /* whole module skipped */ });
|
|
466
|
+
module.todo('Future features', () => { /* whole module pending */ });
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
`skip` and `todo` are also exported top-level for QUnit-style usage:
|
|
470
|
+
|
|
471
|
+
```js
|
|
472
|
+
import { module, test, skip, todo } from 'qunitx';
|
|
302
473
|
```
|
|
303
474
|
|
|
304
|
-
|
|
305
|
-
|
|
475
|
+
The BDD aliases carry the same helpers — `describe.skip`, `describe.todo`, `it.skip`,
|
|
476
|
+
and `it.todo` are the exact same functions.
|
|
306
477
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
`assert.timeout()` (available since QUnit 2.16).
|
|
478
|
+
> **Runtime difference:** Node's `todo` runs the body and ignores its failures. Deno has
|
|
479
|
+
> no todo concept and maps both `skip` and `todo` to "ignored" — the body does not run.
|
|
310
480
|
|
|
311
481
|
---
|
|
312
482
|
|
|
313
|
-
##
|
|
483
|
+
## Runtime options
|
|
314
484
|
|
|
315
|
-
|
|
485
|
+
The optional second argument to `module()` and `test()` is passed straight through to the
|
|
486
|
+
underlying runner, so anything `node:test` or Deno BDD accepts works:
|
|
316
487
|
|
|
317
488
|
```js
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
// Enable parallel execution for this module (Node/Deno only)
|
|
321
|
-
module('Parallel suite', { concurrency: true }, (hooks) => {
|
|
322
|
-
test('first', (assert) => { assert.ok(true); });
|
|
323
|
-
test('second', (assert) => { assert.ok(true); });
|
|
324
|
-
});
|
|
489
|
+
// Tests run sequentially by default, matching QUnit's browser behaviour.
|
|
490
|
+
module('Parallel suite', { concurrency: true }, () => { /* … */ });
|
|
325
491
|
|
|
326
|
-
// Deno-specific
|
|
327
|
-
module('
|
|
492
|
+
// Deno-specific options
|
|
493
|
+
module('File access', { permissions: { read: true }, sanitizeExit: false }, () => {
|
|
328
494
|
test('reads a file', async (assert) => {
|
|
329
|
-
|
|
330
|
-
assert.ok(text.length > 0);
|
|
495
|
+
assert.ok((await Deno.readTextFile('./README.md')).length > 0);
|
|
331
496
|
});
|
|
332
497
|
});
|
|
333
498
|
```
|
|
334
499
|
|
|
500
|
+
Options a runtime does not recognise are ignored, which is what keeps one file portable
|
|
501
|
+
across all three.
|
|
502
|
+
|
|
335
503
|
---
|
|
336
504
|
|
|
337
|
-
##
|
|
505
|
+
## Migrating from QUnit
|
|
338
506
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
507
|
+
One line:
|
|
508
|
+
|
|
509
|
+
```diff
|
|
510
|
+
- import { module, test } from 'qunit';
|
|
511
|
+
+ import { module, test } from 'qunitx';
|
|
512
|
+
```
|
|
344
513
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
familiar browser UI with zero extra layers.
|
|
514
|
+
Coming from a BDD runner instead? Import `describe` / `it` — they are aliases of
|
|
515
|
+
`module` / `test`, see [`describe` / `it`](#describe--it--bdd-aliases).
|
|
348
516
|
|
|
349
517
|
---
|
|
350
518
|
|
|
351
|
-
##
|
|
519
|
+
## QUnit compatibility
|
|
352
520
|
|
|
353
|
-
qunitx
|
|
354
|
-
Node.js and Deno's built-in v8 coverage instruments it naturally — no special configuration needed.
|
|
521
|
+
qunitx follows the same test-environment model as QUnit:
|
|
355
522
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
523
|
+
- **Fresh context per test** — each test gets its own `this` object. Writes in one test never bleed into a sibling.
|
|
524
|
+
- **Prototype-chain inheritance** — a parent module's `before()` hook sets properties on the module context. Each test inherits those properties, so reads work naturally (`this.x`) while writes stay local to the test.
|
|
525
|
+
- **`before()` assertions** — attributed to the first test in the module (matching QUnit's attribution model).
|
|
526
|
+
- **`after()` assertions** — attributed to the last test in the module.
|
|
527
|
+
- **Hook ordering** — `before`/`beforeEach` run FIFO; `afterEach`/`after` run LIFO, exactly as in QUnit.
|
|
528
|
+
|
|
529
|
+
> **Known difference:** In QUnit's browser runner, `before()` hook assertions are attributed to the first test in the *entire subtree* (including nested modules). In the Node/Deno adapters, they are attributed to the first *direct* test of the module. In the common case where direct tests appear before nested modules, the behavior is identical.
|
|
359
530
|
|
|
360
|
-
|
|
531
|
+
---
|
|
532
|
+
|
|
533
|
+
## How it works
|
|
534
|
+
|
|
535
|
+
| Runtime | Adapter |
|
|
536
|
+
|---|---|
|
|
537
|
+
| Node.js | wraps `node:test`'s `describe` / `it` with the QUnit lifecycle |
|
|
538
|
+
| Deno | wraps Deno's BDD helpers with the same lifecycle |
|
|
539
|
+
| Browser | a thin re-export of QUnit itself |
|
|
540
|
+
|
|
541
|
+
Because the browser path *is* QUnit, you get the whole thing there: plugins, custom
|
|
542
|
+
reporters, the event API (`QUnit.on`, `QUnit.done`, …) and the familiar UI, with no extra
|
|
543
|
+
layer in between.
|
|
544
|
+
|
|
545
|
+
QUnitX loads as an ordinary dependency instead of wrapping the runtime process, so v8
|
|
546
|
+
coverage instruments it with no special configuration:
|
|
547
|
+
|
|
548
|
+
```sh
|
|
549
|
+
npx c8 node --test test/ # Node
|
|
361
550
|
npx c8 --reporter=html node --test test/ && open coverage/index.html
|
|
551
|
+
deno test --coverage=cov test/ && deno coverage cov # Deno
|
|
362
552
|
```
|
|
363
553
|
|
|
364
|
-
Browser
|
|
365
|
-
Native
|
|
366
|
-
and
|
|
554
|
+
Browser coverage is the one gap — qunitx-cli bundles test files with esbuild, which
|
|
555
|
+
loses the mapping. Native import-map support in headless Chrome would remove the bundling
|
|
556
|
+
step and with it the limitation.
|
|
367
557
|
|
|
368
558
|
---
|
|
369
559
|
|
|
370
560
|
## Links
|
|
371
561
|
|
|
372
|
-
- [QUnit API reference](https://api.qunitjs.com)
|
|
373
|
-
- [qunitx-cli](https://github.com/izelnakri/qunitx-cli) — browser runner
|
|
374
|
-
- [
|
|
375
|
-
- [Deno testing
|
|
562
|
+
- [QUnit API reference](https://api.qunitjs.com) — every assertion, in depth
|
|
563
|
+
- [qunitx-cli](https://github.com/izelnakri/qunitx-cli) — the browser runner and all of its options
|
|
564
|
+
- [`@izelnakri/qunitx` on JSR](https://jsr.io/@izelnakri/qunitx) · [`qunitx` on npm](https://www.npmjs.com/package/qunitx)
|
|
565
|
+
- [Node.js test runner](https://nodejs.org/api/test.html) · [Deno testing](https://docs.deno.com/runtime/fundamentals/testing/)
|