qunitx 0.12.2 → 0.12.5
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 +13 -4
- package/package.json +10 -4
- package/scripts/check-coverage.js +15 -0
- package/shims/deno/index.js +140 -0
- package/shims/deno/module.js +35 -8
- package/shims/deno/test.js +38 -9
- package/shims/node/module.js +8 -8
- package/shims/node/test.js +8 -8
- package/shims/shared/assert.js +358 -15
- package/shims/shared/index.js +16 -22
- package/shims/shared/module-context.js +1 -1
- package/Makefile +0 -28
package/README.md
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# QUnitX
|
|
4
|
+
|
|
1
5
|
[](https://github.com/izelnakri/qunitx/actions/workflows/ci.yml)
|
|
6
|
+
[](https://codecov.io/gh/izelnakri/qunitx)
|
|
2
7
|
[](https://www.npmjs.com/package/qunitx)
|
|
3
8
|
[](https://www.npmjs.com/package/qunitx)
|
|
4
9
|
[](LICENSE)
|
|
5
10
|
[](https://github.com/izelnakri/qunitx/issues)
|
|
6
11
|
[](https://github.com/sponsors/izelnakri)
|
|
7
12
|
|
|
8
|
-
|
|
13
|
+
</div>
|
|
9
14
|
|
|
10
15
|
**The oldest, most battle-tested JavaScript test API — now universal.**
|
|
11
16
|
|
|
@@ -36,6 +41,10 @@ ecosystem:
|
|
|
36
41
|
QUnitX wraps this API to work with **Node.js's built-in `node:test` runner** and
|
|
37
42
|
**Deno's native test runner** — no Jest, Vitest, or other framework needed.
|
|
38
43
|
|
|
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.
|
|
47
|
+
|
|
39
48
|
---
|
|
40
49
|
|
|
41
50
|
## Demo
|
|
@@ -51,14 +60,12 @@ Live browser UI example (click to see filterable QUnit test suite):
|
|
|
51
60
|
|
|
52
61
|
[objectmodel.js.org/test/?moduleId=6e15ed5f](https://objectmodel.js.org/test/?moduleId=6e15ed5f&moduleId=950ec9c5)
|
|
53
62
|
|
|
54
|
-

|
|
55
|
-
|
|
56
63
|
---
|
|
57
64
|
|
|
58
65
|
## Installation
|
|
59
66
|
|
|
60
67
|
```sh
|
|
61
|
-
npm install qunitx
|
|
68
|
+
npm install qunitx --save-dev
|
|
62
69
|
```
|
|
63
70
|
|
|
64
71
|
Requires **Node.js >= 22** (LTS) or **Deno >= 2**.
|
|
@@ -204,6 +211,8 @@ familiar browser UI with zero extra layers.
|
|
|
204
211
|
|
|
205
212
|
## Code coverage
|
|
206
213
|
|
|
214
|
+
Probably c8 isn't even needed since qunitx runs as a dependency(rather than runtime) on node.js and deno.
|
|
215
|
+
|
|
207
216
|
```sh
|
|
208
217
|
# Node (any c8-compatible reporter)
|
|
209
218
|
npx c8 node --test test/
|
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.5",
|
|
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",
|
|
@@ -63,8 +63,10 @@
|
|
|
63
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",
|
|
@@ -77,7 +79,11 @@
|
|
|
77
79
|
"test:dev": "npm run test | tee test-output.log",
|
|
78
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,3 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QUnitX — universal test library that runs the same test file in Node.js, Deno, and browser.
|
|
3
|
+
*
|
|
4
|
+
* Wraps QUnit's assertion API over each runtime's native BDD test runner so you only
|
|
5
|
+
* write your tests once.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```js
|
|
9
|
+
* import { module, test } from "qunitx";
|
|
10
|
+
*
|
|
11
|
+
* module("Math", (hooks) => {
|
|
12
|
+
* hooks.before((assert) => assert.step("setup"));
|
|
13
|
+
*
|
|
14
|
+
* test("addition", (assert) => {
|
|
15
|
+
* assert.equal(1 + 1, 2);
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* test("async", async (assert) => {
|
|
19
|
+
* const n = await Promise.resolve(42);
|
|
20
|
+
* assert.strictEqual(n, 42);
|
|
21
|
+
* });
|
|
22
|
+
* });
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @module
|
|
26
|
+
*/
|
|
1
27
|
import { AssertionError as DenoAssertionError } from "jsr:@std/assert";
|
|
2
28
|
import '../../vendor/qunit.js';
|
|
3
29
|
import Assert from '../shared/assert.js';
|
|
@@ -6,6 +32,24 @@ import TestContext from '../shared/test-context.js';
|
|
|
6
32
|
import Module from './module.js';
|
|
7
33
|
import Test from './test.js';
|
|
8
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Thrown when an assertion fails. Extends Deno's built-in `AssertionError`
|
|
37
|
+
* so it integrates cleanly with Deno's test runner output.
|
|
38
|
+
*
|
|
39
|
+
* You rarely construct this directly — assertion methods on {@linkcode Assert}
|
|
40
|
+
* throw it automatically on failure.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```js
|
|
44
|
+
* import { AssertionError } from "qunitx";
|
|
45
|
+
*
|
|
46
|
+
* try {
|
|
47
|
+
* throw new AssertionError({ message: "something went wrong" });
|
|
48
|
+
* } catch (e) {
|
|
49
|
+
* console.log(e instanceof AssertionError); // true
|
|
50
|
+
* }
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
9
53
|
export class AssertionError extends DenoAssertionError {
|
|
10
54
|
constructor(object) {
|
|
11
55
|
super(object.message);
|
|
@@ -21,7 +65,103 @@ Object.freeze(Assert);
|
|
|
21
65
|
Object.freeze(ModuleContext);
|
|
22
66
|
Object.freeze(TestContext);
|
|
23
67
|
|
|
68
|
+
export { Assert };
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Defines a test module (suite). Wraps Deno's `describe()` and sets up the
|
|
72
|
+
* QUnit lifecycle — `before`, `beforeEach`, `afterEach`, and `after` hooks,
|
|
73
|
+
* assertion counting, and step tracking.
|
|
74
|
+
*
|
|
75
|
+
* Each {@linkcode test} inside the callback receives an {@linkcode Assert} instance.
|
|
76
|
+
* Modules can be nested by calling `module()` inside another module's callback.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} moduleName - Name of the test suite.
|
|
79
|
+
* @param {object} [runtimeOptions] - Optional Deno BDD options forwarded to `describe()`
|
|
80
|
+
* (e.g. `{ concurrency: false }`, `{ permissions: { read: true } }`).
|
|
81
|
+
* @param {function} moduleContent - Callback that defines tests and hooks.
|
|
82
|
+
* Receives `(hooks, { moduleName, options })` where `hooks` exposes
|
|
83
|
+
* `before`, `beforeEach`, `afterEach`, and `after`.
|
|
84
|
+
* @example
|
|
85
|
+
* ```js
|
|
86
|
+
* import { module, test } from "qunitx";
|
|
87
|
+
*
|
|
88
|
+
* module("Math", (hooks) => {
|
|
89
|
+
* hooks.before((assert) => {
|
|
90
|
+
* assert.step("before hook ran");
|
|
91
|
+
* });
|
|
92
|
+
*
|
|
93
|
+
* test("addition", (assert) => {
|
|
94
|
+
* assert.equal(2 + 2, 4);
|
|
95
|
+
* });
|
|
96
|
+
* });
|
|
97
|
+
* ```
|
|
98
|
+
* @example
|
|
99
|
+
* ```js
|
|
100
|
+
* // Nested modules
|
|
101
|
+
* module("Outer", () => {
|
|
102
|
+
* module("Inner", () => {
|
|
103
|
+
* test("nested test", (assert) => {
|
|
104
|
+
* assert.ok(true);
|
|
105
|
+
* });
|
|
106
|
+
* });
|
|
107
|
+
* });
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
24
110
|
export const module = Module;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Defines an individual test. Wraps Deno's `it()` and handles the full QUnit
|
|
114
|
+
* lifecycle: `beforeEach`/`afterEach` hooks, async assertion waiting, and step
|
|
115
|
+
* verification. Must be called inside a {@linkcode module} callback.
|
|
116
|
+
*
|
|
117
|
+
* The test callback receives `(assert, { testName, options })` where `assert`
|
|
118
|
+
* is an {@linkcode Assert} instance.
|
|
119
|
+
*
|
|
120
|
+
* @param {string} testName - Name of the test.
|
|
121
|
+
* @param {object} [runtimeOptions] - Optional Deno BDD options forwarded to `it()`
|
|
122
|
+
* (e.g. `{ concurrency: false }`, `{ sanitizeExit: false }`).
|
|
123
|
+
* @param {function} testContent - Test callback receiving `(assert, { testName, options })`.
|
|
124
|
+
* @example
|
|
125
|
+
* ```js
|
|
126
|
+
* import { module, test } from "qunitx";
|
|
127
|
+
*
|
|
128
|
+
* module("Math", () => {
|
|
129
|
+
* test("addition", (assert) => {
|
|
130
|
+
* assert.equal(1 + 1, 2);
|
|
131
|
+
* });
|
|
132
|
+
*
|
|
133
|
+
* test("async resolves correctly", async (assert) => {
|
|
134
|
+
* const result = await Promise.resolve(42);
|
|
135
|
+
* assert.strictEqual(result, 42);
|
|
136
|
+
* });
|
|
137
|
+
* });
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
25
140
|
export const test = Test;
|
|
26
141
|
|
|
142
|
+
/**
|
|
143
|
+
* The default export provides the full QUnitX API as a single object.
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* ```js
|
|
147
|
+
* import qunitx from "qunitx";
|
|
148
|
+
*
|
|
149
|
+
* qunitx.module("Math", () => {
|
|
150
|
+
* qunitx.test("addition", (assert) => {
|
|
151
|
+
* assert.equal(1 + 1, 2);
|
|
152
|
+
* });
|
|
153
|
+
* });
|
|
154
|
+
* ```
|
|
155
|
+
*
|
|
156
|
+
* @property {Function} module - Defines a test suite. Wraps Deno's `describe()` with
|
|
157
|
+
* QUnit lifecycle hooks (`before`, `beforeEach`, `afterEach`, `after`).
|
|
158
|
+
* See the named {@linkcode module} export for full parameter documentation.
|
|
159
|
+
* @property {Function} test - Defines an individual test inside a `module()` callback.
|
|
160
|
+
* Receives an {@linkcode Assert} instance as its first argument.
|
|
161
|
+
* See the named {@linkcode test} export for full parameter documentation.
|
|
162
|
+
* @property {typeof AssertionError} AssertionError - The error class thrown when an
|
|
163
|
+
* assertion fails. Extends Deno's built-in `AssertionError`.
|
|
164
|
+
* @property {object} config - Runtime configuration object (currently unused; reserved
|
|
165
|
+
* for future QUnit config compatibility).
|
|
166
|
+
*/
|
|
27
167
|
export default { AssertionError: Assert.AssertionError, module, test, config: {} };
|
package/shims/deno/module.js
CHANGED
|
@@ -5,14 +5,41 @@ import ModuleContext from '../shared/module-context.js';
|
|
|
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
|
@@ -2,29 +2,58 @@ 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
|
@@ -6,6 +6,25 @@ import util from 'node:util';
|
|
|
6
6
|
// NOTE: Another approach for a global report Make this._assertions.set(this.currentTest, (this._assertions.get(this.currentTest) || 0) + 1); for pushResult
|
|
7
7
|
// NOTE: This should *always* be a singleton(?), passed around as an argument for hooks. Seems difficult with concurrency. Singleton needs to be a concurrent data structure.
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* The assertion object passed to every test callback and lifecycle hook.
|
|
11
|
+
*
|
|
12
|
+
* Every {@linkcode test} callback receives an instance of `Assert` as its first argument.
|
|
13
|
+
* All assertion methods throw an {@linkcode AssertionError} on failure, which the test
|
|
14
|
+
* runner catches and reports.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```js
|
|
18
|
+
* import { module, test } from "qunitx";
|
|
19
|
+
*
|
|
20
|
+
* module("Math", () => {
|
|
21
|
+
* test("addition", (assert) => {
|
|
22
|
+
* assert.equal(1 + 1, 2);
|
|
23
|
+
* assert.strictEqual(typeof 42, "number");
|
|
24
|
+
* });
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
9
28
|
export default class Assert {
|
|
10
29
|
static QUnit;
|
|
11
30
|
static AssertionError;
|
|
@@ -17,6 +36,20 @@ export default class Assert {
|
|
|
17
36
|
_incrementAssertionCount() {
|
|
18
37
|
this.test.totalExecutedAssertions++;
|
|
19
38
|
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Sets the number of milliseconds after which the current test will fail if not yet complete.
|
|
42
|
+
*
|
|
43
|
+
* @param {number} number - Timeout in milliseconds (positive integer).
|
|
44
|
+
* @example
|
|
45
|
+
* ```js
|
|
46
|
+
* test("slow async operation", async (assert) => {
|
|
47
|
+
* assert.timeout(500);
|
|
48
|
+
* await somethingAsync();
|
|
49
|
+
* assert.ok(true);
|
|
50
|
+
* });
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
20
53
|
timeout(number) {
|
|
21
54
|
if (!Number.isInteger(number) || number < 0) {
|
|
22
55
|
throw new Error('assert.timeout() expects a positive integer.');
|
|
@@ -24,6 +57,22 @@ export default class Assert {
|
|
|
24
57
|
|
|
25
58
|
this.test.timeout = number;
|
|
26
59
|
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Records a named step. Use with {@linkcode Assert.prototype.verifySteps} to assert that
|
|
63
|
+
* a sequence of steps occurred in the right order.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} message - The step label to record.
|
|
66
|
+
* @example
|
|
67
|
+
* ```js
|
|
68
|
+
* test("event order", (assert) => {
|
|
69
|
+
* assert.expect(3);
|
|
70
|
+
* assert.step("step one");
|
|
71
|
+
* assert.step("step two");
|
|
72
|
+
* assert.verifySteps(["step one", "step two"]);
|
|
73
|
+
* });
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
27
76
|
step(message) {
|
|
28
77
|
let assertionMessage = message;
|
|
29
78
|
let result = !!message;
|
|
@@ -42,10 +91,41 @@ export default class Assert {
|
|
|
42
91
|
message: assertionMessage
|
|
43
92
|
});
|
|
44
93
|
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Asserts that the steps recorded via {@linkcode Assert.prototype.step} match the given array,
|
|
97
|
+
* then clears the recorded steps.
|
|
98
|
+
*
|
|
99
|
+
* @param {string[]} steps - Expected array of step labels in order.
|
|
100
|
+
* @param {string} [message] - Optional failure message.
|
|
101
|
+
* @example
|
|
102
|
+
* ```js
|
|
103
|
+
* test("lifecycle order", (assert) => {
|
|
104
|
+
* assert.step("init");
|
|
105
|
+
* assert.step("run");
|
|
106
|
+
* assert.verifySteps(["init", "run"]);
|
|
107
|
+
* });
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
45
110
|
verifySteps(steps, message = 'Verify steps failed!') {
|
|
46
111
|
this.deepEqual(this.test.steps, steps, message);
|
|
47
112
|
this.test.steps.length = 0;
|
|
48
113
|
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Sets the number of assertions expected to run in the current test.
|
|
117
|
+
* The test fails if a different number of assertions actually ran.
|
|
118
|
+
*
|
|
119
|
+
* @param {number} number - Expected assertion count (non-negative integer).
|
|
120
|
+
* @example
|
|
121
|
+
* ```js
|
|
122
|
+
* test("exactly two assertions", (assert) => {
|
|
123
|
+
* assert.expect(2);
|
|
124
|
+
* assert.ok(true);
|
|
125
|
+
* assert.ok(true);
|
|
126
|
+
* });
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
49
129
|
expect(number) {
|
|
50
130
|
if (!Number.isInteger(number) || number < 0) {
|
|
51
131
|
throw new Error('assert.expect() expects a positive integer.');
|
|
@@ -53,17 +133,57 @@ export default class Assert {
|
|
|
53
133
|
|
|
54
134
|
this.test.expectedAssertionCount = number;
|
|
55
135
|
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Returns a `done` callback for callback-style async tests. The test will not
|
|
139
|
+
* finish until every `done` callback returned by `async()` has been called.
|
|
140
|
+
*
|
|
141
|
+
* For `async/await` tests prefer `async (assert) => { ... }` directly.
|
|
142
|
+
*
|
|
143
|
+
* @returns {function} A callback to invoke when the async work finishes.
|
|
144
|
+
* @example
|
|
145
|
+
* ```js
|
|
146
|
+
* test("async callback style", (assert) => {
|
|
147
|
+
* const done = assert.async();
|
|
148
|
+
* setTimeout(() => {
|
|
149
|
+
* assert.ok(true, "async callback ran");
|
|
150
|
+
* done();
|
|
151
|
+
* }, 10);
|
|
152
|
+
* });
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
56
155
|
async() {
|
|
57
156
|
let resolveFn;
|
|
58
|
-
|
|
157
|
+
const done = new Promise(resolve => { resolveFn = resolve; });
|
|
59
158
|
|
|
60
159
|
this.test.asyncOps.push(done);
|
|
61
160
|
|
|
62
161
|
return () => { resolveFn(); };
|
|
63
162
|
}
|
|
64
|
-
|
|
163
|
+
|
|
164
|
+
waitForAsyncOps() {
|
|
65
165
|
return Promise.all(this.test.asyncOps);
|
|
66
166
|
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Pushes a custom assertion result. Fails the test if `resultInfo.result` is falsy.
|
|
170
|
+
* Throws an {@linkcode AssertionError} on failure.
|
|
171
|
+
*
|
|
172
|
+
* Useful for building custom assertion helpers.
|
|
173
|
+
*
|
|
174
|
+
* @param {{ result: boolean, actual?: unknown, expected?: unknown, message?: string }} resultInfo
|
|
175
|
+
* @example
|
|
176
|
+
* ```js
|
|
177
|
+
* test("custom assertion", (assert) => {
|
|
178
|
+
* assert.pushResult({
|
|
179
|
+
* result: 1 + 1 === 2,
|
|
180
|
+
* actual: 2,
|
|
181
|
+
* expected: 2,
|
|
182
|
+
* message: "custom math check",
|
|
183
|
+
* });
|
|
184
|
+
* });
|
|
185
|
+
* ```
|
|
186
|
+
*/
|
|
67
187
|
pushResult(resultInfo = {}) {
|
|
68
188
|
this._incrementAssertionCount();
|
|
69
189
|
if (!resultInfo.result) {
|
|
@@ -77,6 +197,19 @@ export default class Assert {
|
|
|
77
197
|
|
|
78
198
|
return this;
|
|
79
199
|
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Asserts that `state` is truthy.
|
|
203
|
+
*
|
|
204
|
+
* @param {unknown} state - The value to test.
|
|
205
|
+
* @param {string} [message] - Optional failure message.
|
|
206
|
+
* @example
|
|
207
|
+
* ```js
|
|
208
|
+
* assert.ok(true);
|
|
209
|
+
* assert.ok(1, "non-zero is truthy");
|
|
210
|
+
* assert.ok("hello");
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
80
213
|
ok(state, message) {
|
|
81
214
|
this._incrementAssertionCount();
|
|
82
215
|
if (!state) {
|
|
@@ -88,6 +221,19 @@ export default class Assert {
|
|
|
88
221
|
});
|
|
89
222
|
}
|
|
90
223
|
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Asserts that `state` is falsy.
|
|
227
|
+
*
|
|
228
|
+
* @param {unknown} state - The value to test.
|
|
229
|
+
* @param {string} [message] - Optional failure message.
|
|
230
|
+
* @example
|
|
231
|
+
* ```js
|
|
232
|
+
* assert.notOk(false);
|
|
233
|
+
* assert.notOk(0, "zero is falsy");
|
|
234
|
+
* assert.notOk(null);
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
91
237
|
notOk(state, message) {
|
|
92
238
|
this._incrementAssertionCount();
|
|
93
239
|
if (state) {
|
|
@@ -99,6 +245,18 @@ export default class Assert {
|
|
|
99
245
|
});
|
|
100
246
|
}
|
|
101
247
|
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Asserts that `state === true` (strict boolean true).
|
|
251
|
+
*
|
|
252
|
+
* @param {unknown} state - The value to test.
|
|
253
|
+
* @param {string} [message] - Optional failure message.
|
|
254
|
+
* @example
|
|
255
|
+
* ```js
|
|
256
|
+
* assert.true(1 === 1);
|
|
257
|
+
* assert.true(Array.isArray([]), "arrays are arrays");
|
|
258
|
+
* ```
|
|
259
|
+
*/
|
|
102
260
|
true(state, message) {
|
|
103
261
|
this._incrementAssertionCount();
|
|
104
262
|
if (state !== true) {
|
|
@@ -110,6 +268,18 @@ export default class Assert {
|
|
|
110
268
|
});
|
|
111
269
|
}
|
|
112
270
|
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Asserts that `state === false` (strict boolean false).
|
|
274
|
+
*
|
|
275
|
+
* @param {unknown} state - The value to test.
|
|
276
|
+
* @param {string} [message] - Optional failure message.
|
|
277
|
+
* @example
|
|
278
|
+
* ```js
|
|
279
|
+
* assert.false(1 === 2);
|
|
280
|
+
* assert.false(Number.isNaN(42), "42 is not NaN");
|
|
281
|
+
* ```
|
|
282
|
+
*/
|
|
113
283
|
false(state, message) {
|
|
114
284
|
this._incrementAssertionCount();
|
|
115
285
|
if (state !== false) {
|
|
@@ -121,6 +291,22 @@ export default class Assert {
|
|
|
121
291
|
});
|
|
122
292
|
}
|
|
123
293
|
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Asserts that `actual == expected` (loose equality, allows type coercion).
|
|
297
|
+
*
|
|
298
|
+
* Prefer {@linkcode Assert.prototype.strictEqual} for most comparisons. Use {@linkcode Assert.prototype.notEqual}
|
|
299
|
+
* for the inverse.
|
|
300
|
+
*
|
|
301
|
+
* @param {unknown} actual - The value produced by the code under test.
|
|
302
|
+
* @param {unknown} expected - The expected value.
|
|
303
|
+
* @param {string} [message] - Optional failure message.
|
|
304
|
+
* @example
|
|
305
|
+
* ```js
|
|
306
|
+
* assert.equal(1, 1);
|
|
307
|
+
* assert.equal("1", 1, "loose equality allows coercion");
|
|
308
|
+
* ```
|
|
309
|
+
*/
|
|
124
310
|
equal(actual, expected, message) {
|
|
125
311
|
this._incrementAssertionCount();
|
|
126
312
|
if (actual != expected) {
|
|
@@ -133,6 +319,19 @@ export default class Assert {
|
|
|
133
319
|
});
|
|
134
320
|
}
|
|
135
321
|
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Asserts that `actual != expected` (loose inequality). Inverse of {@linkcode Assert.prototype.equal}.
|
|
325
|
+
*
|
|
326
|
+
* @param {unknown} actual - The actual value.
|
|
327
|
+
* @param {unknown} expected - The value it should not loosely equal.
|
|
328
|
+
* @param {string} [message] - Optional failure message.
|
|
329
|
+
* @example
|
|
330
|
+
* ```js
|
|
331
|
+
* assert.notEqual(1, 2);
|
|
332
|
+
* assert.notEqual("hello", "world");
|
|
333
|
+
* ```
|
|
334
|
+
*/
|
|
136
335
|
notEqual(actual, expected, message) {
|
|
137
336
|
this._incrementAssertionCount();
|
|
138
337
|
if (actual == expected) {
|
|
@@ -145,10 +344,27 @@ export default class Assert {
|
|
|
145
344
|
});
|
|
146
345
|
}
|
|
147
346
|
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Asserts that `actual` and `expected` have the same own enumerable properties
|
|
350
|
+
* and values. Prototype methods are ignored; only own properties are compared.
|
|
351
|
+
*
|
|
352
|
+
* @param {object} actual - The actual object.
|
|
353
|
+
* @param {object} expected - The expected object.
|
|
354
|
+
* @param {string} [message] - Optional failure message.
|
|
355
|
+
* @example
|
|
356
|
+
* ```js
|
|
357
|
+
* assert.propEqual({ a: 1, b: 2 }, { a: 1, b: 2 });
|
|
358
|
+
*
|
|
359
|
+
* // Ignores prototype methods — only own properties matter:
|
|
360
|
+
* function Point(x, y) { this.x = x; this.y = y; }
|
|
361
|
+
* assert.propEqual(new Point(1, 2), { x: 1, y: 2 });
|
|
362
|
+
* ```
|
|
363
|
+
*/
|
|
148
364
|
propEqual(actual, expected, message) {
|
|
149
365
|
this._incrementAssertionCount();
|
|
150
|
-
|
|
151
|
-
|
|
366
|
+
const targetActual = objectValues(actual);
|
|
367
|
+
const targetExpected = objectValues(expected);
|
|
152
368
|
if (!Assert.QUnit.equiv(targetActual, targetExpected)) {
|
|
153
369
|
throw new Assert.AssertionError({
|
|
154
370
|
actual: targetActual,
|
|
@@ -158,10 +374,24 @@ export default class Assert {
|
|
|
158
374
|
});
|
|
159
375
|
}
|
|
160
376
|
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Asserts that `actual` and `expected` do NOT have the same own enumerable
|
|
380
|
+
* properties and values. Inverse of {@linkcode Assert.prototype.propEqual}.
|
|
381
|
+
*
|
|
382
|
+
* @param {object} actual - The actual object.
|
|
383
|
+
* @param {object} expected - The value it should not propEqual.
|
|
384
|
+
* @param {string} [message] - Optional failure message.
|
|
385
|
+
* @example
|
|
386
|
+
* ```js
|
|
387
|
+
* assert.notPropEqual({ a: 1 }, { a: 2 });
|
|
388
|
+
* assert.notPropEqual({ a: 1, b: 2 }, { a: 1 }); // extra key makes them unequal
|
|
389
|
+
* ```
|
|
390
|
+
*/
|
|
161
391
|
notPropEqual(actual, expected, message) {
|
|
162
392
|
this._incrementAssertionCount();
|
|
163
|
-
|
|
164
|
-
|
|
393
|
+
const targetActual = objectValues(actual);
|
|
394
|
+
const targetExpected = objectValues(expected);
|
|
165
395
|
if (Assert.QUnit.equiv(targetActual, targetExpected)) {
|
|
166
396
|
throw new Assert.AssertionError({
|
|
167
397
|
actual: targetActual,
|
|
@@ -171,10 +401,24 @@ export default class Assert {
|
|
|
171
401
|
});
|
|
172
402
|
}
|
|
173
403
|
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Asserts that `actual` contains all own enumerable properties from `expected`
|
|
407
|
+
* with matching values. Extra properties on `actual` are allowed and ignored.
|
|
408
|
+
*
|
|
409
|
+
* @param {object} actual - The actual object (may have extra keys).
|
|
410
|
+
* @param {object} expected - The subset of key/value pairs that must be present.
|
|
411
|
+
* @param {string} [message] - Optional failure message.
|
|
412
|
+
* @example
|
|
413
|
+
* ```js
|
|
414
|
+
* assert.propContains({ a: 1, b: 2, c: 3 }, { a: 1, b: 2 });
|
|
415
|
+
* assert.propContains(user, { role: "admin" });
|
|
416
|
+
* ```
|
|
417
|
+
*/
|
|
174
418
|
propContains(actual, expected, message) {
|
|
175
419
|
this._incrementAssertionCount();
|
|
176
|
-
|
|
177
|
-
|
|
420
|
+
const targetActual = objectValuesSubset(actual, expected);
|
|
421
|
+
const targetExpected = objectValues(expected, false);
|
|
178
422
|
if (!Assert.QUnit.equiv(targetActual, targetExpected)) {
|
|
179
423
|
throw new Assert.AssertionError({
|
|
180
424
|
actual: targetActual,
|
|
@@ -184,10 +428,24 @@ export default class Assert {
|
|
|
184
428
|
});
|
|
185
429
|
}
|
|
186
430
|
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Asserts that `actual` does NOT contain all own enumerable properties
|
|
434
|
+
* from `expected` with matching values. Inverse of {@linkcode Assert.prototype.propContains}.
|
|
435
|
+
*
|
|
436
|
+
* @param {object} actual - The actual object.
|
|
437
|
+
* @param {object} expected - The subset of properties that must NOT all match.
|
|
438
|
+
* @param {string} [message] - Optional failure message.
|
|
439
|
+
* @example
|
|
440
|
+
* ```js
|
|
441
|
+
* assert.notPropContains({ a: 1, b: 2 }, { a: 9 });
|
|
442
|
+
* assert.notPropContains(user, { role: "banned" });
|
|
443
|
+
* ```
|
|
444
|
+
*/
|
|
187
445
|
notPropContains(actual, expected, message) {
|
|
188
446
|
this._incrementAssertionCount();
|
|
189
|
-
|
|
190
|
-
|
|
447
|
+
const targetActual = objectValuesSubset(actual, expected);
|
|
448
|
+
const targetExpected = objectValues(expected);
|
|
191
449
|
if (Assert.QUnit.equiv(targetActual, targetExpected)) {
|
|
192
450
|
throw new Assert.AssertionError({
|
|
193
451
|
actual: targetActual,
|
|
@@ -197,6 +455,20 @@ export default class Assert {
|
|
|
197
455
|
});
|
|
198
456
|
}
|
|
199
457
|
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Asserts deep equality between `actual` and `expected` using recursive structural
|
|
461
|
+
* comparison. Handles nested objects, arrays, `Date`, `RegExp`, and more.
|
|
462
|
+
*
|
|
463
|
+
* @param {unknown} actual - The actual value.
|
|
464
|
+
* @param {unknown} expected - The expected value.
|
|
465
|
+
* @param {string} [message] - Optional failure message.
|
|
466
|
+
* @example
|
|
467
|
+
* ```js
|
|
468
|
+
* assert.deepEqual([1, { a: 2 }], [1, { a: 2 }]);
|
|
469
|
+
* assert.deepEqual(new Date("2024-01-01"), new Date("2024-01-01"));
|
|
470
|
+
* ```
|
|
471
|
+
*/
|
|
200
472
|
deepEqual(actual, expected, message) {
|
|
201
473
|
this._incrementAssertionCount();
|
|
202
474
|
if (!Assert.QUnit.equiv(actual, expected)) {
|
|
@@ -209,6 +481,19 @@ export default class Assert {
|
|
|
209
481
|
});
|
|
210
482
|
}
|
|
211
483
|
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Asserts that `actual` and `expected` are NOT deeply equal. Inverse of {@linkcode Assert.prototype.deepEqual}.
|
|
487
|
+
*
|
|
488
|
+
* @param {unknown} actual - The actual value.
|
|
489
|
+
* @param {unknown} expected - The value it should not deepEqual.
|
|
490
|
+
* @param {string} [message] - Optional failure message.
|
|
491
|
+
* @example
|
|
492
|
+
* ```js
|
|
493
|
+
* assert.notDeepEqual([1, 2], [1, 3]);
|
|
494
|
+
* assert.notDeepEqual({ a: 1 }, { a: 2 });
|
|
495
|
+
* ```
|
|
496
|
+
*/
|
|
212
497
|
notDeepEqual(actual, expected, message) {
|
|
213
498
|
this._incrementAssertionCount();
|
|
214
499
|
if (Assert.QUnit.equiv(actual, expected)) {
|
|
@@ -221,6 +506,19 @@ export default class Assert {
|
|
|
221
506
|
});
|
|
222
507
|
}
|
|
223
508
|
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Asserts that `actual === expected` (strict equality, no type coercion).
|
|
512
|
+
*
|
|
513
|
+
* @param {unknown} actual - The actual value.
|
|
514
|
+
* @param {unknown} expected - The expected value.
|
|
515
|
+
* @param {string} [message] - Optional failure message.
|
|
516
|
+
* @example
|
|
517
|
+
* ```js
|
|
518
|
+
* assert.strictEqual(1 + 1, 2);
|
|
519
|
+
* assert.strictEqual(typeof "hello", "string");
|
|
520
|
+
* ```
|
|
521
|
+
*/
|
|
224
522
|
strictEqual(actual, expected, message) {
|
|
225
523
|
this._incrementAssertionCount();
|
|
226
524
|
if (actual !== expected) {
|
|
@@ -233,6 +531,19 @@ export default class Assert {
|
|
|
233
531
|
});
|
|
234
532
|
}
|
|
235
533
|
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Asserts that `actual !== expected` (strict inequality). Inverse of {@linkcode Assert.prototype.strictEqual}.
|
|
537
|
+
*
|
|
538
|
+
* @param {unknown} actual - The actual value.
|
|
539
|
+
* @param {unknown} expected - The value it should not strictly equal.
|
|
540
|
+
* @param {string} [message] - Optional failure message.
|
|
541
|
+
* @example
|
|
542
|
+
* ```js
|
|
543
|
+
* assert.notStrictEqual(1, "1", "different types");
|
|
544
|
+
* assert.notStrictEqual({}, {}, "different object references");
|
|
545
|
+
* ```
|
|
546
|
+
*/
|
|
236
547
|
notStrictEqual(actual, expected, message) {
|
|
237
548
|
this._incrementAssertionCount();
|
|
238
549
|
if (actual === expected) {
|
|
@@ -245,9 +556,25 @@ export default class Assert {
|
|
|
245
556
|
});
|
|
246
557
|
}
|
|
247
558
|
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Asserts that `blockFn` throws an exception. Optionally validates the thrown
|
|
562
|
+
* error against a string (message substring), RegExp (message pattern),
|
|
563
|
+
* or constructor (`instanceof` check). For async functions use {@linkcode Assert.prototype.rejects}.
|
|
564
|
+
*
|
|
565
|
+
* @param {function} blockFn - A synchronous function expected to throw.
|
|
566
|
+
* @param {string|RegExp|function} [expected] - Optional matcher for the thrown error.
|
|
567
|
+
* @param {string} [message] - Optional failure message.
|
|
568
|
+
* @example
|
|
569
|
+
* ```js
|
|
570
|
+
* assert.throws(() => { throw new Error("boom"); });
|
|
571
|
+
* assert.throws(() => JSON.parse("{bad}"), SyntaxError);
|
|
572
|
+
* assert.throws(() => { throw new Error("bad input"); }, /bad input/);
|
|
573
|
+
* ```
|
|
574
|
+
*/
|
|
248
575
|
throws(blockFn, expectedInput, assertionMessage) {
|
|
249
576
|
this?._incrementAssertionCount();
|
|
250
|
-
|
|
577
|
+
const [expected, message] = validateExpectedExceptionArgs(expectedInput, assertionMessage, 'rejects');
|
|
251
578
|
if (typeof blockFn !== 'function') {
|
|
252
579
|
throw new Assert.AssertionError({
|
|
253
580
|
actual: blockFn,
|
|
@@ -260,7 +587,7 @@ export default class Assert {
|
|
|
260
587
|
try {
|
|
261
588
|
blockFn();
|
|
262
589
|
} catch (error) {
|
|
263
|
-
|
|
590
|
+
const validation = validateException(error, expected, message);
|
|
264
591
|
if (validation.result === false) {
|
|
265
592
|
throw new Assert.AssertionError({
|
|
266
593
|
actual: validation.result,
|
|
@@ -280,10 +607,26 @@ export default class Assert {
|
|
|
280
607
|
stackStartFn: this.throws,
|
|
281
608
|
});
|
|
282
609
|
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Asserts that a promise rejects. Optionally validates the rejection reason
|
|
613
|
+
* against a string (message substring), RegExp (message pattern),
|
|
614
|
+
* or constructor (`instanceof` check). For synchronous throws use {@linkcode Assert.prototype.throws}.
|
|
615
|
+
*
|
|
616
|
+
* @param {Promise<unknown>} promise - A promise expected to reject.
|
|
617
|
+
* @param {string|RegExp|function} [expected] - Optional matcher for the rejection reason.
|
|
618
|
+
* @param {string} [message] - Optional failure message.
|
|
619
|
+
* @example
|
|
620
|
+
* ```js
|
|
621
|
+
* await assert.rejects(Promise.reject(new Error("oops")));
|
|
622
|
+
* await assert.rejects(fetch("/bad-url"), TypeError);
|
|
623
|
+
* await assert.rejects(Promise.reject(new Error("timeout")), /timeout/);
|
|
624
|
+
* ```
|
|
625
|
+
*/
|
|
283
626
|
async rejects(promise, expectedInput, assertionMessage) {
|
|
284
627
|
this._incrementAssertionCount();
|
|
285
|
-
|
|
286
|
-
|
|
628
|
+
const [expected, message] = validateExpectedExceptionArgs(expectedInput, assertionMessage, 'rejects');
|
|
629
|
+
const then = promise && promise.then;
|
|
287
630
|
if (typeof then !== 'function') {
|
|
288
631
|
throw new Assert.AssertionError({
|
|
289
632
|
actual: promise,
|
|
@@ -302,7 +645,7 @@ export default class Assert {
|
|
|
302
645
|
stackStartFn: this.rejects,
|
|
303
646
|
});
|
|
304
647
|
} catch (error) {
|
|
305
|
-
|
|
648
|
+
const validation = validateException(error, expected, message);
|
|
306
649
|
if (validation.result === false) {
|
|
307
650
|
throw new Assert.AssertionError({
|
|
308
651
|
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,28 +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 (defaults to patch)
|
|
17
|
-
# make release LEVEL=minor|major
|
|
18
|
-
LEVEL ?= patch
|
|
19
|
-
release:
|
|
20
|
-
@npm whoami 2>/dev/null || npm login
|
|
21
|
-
npm run lint
|
|
22
|
-
npm version $(LEVEL) --no-git-tag-version
|
|
23
|
-
npm run changelog:update
|
|
24
|
-
git add package.json package-lock.json CHANGELOG.md
|
|
25
|
-
git commit -m "Release $$(node -p 'require("./package.json").version')"
|
|
26
|
-
git tag "v$$(node -p 'require("./package.json").version')"
|
|
27
|
-
git push && git push --tags
|
|
28
|
-
npm publish --access public
|