qunitx 0.12.0 → 0.12.4
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 +180 -101
- package/package.json +13 -7
- package/scripts/check-coverage.js +15 -0
- package/shims/deno/index.js +1 -1
- package/shims/deno/module.js +36 -9
- package/shims/deno/test.js +39 -10
- package/shims/node/module.js +8 -8
- package/shims/node/test.js +8 -8
- package/shims/shared/assert.js +15 -15
- package/shims/shared/index.js +16 -22
- package/shims/shared/module-context.js +1 -1
- package/Makefile +0 -26
package/README.md
CHANGED
|
@@ -1,156 +1,235 @@
|
|
|
1
|
-
|
|
2
|
-
[](https://badge.fury.io/js/qunitx)
|
|
3
|
-
[](https://github.com/izelnakri/qunitx/issues)
|
|
1
|
+
<div align="center">
|
|
4
2
|
|
|
5
3
|
# QUnitX
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
[](https://github.com/izelnakri/qunitx/actions/workflows/ci.yml)
|
|
6
|
+
[](https://codecov.io/gh/izelnakri/qunitx)
|
|
7
|
+
[](https://www.npmjs.com/package/qunitx)
|
|
8
|
+
[](https://www.npmjs.com/package/qunitx)
|
|
9
|
+
[](LICENSE)
|
|
10
|
+
[](https://github.com/izelnakri/qunitx/issues)
|
|
11
|
+
[](https://github.com/sponsors/izelnakri)
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
testing API in the JavaScript ecosystem. Run the same test file in node.js, deno or in the browser***
|
|
13
|
+
</div>
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
the default test runner of node.js or deno, or with a browser runner of your
|
|
14
|
-
choice!
|
|
15
|
+
**The oldest, most battle-tested JavaScript test API — now universal.**
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
Run the **same test file** in Node.js, Deno, and the browser without changes.
|
|
18
|
+
Zero dependencies. No config needed for Node. TypeScript works out of the box.
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
[QUnit](https://github.com/qunitjs/qunit) and share the web links with your
|
|
20
|
-
colleagues thanks to the test filters through query params feature of QUnit:
|
|
20
|
+
---
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
## Why QUnit?
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
QUnit was created in 2008 by the jQuery team. While newer frameworks come and go,
|
|
25
|
+
QUnit has quietly accumulated 16+ years of real-world edge-case handling that younger
|
|
26
|
+
tools are still catching up to. Its assertion API is the most mature in the JavaScript
|
|
27
|
+
ecosystem:
|
|
25
28
|
|
|
26
|
-
|
|
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
|
+
- **Hooks** — `before`, `beforeEach`, `afterEach`, `after` with correct FIFO/LIFO ordering,
|
|
37
|
+
properly scoped across nested modules
|
|
38
|
+
- **Shareable browser URLs** — the QUnit browser UI filters tests via query params, so you can
|
|
39
|
+
share `https://yourapp.test/?moduleId=abc123` with a colleague and they see exactly the same view
|
|
27
40
|
|
|
28
|
-
|
|
41
|
+
QUnitX wraps this API to work with **Node.js's built-in `node:test` runner** and
|
|
42
|
+
**Deno's native test runner** — no Jest, Vitest, or other framework needed.
|
|
29
43
|
|
|
30
|
-
|
|
31
|
-
|
|
44
|
+
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
|
|
45
|
+
runtime, faster than node.js and deno default assertions in most cases. Therefore I consider myself very objective
|
|
46
|
+
when I say QUnit(X) is the best JS/TS testing tool out there.
|
|
32
47
|
|
|
33
|
-
|
|
48
|
+
---
|
|
34
49
|
|
|
35
|
-
|
|
36
|
-
import { module, test } from 'qunit';
|
|
50
|
+
## Demo
|
|
37
51
|
|
|
38
|
-
|
|
39
|
-
|
|
52
|
+
> Left window: `node --test` and `deno test` running the same file.
|
|
53
|
+
> Right window: QUnit browser UI with filterable, shareable test results.
|
|
54
|
+
|
|
55
|
+
<!-- Demo GIF: see docs/demo.tape (VHS script) for terminal portion.
|
|
56
|
+
For the combined terminal + browser recording, see "Recording the demo" below. -->
|
|
57
|
+

|
|
58
|
+
|
|
59
|
+
Live browser UI example (click to see filterable QUnit test suite):
|
|
60
|
+
|
|
61
|
+
[objectmodel.js.org/test/?moduleId=6e15ed5f](https://objectmodel.js.org/test/?moduleId=6e15ed5f&moduleId=950ec9c5)
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Installation
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
npm install qunitx --save-dev
|
|
40
69
|
```
|
|
41
70
|
|
|
42
|
-
|
|
71
|
+
Requires **Node.js >= 22** (LTS) or **Deno >= 2**.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Quick start
|
|
43
76
|
|
|
44
77
|
```js
|
|
45
|
-
//
|
|
78
|
+
// math-test.js (works in Node, Deno, and browser unchanged)
|
|
46
79
|
import { module, test } from 'qunitx';
|
|
47
|
-
import $ from 'jquery';
|
|
48
80
|
|
|
49
|
-
module('
|
|
50
|
-
|
|
51
|
-
assert.
|
|
81
|
+
module('Math utilities', (hooks) => {
|
|
82
|
+
hooks.before((assert) => {
|
|
83
|
+
assert.step('setup complete');
|
|
52
84
|
});
|
|
53
85
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
86
|
+
test('addition', (assert) => {
|
|
87
|
+
assert.equal(2 + 2, 4);
|
|
88
|
+
assert.notEqual(2 + 2, 5);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('deepEqual', (assert) => {
|
|
92
|
+
assert.deepEqual({ a: 1, b: [2, 3] }, { a: 1, b: [2, 3] });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
module('Async', () => {
|
|
96
|
+
test('resolves correctly', async (assert) => {
|
|
97
|
+
const result = await Promise.resolve(42);
|
|
98
|
+
assert.strictEqual(result, 42);
|
|
60
99
|
});
|
|
61
100
|
});
|
|
62
101
|
});
|
|
63
102
|
```
|
|
64
103
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
104
|
+
### Node.js
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
# No extra dependencies — uses the Node built-in test runner
|
|
108
|
+
node --test math-test.js
|
|
109
|
+
|
|
110
|
+
# Watch mode (re-runs on save)
|
|
111
|
+
node --test --watch math-test.js
|
|
112
|
+
|
|
113
|
+
# Glob pattern
|
|
114
|
+
node --test --watch 'test/**/*.js'
|
|
115
|
+
|
|
116
|
+
# TypeScript (tsconfig.json with moduleResolution: NodeNext required)
|
|
117
|
+
node --import=tsx/esm --test math-test.ts
|
|
118
|
+
|
|
119
|
+
# Code coverage
|
|
120
|
+
npx c8 node --test math-test.js
|
|
121
|
+
```
|
|
68
122
|
|
|
69
|
-
|
|
70
|
-
$ node --loader=ts-node/esm/transpile-only --test some-test.ts
|
|
123
|
+
### Deno
|
|
71
124
|
|
|
72
|
-
|
|
73
|
-
|
|
125
|
+
```sh
|
|
126
|
+
# One-time: create a deno.json import map
|
|
127
|
+
echo '{"imports": {"qunitx": "https://esm.sh/qunitx/shims/deno/index.js"}}' > deno.json
|
|
74
128
|
|
|
75
|
-
#
|
|
76
|
-
|
|
129
|
+
# Run
|
|
130
|
+
deno test math-test.js
|
|
77
131
|
|
|
78
|
-
#
|
|
79
|
-
|
|
132
|
+
# With explicit permissions
|
|
133
|
+
deno test --allow-read --allow-env math-test.js
|
|
80
134
|
```
|
|
81
135
|
|
|
82
|
-
###
|
|
136
|
+
### Browser
|
|
83
137
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
You can use [QUnitX CLI](https://github.com/izelnakri/qunitx-cli) to get your
|
|
87
|
-
browser tests to stdout/CI or use the watch mode during the development.
|
|
138
|
+
Use [qunitx-cli](https://github.com/izelnakri/qunitx-cli) to get browser test output
|
|
139
|
+
in your terminal / CI, or to open the live QUnit UI during development:
|
|
88
140
|
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
$ npm install -g qunitx-cli
|
|
92
|
-
$ qunitx
|
|
93
|
-
$ qunitx some-test.js
|
|
141
|
+
```sh
|
|
142
|
+
npm install -g qunitx-cli
|
|
94
143
|
|
|
95
|
-
#
|
|
96
|
-
|
|
144
|
+
# Headless (CI-friendly — outputs TAP to stdout)
|
|
145
|
+
qunitx math-test.js
|
|
97
146
|
|
|
147
|
+
# Open QUnit browser UI alongside terminal output
|
|
148
|
+
qunitx math-test.js --debug
|
|
98
149
|
```
|
|
99
150
|
|
|
100
|
-
|
|
151
|
+
The browser UI lets you:
|
|
152
|
+
- Filter by module or test name (filter state is preserved in the URL)
|
|
153
|
+
- Share a link that reproduces the exact filtered view with a colleague
|
|
154
|
+
- Re-run individual tests by clicking them
|
|
155
|
+
- See full assertion diffs inline
|
|
101
156
|
|
|
102
|
-
|
|
103
|
-
- `QUnit.module(testName, optionsOrHandler?, handler?)`
|
|
104
|
-
- `QUnit.test(testName, optionsOrHandler?, handler?)`
|
|
157
|
+
---
|
|
105
158
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
159
|
+
## Migrating from QUnit
|
|
160
|
+
|
|
161
|
+
One import line is all that changes:
|
|
109
162
|
|
|
110
163
|
```js
|
|
111
|
-
//
|
|
164
|
+
// Before:
|
|
165
|
+
import { module, test } from 'qunit';
|
|
166
|
+
|
|
167
|
+
// After:
|
|
112
168
|
import { module, test } from 'qunitx';
|
|
113
|
-
|
|
169
|
+
```
|
|
114
170
|
|
|
115
|
-
|
|
116
|
-
test('it works', { concurrency: false }, function (assert) {
|
|
117
|
-
assert.equal(true, true);
|
|
118
|
-
});
|
|
171
|
+
---
|
|
119
172
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
173
|
+
## Concurrency options
|
|
174
|
+
|
|
175
|
+
`module()` and `test()` accept an optional options object forwarded directly to the underlying
|
|
176
|
+
Node / Deno test runner:
|
|
177
|
+
|
|
178
|
+
```js
|
|
179
|
+
import { module, test } from 'qunitx';
|
|
180
|
+
|
|
181
|
+
// Run tests in this module serially
|
|
182
|
+
module('Serial suite', { concurrency: false }, (hooks) => {
|
|
183
|
+
test('first', (assert) => { assert.ok(true); });
|
|
184
|
+
test('second', (assert) => { assert.ok(true); });
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Deno-specific: permissions, sanitizeExit, etc.
|
|
188
|
+
module('Deno file access', { permissions: { read: true }, sanitizeExit: false }, (hooks) => {
|
|
189
|
+
test('reads a file', async (assert) => {
|
|
190
|
+
const text = await Deno.readTextFile('./README.md');
|
|
191
|
+
assert.ok(text.length > 0);
|
|
127
192
|
});
|
|
128
193
|
});
|
|
129
194
|
```
|
|
130
195
|
|
|
131
|
-
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## How it works
|
|
199
|
+
|
|
200
|
+
| Runtime | Adapter |
|
|
201
|
+
|---------|---------|
|
|
202
|
+
| Node.js | Wraps `node:test` `describe` / `it` with QUnit lifecycle |
|
|
203
|
+
| Deno | Wraps Deno BDD helpers with the same QUnit lifecycle |
|
|
204
|
+
| Browser | Thin re-export of QUnit's native browser API |
|
|
205
|
+
|
|
206
|
+
The browser path is literally QUnit itself, so you get full QUnit compatibility:
|
|
207
|
+
plugins, custom reporters, the event API (`QUnit.on`, `QUnit.done`, etc.), and the
|
|
208
|
+
familiar browser UI with zero extra layers.
|
|
209
|
+
|
|
210
|
+
---
|
|
132
211
|
|
|
133
|
-
|
|
134
|
-
you can use any code coverage tool you like. When running the tests in
|
|
135
|
-
`qunit`(the browser mode) code coverage support is limited.
|
|
212
|
+
## Code coverage
|
|
136
213
|
|
|
137
|
-
|
|
138
|
-
|
|
214
|
+
Probably c8 isn't even needed since qunitx runs as a dependency(rather than runtime) on node.js and deno.
|
|
215
|
+
|
|
216
|
+
```sh
|
|
217
|
+
# Node (any c8-compatible reporter)
|
|
218
|
+
npx c8 node --test test/
|
|
219
|
+
|
|
220
|
+
# View HTML report
|
|
221
|
+
npx c8 --reporter=html node --test test/ && open coverage/index.html
|
|
139
222
|
```
|
|
140
223
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
Esbuild plugin interface is an ongoing development, we might be able to figure
|
|
155
|
-
out a way to generate this instrumentation with esbuild in the future, which
|
|
156
|
-
could allow code coverage for --browser mode.
|
|
224
|
+
Browser-mode coverage is limited because qunitx-cli bundles test files with esbuild.
|
|
225
|
+
Native ES import maps support in Puppeteer/Chrome would eliminate the bundling step
|
|
226
|
+
and unlock v8 instrumentation for browser coverage.
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Links
|
|
231
|
+
|
|
232
|
+
- [QUnit API reference](https://api.qunitjs.com)
|
|
233
|
+
- [qunitx-cli](https://github.com/izelnakri/qunitx-cli) — browser runner / CI reporter
|
|
234
|
+
- [Node.js test runner docs](https://nodejs.org/api/test.html)
|
|
235
|
+
- [Deno testing docs](https://docs.deno.com/runtime/fundamentals/testing/)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qunitx",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.12.
|
|
4
|
+
"version": "0.12.4",
|
|
5
5
|
"description": "A universal test framework for testing any js file on node.js, browser or deno with QUnit API",
|
|
6
6
|
"author": "Izel Nakri",
|
|
7
7
|
"license": "MIT",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"browser"
|
|
45
45
|
],
|
|
46
46
|
"engines": {
|
|
47
|
-
"node": ">=
|
|
47
|
+
"node": ">=22.0.0"
|
|
48
48
|
},
|
|
49
49
|
"imports": {
|
|
50
50
|
"qunitx": {
|
|
@@ -60,11 +60,13 @@
|
|
|
60
60
|
},
|
|
61
61
|
"repository": {
|
|
62
62
|
"type": "git",
|
|
63
|
-
"url": "https://github.com/izelnakri/qunitx.git"
|
|
63
|
+
"url": "git+https://github.com/izelnakri/qunitx.git"
|
|
64
64
|
},
|
|
65
65
|
"scripts": {
|
|
66
|
-
"
|
|
67
|
-
"
|
|
66
|
+
"format": "prettier --check \"test/**/*.js\" \"*.js\" \"package.json\"",
|
|
67
|
+
"format:fix": "prettier --write \"test/**/*.js\" \"*.js\" \"package.json\"",
|
|
68
|
+
"lint": "deno lint shims/",
|
|
69
|
+
"lint:docs": "deno doc --lint shims/deno/module.js shims/deno/test.js",
|
|
68
70
|
"build": "node build.js",
|
|
69
71
|
"run:all": "npm run run:node && npm run run:deno",
|
|
70
72
|
"run:node": "node --test test/helpers/passing-tests.js && node --test test/helpers/failing-tests.js",
|
|
@@ -75,9 +77,13 @@
|
|
|
75
77
|
"prepack": "npm run build",
|
|
76
78
|
"test": "npm run test:browser && npm run test:node && npm run test:deno",
|
|
77
79
|
"test:dev": "npm run test | tee test-output.log",
|
|
78
|
-
"test:browser": "
|
|
80
|
+
"test:browser": "qunitx test/index.js --debug",
|
|
79
81
|
"test:deno": "deno test --allow-read --allow-env --allow-run test/index.js",
|
|
80
|
-
"test:
|
|
82
|
+
"test:doctests": "deno test --doc --allow-env --allow-read shims/deno/module.js shims/deno/test.js",
|
|
83
|
+
"test:node": "node --test test/index.js",
|
|
84
|
+
"coverage": "deno test --coverage=tmp/coverage --allow-read --allow-env --allow-run test/index.js && deno coverage --lcov --output=tmp/coverage/lcov.info --include='shims/' tmp/coverage && node scripts/check-coverage.js",
|
|
85
|
+
"coverage:report": "npm run coverage && deno coverage --html --include='shims/' tmp/coverage",
|
|
86
|
+
"docs": "deno doc --html --name=\"QUnitX\" --output=docs/src shims/deno/index.js"
|
|
81
87
|
},
|
|
82
88
|
"devDependencies": {
|
|
83
89
|
"prettier": "^3.8.1",
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
const THRESHOLD = 85;
|
|
4
|
+
const lcov = await readFile('tmp/coverage/lcov.info', 'utf8');
|
|
5
|
+
|
|
6
|
+
const lh = [...lcov.matchAll(/^LH:(\d+)/gm)].reduce((s, m) => s + parseInt(m[1]), 0);
|
|
7
|
+
const lf = [...lcov.matchAll(/^LF:(\d+)/gm)].reduce((s, m) => s + parseInt(m[1]), 0);
|
|
8
|
+
const pct = lf > 0 ? (lh / lf) * 100 : 0;
|
|
9
|
+
|
|
10
|
+
console.log(`Coverage: ${pct.toFixed(1)}% (${lh}/${lf} lines)`);
|
|
11
|
+
|
|
12
|
+
if (pct < THRESHOLD) {
|
|
13
|
+
console.error(`Error: coverage ${pct.toFixed(1)}% is below the ${THRESHOLD}% threshold.`);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
package/shims/deno/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AssertionError as DenoAssertionError } from "
|
|
1
|
+
import { AssertionError as DenoAssertionError } from "jsr:@std/assert";
|
|
2
2
|
import '../../vendor/qunit.js';
|
|
3
3
|
import Assert from '../shared/assert.js';
|
|
4
4
|
import ModuleContext from '../shared/module-context.js';
|
package/shims/deno/module.js
CHANGED
|
@@ -1,18 +1,45 @@
|
|
|
1
|
-
import { describe, beforeAll, afterAll } from "
|
|
1
|
+
import { describe, beforeAll, afterAll } from "jsr:@std/testing/bdd";
|
|
2
2
|
import ModuleContext from '../shared/module-context.js';
|
|
3
3
|
|
|
4
4
|
// NOTE: node.js beforeEach & afterEach is buggy because the TestContext it has is NOT correct reference when called, it gets the last context
|
|
5
5
|
// NOTE: QUnit expect() logic is buggy in nested modules
|
|
6
6
|
// NOTE: after gets the last direct children test of the module, not last defined context of a module(last defined context is a module)
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Defines a test module (suite) for Deno's BDD test runner.
|
|
10
|
+
*
|
|
11
|
+
* Wraps `describe()` from `@std/testing/bdd` and sets up the QUnit lifecycle
|
|
12
|
+
* (before/beforeEach/afterEach/after hooks, assertion counting, steps tracking).
|
|
13
|
+
*
|
|
14
|
+
* @param {string} moduleName - Name of the test suite
|
|
15
|
+
* @param {object} [runtimeOptions] - Optional Deno BDD options forwarded to `describe()`
|
|
16
|
+
* (e.g. `{ concurrency: false }`, `{ permissions: { read: true } }`)
|
|
17
|
+
* @param {function} moduleContent - Callback that defines tests and hooks via `hooks.before`,
|
|
18
|
+
* `hooks.beforeEach`, `hooks.afterEach`, `hooks.after`
|
|
19
|
+
* @returns {void}
|
|
20
|
+
* @example
|
|
21
|
+
* ```js ignore
|
|
22
|
+
* import { module, test } from "qunitx";
|
|
23
|
+
*
|
|
24
|
+
* module("Math", (hooks) => {
|
|
25
|
+
* hooks.before((assert) => {
|
|
26
|
+
* assert.step("before hook ran");
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* test("addition", (assert) => {
|
|
30
|
+
* assert.equal(2 + 2, 4);
|
|
31
|
+
* });
|
|
32
|
+
* });
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
8
35
|
export default function module(moduleName, runtimeOptions, moduleContent) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
36
|
+
const targetRuntimeOptions = moduleContent ? runtimeOptions : {};
|
|
37
|
+
const targetModuleContent = moduleContent ? moduleContent : runtimeOptions;
|
|
38
|
+
const moduleContext = new ModuleContext(moduleName);
|
|
12
39
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
40
|
+
describe(moduleName, { concurrency: true, ...targetRuntimeOptions }, function () {
|
|
41
|
+
const beforeHooks = [];
|
|
42
|
+
const afterHooks = [];
|
|
16
43
|
|
|
17
44
|
beforeAll(async function () {
|
|
18
45
|
Object.assign(moduleContext.context, moduleContext.moduleChain.reduce((result, module) => {
|
|
@@ -24,7 +51,7 @@ export default function module(moduleName, runtimeOptions, moduleContent) {
|
|
|
24
51
|
});
|
|
25
52
|
}, { steps: [], expectedAssertionCount: undefined }));
|
|
26
53
|
|
|
27
|
-
for (
|
|
54
|
+
for (const hook of beforeHooks) {
|
|
28
55
|
await hook.call(moduleContext.context, moduleContext.assert);
|
|
29
56
|
}
|
|
30
57
|
|
|
@@ -41,7 +68,7 @@ export default function module(moduleName, runtimeOptions, moduleContent) {
|
|
|
41
68
|
await assert.waitForAsyncOps();
|
|
42
69
|
}
|
|
43
70
|
|
|
44
|
-
|
|
71
|
+
const targetContext = moduleContext.tests[moduleContext.tests.length - 1];
|
|
45
72
|
for (let j = afterHooks.length - 1; j >= 0; j--) {
|
|
46
73
|
await afterHooks[j].call(targetContext, targetContext.assert);
|
|
47
74
|
}
|
package/shims/deno/test.js
CHANGED
|
@@ -1,30 +1,59 @@
|
|
|
1
|
-
import { it } from "
|
|
1
|
+
import { it } from "jsr:@std/testing/bdd";
|
|
2
2
|
import TestContext from '../shared/test-context.js';
|
|
3
3
|
import ModuleContext from '../shared/module-context.js';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Defines an individual test within a module for Deno's BDD test runner.
|
|
7
|
+
*
|
|
8
|
+
* Wraps `it()` from `@std/testing/bdd` and handles the full QUnit lifecycle:
|
|
9
|
+
* beforeEach/afterEach hooks, async assertion waiting, and step verification.
|
|
10
|
+
*
|
|
11
|
+
* Must be called inside a `module()` callback.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} testName - Name of the test
|
|
14
|
+
* @param {object} [runtimeOptions] - Optional Deno BDD options forwarded to `it()`
|
|
15
|
+
* (e.g. `{ concurrency: false }`, `{ sanitizeExit: false }`)
|
|
16
|
+
* @param {function} testContent - Test callback receiving `(assert, { testName, options })`
|
|
17
|
+
* @returns {void}
|
|
18
|
+
* @example
|
|
19
|
+
* ```js ignore
|
|
20
|
+
* import { module, test } from "qunitx";
|
|
21
|
+
*
|
|
22
|
+
* module("Math", () => {
|
|
23
|
+
* test("addition", (assert) => {
|
|
24
|
+
* assert.equal(1 + 1, 2);
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* test("async resolves correctly", async (assert) => {
|
|
28
|
+
* const result = await Promise.resolve(42);
|
|
29
|
+
* assert.strictEqual(result, 42);
|
|
30
|
+
* });
|
|
31
|
+
* });
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
5
34
|
export default function test(testName, runtimeOptions, testContent) {
|
|
6
|
-
|
|
35
|
+
const moduleContext = ModuleContext.lastModule;
|
|
7
36
|
if (!moduleContext) {
|
|
8
37
|
throw new Error(`Test '${testName}' called outside of module context.`);
|
|
9
38
|
}
|
|
10
39
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
40
|
+
const targetRuntimeOptions = testContent ? runtimeOptions : {};
|
|
41
|
+
const targetTestContent = testContent ? testContent : runtimeOptions;
|
|
42
|
+
const context = new TestContext(testName, moduleContext);
|
|
14
43
|
|
|
15
|
-
|
|
16
|
-
for (
|
|
17
|
-
for (
|
|
44
|
+
it(testName, { concurrency: true, ...targetRuntimeOptions }, async function () {
|
|
45
|
+
for (const module of context.module.moduleChain) {
|
|
46
|
+
for (const hook of module.beforeEachHooks) {
|
|
18
47
|
await hook.call(context, context.assert);
|
|
19
48
|
}
|
|
20
49
|
}
|
|
21
50
|
|
|
22
|
-
|
|
51
|
+
const result = await targetTestContent.call(context, context.assert, { testName, options: runtimeOptions });
|
|
23
52
|
|
|
24
53
|
await context.assert.waitForAsyncOps();
|
|
25
54
|
|
|
26
55
|
for (let i = context.module.moduleChain.length - 1; i >= 0; i--) {
|
|
27
|
-
|
|
56
|
+
const module = context.module.moduleChain[i];
|
|
28
57
|
for (let j = module.afterEachHooks.length - 1; j >= 0; j--) {
|
|
29
58
|
await module.afterEachHooks[j].call(context, context.assert);
|
|
30
59
|
}
|
package/shims/node/module.js
CHANGED
|
@@ -6,13 +6,13 @@ import ModuleContext from '../shared/module-context.js';
|
|
|
6
6
|
// NOTE: after gets the last direct children test of the module, not last defined context of a module(last defined context is a module)
|
|
7
7
|
|
|
8
8
|
export default function module(moduleName, runtimeOptions, moduleContent) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
const targetRuntimeOptions = moduleContent ? runtimeOptions : {};
|
|
10
|
+
const targetModuleContent = moduleContent ? moduleContent : runtimeOptions;
|
|
11
|
+
const moduleContext = new ModuleContext(moduleName);
|
|
12
12
|
|
|
13
|
-
return describe(moduleName, { concurrency: true, ...targetRuntimeOptions },
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
return describe(moduleName, { concurrency: true, ...targetRuntimeOptions }, function () {
|
|
14
|
+
const beforeHooks = [];
|
|
15
|
+
const afterHooks = [];
|
|
16
16
|
|
|
17
17
|
beforeAll(async function () {
|
|
18
18
|
Object.assign(moduleContext.context, moduleContext.moduleChain.reduce((result, module) => {
|
|
@@ -24,7 +24,7 @@ export default function module(moduleName, runtimeOptions, moduleContent) {
|
|
|
24
24
|
});
|
|
25
25
|
}, { steps: [], expectedAssertionCount: undefined }));
|
|
26
26
|
|
|
27
|
-
for (
|
|
27
|
+
for (const hook of beforeHooks) {
|
|
28
28
|
await hook.call(moduleContext.context, moduleContext.assert);
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -41,7 +41,7 @@ export default function module(moduleName, runtimeOptions, moduleContent) {
|
|
|
41
41
|
await assert.waitForAsyncOps();
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
const targetContext = moduleContext.tests[moduleContext.tests.length - 1];
|
|
45
45
|
for (let j = afterHooks.length - 1; j >= 0; j--) {
|
|
46
46
|
await afterHooks[j].call(targetContext, targetContext.assert);
|
|
47
47
|
}
|
package/shims/node/test.js
CHANGED
|
@@ -3,28 +3,28 @@ import TestContext from '../shared/test-context.js';
|
|
|
3
3
|
import ModuleContext from '../shared/module-context.js';
|
|
4
4
|
|
|
5
5
|
export default function test(testName, runtimeOptions, testContent) {
|
|
6
|
-
|
|
6
|
+
const moduleContext = ModuleContext.lastModule;
|
|
7
7
|
if (!moduleContext) {
|
|
8
8
|
throw new Error(`Test '${testName}' called outside of module context.`);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
const targetRuntimeOptions = testContent ? runtimeOptions : {};
|
|
12
|
+
const targetTestContent = testContent ? testContent : runtimeOptions;
|
|
13
|
+
const context = new TestContext(testName, moduleContext);
|
|
14
14
|
|
|
15
15
|
return it(testName, { concurrency: true, ...targetRuntimeOptions }, async function () {
|
|
16
|
-
for (
|
|
17
|
-
for (
|
|
16
|
+
for (const module of context.module.moduleChain) {
|
|
17
|
+
for (const hook of module.beforeEachHooks) {
|
|
18
18
|
await hook.call(context, context.assert);
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
const result = await targetTestContent.call(context, context.assert, { testName, options: runtimeOptions });
|
|
23
23
|
|
|
24
24
|
await context.assert.waitForAsyncOps();
|
|
25
25
|
|
|
26
26
|
for (let i = context.module.moduleChain.length - 1; i >= 0; i--) {
|
|
27
|
-
|
|
27
|
+
const module = context.module.moduleChain[i];
|
|
28
28
|
for (let j = module.afterEachHooks.length - 1; j >= 0; j--) {
|
|
29
29
|
await module.afterEachHooks[j].call(context, context.assert);
|
|
30
30
|
}
|
package/shims/shared/assert.js
CHANGED
|
@@ -55,13 +55,13 @@ export default class Assert {
|
|
|
55
55
|
}
|
|
56
56
|
async() {
|
|
57
57
|
let resolveFn;
|
|
58
|
-
|
|
58
|
+
const done = new Promise(resolve => { resolveFn = resolve; });
|
|
59
59
|
|
|
60
60
|
this.test.asyncOps.push(done);
|
|
61
61
|
|
|
62
62
|
return () => { resolveFn(); };
|
|
63
63
|
}
|
|
64
|
-
|
|
64
|
+
waitForAsyncOps() {
|
|
65
65
|
return Promise.all(this.test.asyncOps);
|
|
66
66
|
}
|
|
67
67
|
pushResult(resultInfo = {}) {
|
|
@@ -147,8 +147,8 @@ export default class Assert {
|
|
|
147
147
|
}
|
|
148
148
|
propEqual(actual, expected, message) {
|
|
149
149
|
this._incrementAssertionCount();
|
|
150
|
-
|
|
151
|
-
|
|
150
|
+
const targetActual = objectValues(actual);
|
|
151
|
+
const targetExpected = objectValues(expected);
|
|
152
152
|
if (!Assert.QUnit.equiv(targetActual, targetExpected)) {
|
|
153
153
|
throw new Assert.AssertionError({
|
|
154
154
|
actual: targetActual,
|
|
@@ -160,8 +160,8 @@ export default class Assert {
|
|
|
160
160
|
}
|
|
161
161
|
notPropEqual(actual, expected, message) {
|
|
162
162
|
this._incrementAssertionCount();
|
|
163
|
-
|
|
164
|
-
|
|
163
|
+
const targetActual = objectValues(actual);
|
|
164
|
+
const targetExpected = objectValues(expected);
|
|
165
165
|
if (Assert.QUnit.equiv(targetActual, targetExpected)) {
|
|
166
166
|
throw new Assert.AssertionError({
|
|
167
167
|
actual: targetActual,
|
|
@@ -173,8 +173,8 @@ export default class Assert {
|
|
|
173
173
|
}
|
|
174
174
|
propContains(actual, expected, message) {
|
|
175
175
|
this._incrementAssertionCount();
|
|
176
|
-
|
|
177
|
-
|
|
176
|
+
const targetActual = objectValuesSubset(actual, expected);
|
|
177
|
+
const targetExpected = objectValues(expected, false);
|
|
178
178
|
if (!Assert.QUnit.equiv(targetActual, targetExpected)) {
|
|
179
179
|
throw new Assert.AssertionError({
|
|
180
180
|
actual: targetActual,
|
|
@@ -186,8 +186,8 @@ export default class Assert {
|
|
|
186
186
|
}
|
|
187
187
|
notPropContains(actual, expected, message) {
|
|
188
188
|
this._incrementAssertionCount();
|
|
189
|
-
|
|
190
|
-
|
|
189
|
+
const targetActual = objectValuesSubset(actual, expected);
|
|
190
|
+
const targetExpected = objectValues(expected);
|
|
191
191
|
if (Assert.QUnit.equiv(targetActual, targetExpected)) {
|
|
192
192
|
throw new Assert.AssertionError({
|
|
193
193
|
actual: targetActual,
|
|
@@ -247,7 +247,7 @@ export default class Assert {
|
|
|
247
247
|
}
|
|
248
248
|
throws(blockFn, expectedInput, assertionMessage) {
|
|
249
249
|
this?._incrementAssertionCount();
|
|
250
|
-
|
|
250
|
+
const [expected, message] = validateExpectedExceptionArgs(expectedInput, assertionMessage, 'rejects');
|
|
251
251
|
if (typeof blockFn !== 'function') {
|
|
252
252
|
throw new Assert.AssertionError({
|
|
253
253
|
actual: blockFn,
|
|
@@ -260,7 +260,7 @@ export default class Assert {
|
|
|
260
260
|
try {
|
|
261
261
|
blockFn();
|
|
262
262
|
} catch (error) {
|
|
263
|
-
|
|
263
|
+
const validation = validateException(error, expected, message);
|
|
264
264
|
if (validation.result === false) {
|
|
265
265
|
throw new Assert.AssertionError({
|
|
266
266
|
actual: validation.result,
|
|
@@ -282,8 +282,8 @@ export default class Assert {
|
|
|
282
282
|
}
|
|
283
283
|
async rejects(promise, expectedInput, assertionMessage) {
|
|
284
284
|
this._incrementAssertionCount();
|
|
285
|
-
|
|
286
|
-
|
|
285
|
+
const [expected, message] = validateExpectedExceptionArgs(expectedInput, assertionMessage, 'rejects');
|
|
286
|
+
const then = promise && promise.then;
|
|
287
287
|
if (typeof then !== 'function') {
|
|
288
288
|
throw new Assert.AssertionError({
|
|
289
289
|
actual: promise,
|
|
@@ -302,7 +302,7 @@ export default class Assert {
|
|
|
302
302
|
stackStartFn: this.rejects,
|
|
303
303
|
});
|
|
304
304
|
} catch (error) {
|
|
305
|
-
|
|
305
|
+
const validation = validateException(error, expected, message);
|
|
306
306
|
if (validation.result === false) {
|
|
307
307
|
throw new Assert.AssertionError({
|
|
308
308
|
actual: validation.result,
|
package/shims/shared/index.js
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
const hasOwn = Object.prototype.hasOwnProperty
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) {
|
|
7
|
-
return typeof obj;
|
|
8
|
-
} : function (obj) {
|
|
9
|
-
return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
|
|
10
|
-
}, _typeof(obj);
|
|
11
|
-
}
|
|
3
|
+
const _typeof = typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol'
|
|
4
|
+
? (obj) => typeof obj
|
|
5
|
+
: (obj) => obj && typeof Symbol === 'function' && obj.constructor === Symbol && obj !== Symbol.prototype ? 'symbol' : typeof obj;
|
|
12
6
|
|
|
13
7
|
export function objectType(obj) {
|
|
14
8
|
if (typeof obj === 'undefined') {
|
|
@@ -19,8 +13,8 @@ export function objectType(obj) {
|
|
|
19
13
|
if (obj === null) {
|
|
20
14
|
return 'null';
|
|
21
15
|
}
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
const match = toString.call(obj).match(/^\[object\s(.*)\]$/);
|
|
17
|
+
const type = match && match[1];
|
|
24
18
|
switch (type) {
|
|
25
19
|
case 'Number':
|
|
26
20
|
if (isNaN(obj)) {
|
|
@@ -47,12 +41,12 @@ function is(type, obj) {
|
|
|
47
41
|
}
|
|
48
42
|
|
|
49
43
|
export function objectValues(obj) {
|
|
50
|
-
|
|
51
|
-
|
|
44
|
+
const allowArray = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
|
|
45
|
+
const vals = allowArray && is('array', obj) ? [] : {};
|
|
52
46
|
|
|
53
|
-
for (
|
|
47
|
+
for (const key in obj) {
|
|
54
48
|
if (hasOwn.call(obj, key)) {
|
|
55
|
-
|
|
49
|
+
const val = obj[key];
|
|
56
50
|
vals[key] = val === Object(val) ? objectValues(val, allowArray) : val;
|
|
57
51
|
}
|
|
58
52
|
}
|
|
@@ -80,8 +74,8 @@ export function objectValuesSubset(obj, model) {
|
|
|
80
74
|
|
|
81
75
|
// Unlike objectValues(), subset arrays to a plain objects as well.
|
|
82
76
|
// This enables subsetting [20, 30] with {1: 30}.
|
|
83
|
-
|
|
84
|
-
for (
|
|
77
|
+
const subset = {};
|
|
78
|
+
for (const key in model) {
|
|
85
79
|
if (hasOwn.call(model, key) && hasOwn.call(obj, key)) {
|
|
86
80
|
subset[key] = objectValuesSubset(obj[key], model[key]);
|
|
87
81
|
}
|
|
@@ -90,7 +84,7 @@ export function objectValuesSubset(obj, model) {
|
|
|
90
84
|
}
|
|
91
85
|
|
|
92
86
|
export function validateExpectedExceptionArgs(expected, message, assertionMethod) {
|
|
93
|
-
|
|
87
|
+
const expectedType = objectType(expected);
|
|
94
88
|
|
|
95
89
|
// 'expected' is optional unless doing string comparison
|
|
96
90
|
if (expectedType === 'string') {
|
|
@@ -102,7 +96,7 @@ export function validateExpectedExceptionArgs(expected, message, assertionMethod
|
|
|
102
96
|
throw new Error('assert.' + assertionMethod + ' does not accept a string value for the expected argument.\n' + 'Use a non-string object value (e.g. RegExp or validator function) ' + 'instead if necessary.');
|
|
103
97
|
}
|
|
104
98
|
}
|
|
105
|
-
|
|
99
|
+
const valid = !expected ||
|
|
106
100
|
// TODO: be more explicit here
|
|
107
101
|
expectedType === 'regexp' || expectedType === 'function' || expectedType === 'object';
|
|
108
102
|
if (!valid) {
|
|
@@ -112,8 +106,8 @@ export function validateExpectedExceptionArgs(expected, message, assertionMethod
|
|
|
112
106
|
}
|
|
113
107
|
|
|
114
108
|
export function validateException(actual, expected, message) {
|
|
115
|
-
|
|
116
|
-
|
|
109
|
+
let result = false;
|
|
110
|
+
const expectedType = objectType(expected);
|
|
117
111
|
|
|
118
112
|
// These branches should be exhaustive, based on validation done in validateExpectedException
|
|
119
113
|
|
|
@@ -157,7 +151,7 @@ export function validateException(actual, expected, message) {
|
|
|
157
151
|
|
|
158
152
|
function errorString(error) {
|
|
159
153
|
// Use String() instead of toString() to handle non-object values like undefined or null.
|
|
160
|
-
|
|
154
|
+
const resultErrorString = String(error);
|
|
161
155
|
|
|
162
156
|
// If the error wasn't a subclass of Error but something like
|
|
163
157
|
// an object literal with name and message properties...
|
|
@@ -16,7 +16,7 @@ export default class ModuleContext {
|
|
|
16
16
|
tests = [];
|
|
17
17
|
|
|
18
18
|
constructor(name) {
|
|
19
|
-
|
|
19
|
+
const parentModule = ModuleContext.currentModuleChain[ModuleContext.currentModuleChain.length - 1];
|
|
20
20
|
|
|
21
21
|
ModuleContext.currentModuleChain.push(this);
|
|
22
22
|
|
package/Makefile
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
.PHONY: check test lint build release
|
|
2
|
-
|
|
3
|
-
check: lint test
|
|
4
|
-
|
|
5
|
-
lint:
|
|
6
|
-
npm run lint
|
|
7
|
-
|
|
8
|
-
test:
|
|
9
|
-
npm test
|
|
10
|
-
|
|
11
|
-
build:
|
|
12
|
-
npm run build
|
|
13
|
-
|
|
14
|
-
# Lint, bump version, update changelog, commit, tag, push, publish to npm.
|
|
15
|
-
# CI then creates the GitHub release.
|
|
16
|
-
# Usage: make release LEVEL=patch|minor|major
|
|
17
|
-
release:
|
|
18
|
-
@test -n "$(LEVEL)" || (echo "Usage: make release LEVEL=patch|minor|major" && exit 1)
|
|
19
|
-
npm run lint
|
|
20
|
-
npm version $(LEVEL) --no-git-tag-version
|
|
21
|
-
npm run changelog:update
|
|
22
|
-
git add package.json package-lock.json CHANGELOG.md
|
|
23
|
-
git commit -m "Release $$(node -p 'require("./package.json").version')"
|
|
24
|
-
git tag "v$$(node -p 'require("./package.json").version')"
|
|
25
|
-
git push && git push --tags
|
|
26
|
-
npm publish --access public
|