qunitx 1.0.3 → 1.0.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 +18 -5
- package/package.json +3 -3
- package/shims/deno/module.js +13 -26
- package/shims/deno/test.js +10 -5
- package/shims/node/module.js +13 -26
- package/shims/node/test.js +10 -5
- package/shims/shared/assert.js +0 -4
- package/shims/shared/module-context.js +5 -0
package/README.md
CHANGED
|
@@ -170,16 +170,29 @@ import { module, test } from 'qunitx';
|
|
|
170
170
|
|
|
171
171
|
---
|
|
172
172
|
|
|
173
|
-
##
|
|
173
|
+
## QUnit compatibility
|
|
174
174
|
|
|
175
|
-
|
|
176
|
-
|
|
175
|
+
qunitx follows the same test-environment model as QUnit:
|
|
176
|
+
|
|
177
|
+
- **Fresh context per test** — each test gets its own `this` object. Writes in one test never bleed into a sibling.
|
|
178
|
+
- **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.
|
|
179
|
+
- **`before()` assertions** — attributed to the first test in the module (matching QUnit's attribution model).
|
|
180
|
+
- **`after()` assertions** — attributed to the last test in the module.
|
|
181
|
+
- **Hook ordering** — `before`/`beforeEach` run FIFO; `afterEach`/`after` run LIFO, exactly as in QUnit.
|
|
182
|
+
|
|
183
|
+
> **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.
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Concurrency
|
|
188
|
+
|
|
189
|
+
Tests run **sequentially by default** — matching QUnit's browser behavior where tests run one at a time. You can enable concurrency by passing options to the underlying Node / Deno runner:
|
|
177
190
|
|
|
178
191
|
```js
|
|
179
192
|
import { module, test } from 'qunitx';
|
|
180
193
|
|
|
181
|
-
//
|
|
182
|
-
module('
|
|
194
|
+
// Enable parallel execution for this module (Node/Deno only)
|
|
195
|
+
module('Parallel suite', { concurrency: true }, (hooks) => {
|
|
183
196
|
test('first', (assert) => { assert.ok(true); });
|
|
184
197
|
test('second', (assert) => { assert.ok(true); });
|
|
185
198
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qunitx",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.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",
|
|
@@ -91,8 +91,8 @@
|
|
|
91
91
|
"devDependencies": {
|
|
92
92
|
"prettier": "^3.8.1",
|
|
93
93
|
"qunit": "^2.25.0",
|
|
94
|
-
"qunitx": "^0.
|
|
95
|
-
"qunitx-cli": "^0.5.
|
|
94
|
+
"qunitx": "^1.0.3",
|
|
95
|
+
"qunitx-cli": "^0.5.3"
|
|
96
96
|
},
|
|
97
97
|
"volta": {
|
|
98
98
|
"node": "24.14.0"
|
package/shims/deno/module.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { describe, beforeAll, afterAll } from "jsr:@std/testing/bdd";
|
|
2
2
|
import ModuleContext from '../shared/module-context.js';
|
|
3
3
|
|
|
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
|
-
// NOTE: QUnit expect() logic is buggy in nested modules
|
|
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
|
-
|
|
8
4
|
/**
|
|
9
5
|
* Defines a test module (suite) for Deno's BDD test runner.
|
|
10
6
|
*
|
|
@@ -37,40 +33,31 @@ export default function module(moduleName, runtimeOptions, moduleContent) {
|
|
|
37
33
|
const targetModuleContent = moduleContent ? moduleContent : runtimeOptions;
|
|
38
34
|
const moduleContext = new ModuleContext(moduleName);
|
|
39
35
|
|
|
40
|
-
describe(moduleName, {
|
|
36
|
+
describe(moduleName, { ...targetRuntimeOptions }, function () {
|
|
41
37
|
const beforeHooks = [];
|
|
42
38
|
const afterHooks = [];
|
|
43
39
|
|
|
44
40
|
beforeAll(async function () {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
? module.context.expectedAssertionCount
|
|
50
|
-
: result.expectedAssertionCount
|
|
51
|
-
});
|
|
52
|
-
}, { steps: [], expectedAssertionCount: undefined }));
|
|
41
|
+
// before() assertions are attributed to the first direct test only (matching QUnit's model).
|
|
42
|
+
// Tests inherit parent context via prototype chain, so no Object.assign needed.
|
|
43
|
+
const firstTest = moduleContext.tests[0];
|
|
44
|
+
const beforeAssert = firstTest ? firstTest.assert : moduleContext.assert;
|
|
53
45
|
|
|
54
46
|
for (const hook of beforeHooks) {
|
|
55
|
-
await hook.call(moduleContext.
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
for (let i = 0, len = moduleContext.tests.length; i < len; i++) {
|
|
59
|
-
Object.assign(moduleContext.tests[i], moduleContext.context, {
|
|
60
|
-
steps: moduleContext.context.steps,
|
|
61
|
-
totalExecutedAssertions: moduleContext.context.totalExecutedAssertions,
|
|
62
|
-
expectedAssertionCount: moduleContext.context.expectedAssertionCount,
|
|
63
|
-
});
|
|
47
|
+
await hook.call(moduleContext.userContext, beforeAssert);
|
|
64
48
|
}
|
|
65
49
|
});
|
|
50
|
+
|
|
66
51
|
afterAll(async () => {
|
|
67
52
|
for (const testContext of moduleContext.tests) {
|
|
68
53
|
await testContext.assert.waitForAsyncOps();
|
|
69
54
|
}
|
|
70
55
|
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
56
|
+
const lastTest = moduleContext.tests[moduleContext.tests.length - 1];
|
|
57
|
+
if (lastTest) {
|
|
58
|
+
for (let j = afterHooks.length - 1; j >= 0; j--) {
|
|
59
|
+
await afterHooks[j].call(lastTest.userContext, lastTest.assert);
|
|
60
|
+
}
|
|
74
61
|
}
|
|
75
62
|
|
|
76
63
|
for (let i = 0, len = moduleContext.tests.length; i < len; i++) {
|
|
@@ -78,7 +65,7 @@ export default function module(moduleName, runtimeOptions, moduleContent) {
|
|
|
78
65
|
}
|
|
79
66
|
});
|
|
80
67
|
|
|
81
|
-
targetModuleContent.call(moduleContext.
|
|
68
|
+
targetModuleContent.call(moduleContext.userContext, {
|
|
82
69
|
before(beforeFn) {
|
|
83
70
|
beforeHooks[beforeHooks.length] = beforeFn;
|
|
84
71
|
},
|
package/shims/deno/test.js
CHANGED
|
@@ -41,25 +41,30 @@ export default function test(testName, runtimeOptions, testContent) {
|
|
|
41
41
|
const targetTestContent = testContent ? testContent : runtimeOptions;
|
|
42
42
|
const context = new TestContext(testName, moduleContext);
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
// Each test gets a fresh plain object inheriting from the module's user context.
|
|
45
|
+
// This matches QUnit's prototype-chain model: before() sets props on the module context,
|
|
46
|
+
// tests inherit them, and each test's own writes don't pollute sibling tests.
|
|
47
|
+
const userContext = Object.create(moduleContext.userContext);
|
|
48
|
+
context.userContext = userContext;
|
|
49
|
+
|
|
50
|
+
it(testName, { ...targetRuntimeOptions }, async function () {
|
|
45
51
|
for (const module of context.module.moduleChain) {
|
|
46
52
|
for (const hook of module.beforeEachHooks) {
|
|
47
|
-
await hook.call(
|
|
53
|
+
await hook.call(userContext, context.assert);
|
|
48
54
|
}
|
|
49
55
|
}
|
|
50
56
|
|
|
51
|
-
const result = await targetTestContent.call(
|
|
57
|
+
const result = await targetTestContent.call(userContext, context.assert, { testName, options: runtimeOptions });
|
|
52
58
|
|
|
53
59
|
await context.assert.waitForAsyncOps();
|
|
54
60
|
|
|
55
61
|
for (let i = context.module.moduleChain.length - 1; i >= 0; i--) {
|
|
56
62
|
const module = context.module.moduleChain[i];
|
|
57
63
|
for (let j = module.afterEachHooks.length - 1; j >= 0; j--) {
|
|
58
|
-
await module.afterEachHooks[j].call(
|
|
64
|
+
await module.afterEachHooks[j].call(userContext, context.assert);
|
|
59
65
|
}
|
|
60
66
|
}
|
|
61
67
|
|
|
62
68
|
return result;
|
|
63
69
|
});
|
|
64
70
|
}
|
|
65
|
-
|
package/shims/node/module.js
CHANGED
|
@@ -1,49 +1,36 @@
|
|
|
1
1
|
import { describe, before as beforeAll, after as afterAll } from 'node:test';
|
|
2
2
|
import ModuleContext from '../shared/module-context.js';
|
|
3
3
|
|
|
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
|
-
// NOTE: QUnit expect() logic is buggy in nested modules
|
|
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
|
-
|
|
8
4
|
export default function module(moduleName, runtimeOptions, moduleContent) {
|
|
9
5
|
const targetRuntimeOptions = moduleContent ? runtimeOptions : {};
|
|
10
6
|
const targetModuleContent = moduleContent ? moduleContent : runtimeOptions;
|
|
11
7
|
const moduleContext = new ModuleContext(moduleName);
|
|
12
8
|
|
|
13
|
-
return describe(moduleName, {
|
|
9
|
+
return describe(moduleName, { ...targetRuntimeOptions }, function () {
|
|
14
10
|
const beforeHooks = [];
|
|
15
11
|
const afterHooks = [];
|
|
16
12
|
|
|
17
13
|
beforeAll(async function () {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
? module.context.expectedAssertionCount
|
|
23
|
-
: result.expectedAssertionCount
|
|
24
|
-
});
|
|
25
|
-
}, { steps: [], expectedAssertionCount: undefined }));
|
|
14
|
+
// before() assertions are attributed to the first direct test only (matching QUnit's model).
|
|
15
|
+
// Tests inherit parent context via prototype chain, so no Object.assign needed.
|
|
16
|
+
const firstTest = moduleContext.tests[0];
|
|
17
|
+
const beforeAssert = firstTest ? firstTest.assert : moduleContext.assert;
|
|
26
18
|
|
|
27
19
|
for (const hook of beforeHooks) {
|
|
28
|
-
await hook.call(moduleContext.
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
for (let i = 0, len = moduleContext.tests.length; i < len; i++) {
|
|
32
|
-
Object.assign(moduleContext.tests[i], moduleContext.context, {
|
|
33
|
-
steps: moduleContext.context.steps,
|
|
34
|
-
totalExecutedAssertions: moduleContext.context.totalExecutedAssertions,
|
|
35
|
-
expectedAssertionCount: moduleContext.context.expectedAssertionCount,
|
|
36
|
-
});
|
|
20
|
+
await hook.call(moduleContext.userContext, beforeAssert);
|
|
37
21
|
}
|
|
38
22
|
});
|
|
23
|
+
|
|
39
24
|
afterAll(async () => {
|
|
40
25
|
for (const testContext of moduleContext.tests) {
|
|
41
26
|
await testContext.assert.waitForAsyncOps();
|
|
42
27
|
}
|
|
43
28
|
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
29
|
+
const lastTest = moduleContext.tests[moduleContext.tests.length - 1];
|
|
30
|
+
if (lastTest) {
|
|
31
|
+
for (let j = afterHooks.length - 1; j >= 0; j--) {
|
|
32
|
+
await afterHooks[j].call(lastTest.userContext, lastTest.assert);
|
|
33
|
+
}
|
|
47
34
|
}
|
|
48
35
|
|
|
49
36
|
for (let i = 0, len = moduleContext.tests.length; i < len; i++) {
|
|
@@ -51,7 +38,7 @@ export default function module(moduleName, runtimeOptions, moduleContent) {
|
|
|
51
38
|
}
|
|
52
39
|
});
|
|
53
40
|
|
|
54
|
-
targetModuleContent.call(moduleContext.
|
|
41
|
+
targetModuleContent.call(moduleContext.userContext, {
|
|
55
42
|
before(beforeFn) {
|
|
56
43
|
beforeHooks[beforeHooks.length] = beforeFn;
|
|
57
44
|
},
|
package/shims/node/test.js
CHANGED
|
@@ -12,25 +12,30 @@ export default function test(testName, runtimeOptions, testContent) {
|
|
|
12
12
|
const targetTestContent = testContent ? testContent : runtimeOptions;
|
|
13
13
|
const context = new TestContext(testName, moduleContext);
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
// Each test gets a fresh plain object inheriting from the module's user context.
|
|
16
|
+
// This matches QUnit's prototype-chain model: before() sets props on the module context,
|
|
17
|
+
// tests inherit them, and each test's own writes don't pollute sibling tests.
|
|
18
|
+
const userContext = Object.create(moduleContext.userContext);
|
|
19
|
+
context.userContext = userContext;
|
|
20
|
+
|
|
21
|
+
return it(testName, { ...targetRuntimeOptions }, async function () {
|
|
16
22
|
for (const module of context.module.moduleChain) {
|
|
17
23
|
for (const hook of module.beforeEachHooks) {
|
|
18
|
-
await hook.call(
|
|
24
|
+
await hook.call(userContext, context.assert);
|
|
19
25
|
}
|
|
20
26
|
}
|
|
21
27
|
|
|
22
|
-
const result = await targetTestContent.call(
|
|
28
|
+
const result = await targetTestContent.call(userContext, context.assert, { testName, options: runtimeOptions });
|
|
23
29
|
|
|
24
30
|
await context.assert.waitForAsyncOps();
|
|
25
31
|
|
|
26
32
|
for (let i = context.module.moduleChain.length - 1; i >= 0; i--) {
|
|
27
33
|
const module = context.module.moduleChain[i];
|
|
28
34
|
for (let j = module.afterEachHooks.length - 1; j >= 0; j--) {
|
|
29
|
-
await module.afterEachHooks[j].call(
|
|
35
|
+
await module.afterEachHooks[j].call(userContext, context.assert);
|
|
30
36
|
}
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
return result;
|
|
34
40
|
});
|
|
35
41
|
}
|
|
36
|
-
|
package/shims/shared/assert.js
CHANGED
|
@@ -2,10 +2,6 @@ import '../../vendor/qunit.js';
|
|
|
2
2
|
import { objectValues, objectValuesSubset, validateExpectedExceptionArgs, validateException } from '../shared/index.js';
|
|
3
3
|
import util from 'node:util';
|
|
4
4
|
|
|
5
|
-
// More: contexts needed for timeout
|
|
6
|
-
// NOTE: Another approach for a global report Make this._assertions.set(this.currentTest, (this._assertions.get(this.currentTest) || 0) + 1); for pushResult
|
|
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
|
-
|
|
9
5
|
/**
|
|
10
6
|
* The assertion object passed to every test callback and lifecycle hook.
|
|
11
7
|
*
|
|
@@ -8,6 +8,7 @@ export default class ModuleContext {
|
|
|
8
8
|
return this.currentModuleChain[this.currentModuleChain.length - 1];
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
// Internal fallback assert for modules with no direct tests
|
|
11
12
|
context = new TestContext();
|
|
12
13
|
|
|
13
14
|
moduleChain = [];
|
|
@@ -24,6 +25,10 @@ export default class ModuleContext {
|
|
|
24
25
|
this.name = parentModule ? `${parentModule.name} > ${name}` : name;
|
|
25
26
|
this.assert = new ModuleContext.Assert(this);
|
|
26
27
|
|
|
28
|
+
// User context uses prototype chain from parent module for proper QUnit-style inheritance:
|
|
29
|
+
// parent before() sets props on parent userContext, child tests inherit via prototype.
|
|
30
|
+
this.userContext = parentModule ? Object.create(parentModule.userContext) : Object.create(null);
|
|
31
|
+
|
|
27
32
|
return Object.freeze(this);
|
|
28
33
|
}
|
|
29
34
|
}
|