qunitx 0.8.3 → 0.9.0

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "qunitx",
3
3
  "type": "module",
4
- "version": "0.8.3",
4
+ "version": "0.9.0",
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",
@@ -61,7 +61,7 @@
61
61
  "ts-node": ">=10.7.0"
62
62
  },
63
63
  "volta": {
64
- "node": "20.4.0"
64
+ "node": "20.5.0"
65
65
  },
66
66
  "prettier": {
67
67
  "printWidth": 100,
@@ -9,39 +9,79 @@ export class AssertionError extends DenoAssertionError {
9
9
  }
10
10
  }
11
11
 
12
- // NOTE: Maybe do the expect, steps in some object, and also do timeout and async(?)
13
- export default {
14
- _steps: [],
15
- timeout() {
16
- return true; // NOTE: NOT implemented
17
- },
18
- step(value = '') {
19
- this._steps.push(value);
20
- },
21
- verifySteps(steps, message = 'Verify steps failed!') {
22
- const result = this.deepEqual(this._steps, steps, message);
12
+ export default class Assert {
13
+ AssertionError = AssertionError
14
+
15
+ #asyncOps = [];
16
+
17
+ constructor(module, test) {
18
+ this.test = test || module;
19
+ }
20
+ _incrementAssertionCount() {
21
+ this.test.totalExecutedAssertions++;
22
+ }
23
+ timeout(number) {
24
+ if (!Number.isInteger(number) || number < 0) {
25
+ throw new Error('assert.timeout() expects a positive integer.');
26
+ }
27
+
28
+ this.test.timeout = number;
29
+ }
30
+ step(message) {
31
+ let assertionMessage = message;
32
+ let result = !!message;
33
+
34
+ this.test.steps.push(message);
35
+
36
+ if (typeof message === 'undefined' || message === '') {
37
+ assertionMessage = 'You must provide a message to assert.step';
38
+ } else if (typeof message !== 'string') {
39
+ assertionMessage = 'You must provide a string value to assert.step';
40
+ result = false;
41
+ }
23
42
 
24
- this._steps.length = 0;
43
+ this.pushResult({
44
+ result,
45
+ message: assertionMessage
46
+ });
47
+ }
48
+ verifySteps(steps, message = 'Verify steps failed!') {
49
+ this.deepEqual(this.test.steps, steps, message);
50
+ this.test.steps.length = 0;
51
+ }
52
+ expect(number) {
53
+ if (!Number.isInteger(number) || number < 0) {
54
+ throw new Error('assert.expect() expects a positive integer.');
55
+ }
25
56
 
26
- return result;
27
- },
28
- expect() {
29
- return () => {}; // NOTE: NOT implemented
30
- },
57
+ this.test.expectedAssertionCount = number;
58
+ }
31
59
  async() {
32
- return () => {}; // NOTE: noop, node should have sanitizeResources
33
- },
60
+ let resolveFn;
61
+ let done = new Promise(resolve => { resolveFn = resolve; });
62
+
63
+ this.#asyncOps.push(done);
64
+
65
+ return () => { resolveFn(); };
66
+ }
67
+ async waitForAsyncOps() {
68
+ return Promise.all(this.#asyncOps);
69
+ }
34
70
  pushResult(resultInfo = {}) {
35
- if (!result) {
71
+ this._incrementAssertionCount();
72
+ if (!resultInfo.result) {
36
73
  throw new AssertionError({
37
74
  actual: resultInfo.actual,
38
75
  expected: resultInfo.expected,
39
- message: result.Infomessage || 'Custom assertion failed!',
76
+ message: resultInfo.message || 'Custom assertion failed!',
40
77
  stackStartFn: this.pushResult,
41
78
  });
42
79
  }
43
- },
80
+
81
+ return this;
82
+ }
44
83
  ok(state, message) {
84
+ this._incrementAssertionCount();
45
85
  if (!state) {
46
86
  throw new AssertionError({
47
87
  actual: state,
@@ -50,8 +90,9 @@ export default {
50
90
  stackStartFn: this.ok,
51
91
  });
52
92
  }
53
- },
93
+ }
54
94
  notOk(state, message) {
95
+ this._incrementAssertionCount();
55
96
  if (state) {
56
97
  throw new AssertionError({
57
98
  actual: state,
@@ -60,8 +101,9 @@ export default {
60
101
  stackStartFn: this.notOk,
61
102
  });
62
103
  }
63
- },
104
+ }
64
105
  true(state, message) {
106
+ this._incrementAssertionCount();
65
107
  if (state !== true) {
66
108
  throw new AssertionError({
67
109
  actual: state,
@@ -70,8 +112,9 @@ export default {
70
112
  stackStartFn: this.true,
71
113
  });
72
114
  }
73
- },
115
+ }
74
116
  false(state, message) {
117
+ this._incrementAssertionCount();
75
118
  if (state !== false) {
76
119
  throw new AssertionError({
77
120
  actual: state,
@@ -80,8 +123,9 @@ export default {
80
123
  stackStartFn: this.false,
81
124
  });
82
125
  }
83
- },
126
+ }
84
127
  equal(actual, expected, message) {
128
+ this._incrementAssertionCount();
85
129
  if (actual != expected) {
86
130
  throw new AssertionError({
87
131
  actual,
@@ -91,8 +135,9 @@ export default {
91
135
  stackStartFn: this.equal,
92
136
  });
93
137
  }
94
- },
138
+ }
95
139
  notEqual(actual, expected, message) {
140
+ this._incrementAssertionCount();
96
141
  if (actual == expected) {
97
142
  throw new AssertionError({
98
143
  actual,
@@ -102,11 +147,12 @@ export default {
102
147
  stackStartFn: this.notEqual,
103
148
  });
104
149
  }
105
- },
150
+ }
106
151
  propEqual(actual, expected, message) {
152
+ this._incrementAssertionCount();
107
153
  let targetActual = objectValues(actual);
108
154
  let targetExpected = objectValues(expected);
109
- if (!window.QUnit.equiv(targetActual, targetExpected)) {
155
+ if (!QUnit.equiv(targetActual, targetExpected)) {
110
156
  throw new AssertionError({
111
157
  actual: targetActual,
112
158
  expected: targetExpected,
@@ -114,11 +160,12 @@ export default {
114
160
  stackStartFn: this.propEqual,
115
161
  });
116
162
  }
117
- },
163
+ }
118
164
  notPropEqual(actual, expected, message) {
165
+ this._incrementAssertionCount();
119
166
  let targetActual = objectValues(actual);
120
167
  let targetExpected = objectValues(expected);
121
- if (window.QUnit.equiv(targetActual, targetExpected)) {
168
+ if (QUnit.equiv(targetActual, targetExpected)) {
122
169
  throw new AssertionError({
123
170
  actual: targetActual,
124
171
  expected: targetExpected,
@@ -126,11 +173,12 @@ export default {
126
173
  stackStartFn: this.notPropEqual,
127
174
  });
128
175
  }
129
- },
176
+ }
130
177
  propContains(actual, expected, message) {
178
+ this._incrementAssertionCount();
131
179
  let targetActual = objectValuesSubset(actual, expected);
132
180
  let targetExpected = objectValues(expected, false);
133
- if (!window.QUnit.equiv(targetActual, targetExpected)) {
181
+ if (!QUnit.equiv(targetActual, targetExpected)) {
134
182
  throw new AssertionError({
135
183
  actual: targetActual,
136
184
  expected: targetExpected,
@@ -138,11 +186,12 @@ export default {
138
186
  stackStartFn: this.propContains,
139
187
  });
140
188
  }
141
- },
189
+ }
142
190
  notPropContains(actual, expected, message) {
191
+ this._incrementAssertionCount();
143
192
  let targetActual = objectValuesSubset(actual, expected);
144
193
  let targetExpected = objectValues(expected);
145
- if (window.QUnit.equiv(targetActual, targetExpected)) {
194
+ if (QUnit.equiv(targetActual, targetExpected)) {
146
195
  throw new AssertionError({
147
196
  actual: targetActual,
148
197
  expected: targetExpected,
@@ -150,9 +199,10 @@ export default {
150
199
  stackStartFn: this.notPropContains,
151
200
  });
152
201
  }
153
- },
202
+ }
154
203
  deepEqual(actual, expected, message) {
155
- if (!window.QUnit.equiv(actual, expected)) {
204
+ this._incrementAssertionCount();
205
+ if (!QUnit.equiv(actual, expected)) {
156
206
  throw new AssertionError({
157
207
  actual,
158
208
  expected,
@@ -161,9 +211,10 @@ export default {
161
211
  stackStartFn: this.deepEqual,
162
212
  });
163
213
  }
164
- },
214
+ }
165
215
  notDeepEqual(actual, expected, message) {
166
- if (window.QUnit.equiv(actual, expected)) {
216
+ this._incrementAssertionCount();
217
+ if (QUnit.equiv(actual, expected)) {
167
218
  throw new AssertionError({
168
219
  actual,
169
220
  expected,
@@ -172,8 +223,9 @@ export default {
172
223
  stackStartFn: this.notDeepEqual,
173
224
  });
174
225
  }
175
- },
226
+ }
176
227
  strictEqual(actual, expected, message) {
228
+ this._incrementAssertionCount();
177
229
  if (actual !== expected) {
178
230
  throw new AssertionError({
179
231
  actual,
@@ -183,8 +235,9 @@ export default {
183
235
  stackStartFn: this.strictEqual,
184
236
  });
185
237
  }
186
- },
238
+ }
187
239
  notStrictEqual(actual, expected, message) {
240
+ this._incrementAssertionCount();
188
241
  if (actual === expected) {
189
242
  throw new AssertionError({
190
243
  actual,
@@ -194,8 +247,9 @@ export default {
194
247
  stackStartFn: this.notStrictEqual,
195
248
  });
196
249
  }
197
- },
250
+ }
198
251
  throws(blockFn, expectedInput, assertionMessage) {
252
+ this?._incrementAssertionCount();
199
253
  let [expected, message] = validateExpectedExceptionArgs(expectedInput, assertionMessage, 'rejects');
200
254
  if (typeof blockFn !== 'function') {
201
255
  throw new AssertionError({
@@ -228,8 +282,9 @@ export default {
228
282
  message: 'Function passed to `assert.throws` did not throw an exception!',
229
283
  stackStartFn: this.throws,
230
284
  });
231
- },
285
+ }
232
286
  async rejects(promise, expectedInput, assertionMessage) {
287
+ this._incrementAssertionCount();
233
288
  let [expected, message] = validateExpectedExceptionArgs(expectedInput, assertionMessage, 'rejects');
234
289
  let then = promise && promise.then;
235
290
  if (typeof then !== 'function') {
@@ -276,4 +331,3 @@ ${inspect(expected)}`
276
331
  function inspect(value) {
277
332
  return util.inspect(value, { depth: 10, colors: true, compact: false });
278
333
  }
279
-
@@ -1,48 +1,238 @@
1
1
  import {
2
- afterEach,
3
- beforeEach,
4
2
  beforeAll,
5
3
  afterAll,
6
4
  describe,
7
5
  it,
8
6
  } from "https://deno.land/std@0.192.0/testing/bdd.ts";
9
- import assert from './assert.js';
7
+ import Assert from './assert.js';
10
8
 
11
- // TODO: TEST beforeEach, before, afterEach, after, currently not sure if they work!
12
- export const module = async function(moduleName, runtimeOptions, moduleContent) {
13
- let targetRuntimeOptions = moduleContent ? Object.assign(runtimeOptions, { name: moduleName }) : { name: moduleName };
14
- let targetModuleContent = moduleContent ? moduleName : runtimeOptions;
9
+ class TestContext {
10
+ name;
15
11
 
16
- return describe(assignDefaultValues(targetRuntimeOptions, { concurrency: true }), async function() {
17
- return await targetModuleContent({ before: beforeAll, after: afterAll, beforeEach, afterEach }, {
18
- moduleName,
19
- options: runtimeOptions
12
+ #module;
13
+ get module() {
14
+ return this.#module;
15
+ }
16
+ set module(value) {
17
+ this.#module = value;
18
+ }
19
+
20
+ #assert;
21
+ get assert() {
22
+ return this.#assert;
23
+ }
24
+ set assert(value) {
25
+ this.#assert = value;
26
+ }
27
+
28
+ #timeout;
29
+ get timeout() {
30
+ return this.#timeout;
31
+ }
32
+ set timeout(value) {
33
+ this.#timeout = value;
34
+ }
35
+
36
+ #steps = [];
37
+ get steps() {
38
+ return this.#steps;
39
+ }
40
+ set steps(value) {
41
+ this.#steps = value;
42
+ }
43
+
44
+ #expectedAssertionCount;
45
+ get expectedAssertionCount() {
46
+ return this.#expectedAssertionCount;
47
+ }
48
+ set expectedAssertionCount(value) {
49
+ this.#expectedAssertionCount = value;
50
+ }
51
+
52
+ #totalExecutedAssertions = 0;
53
+ get totalExecutedAssertions() {
54
+ return this.#totalExecutedAssertions;
55
+ }
56
+ set totalExecutedAssertions(value) {
57
+ this.#totalExecutedAssertions = value;
58
+ }
59
+
60
+ constructor(name, moduleContext) {
61
+ if (moduleContext) {
62
+ this.name = `${moduleContext.name} | ${name}`;
63
+ this.module = moduleContext;
64
+ this.module.tests.push(this);
65
+ this.assert = new Assert(moduleContext, this);
66
+ }
67
+ }
68
+
69
+ finish() {
70
+ if (this.totalExecutedAssertions === 0) {
71
+ this.assert.pushResult({
72
+ result: false,
73
+ actual: this.totalExecutedAssertions,
74
+ expected: '> 0',
75
+ message: `Expected at least one assertion to be run for test: ${this.name}`,
76
+ });
77
+ } else if (this.steps.length > 0) {
78
+ this.assert.pushResult({
79
+ result: false,
80
+ actual: this.steps,
81
+ expected: [],
82
+ message: `Expected assert.verifySteps() to be called before end of test after using assert.step(). Unverified steps: ${this.steps.join(', ')}`,
83
+ });
84
+ } else if (this.expectedAssertionCount && this.expectedAssertionCount !== this.totalExecutedAssertions) {
85
+ this.assert.pushResult({
86
+ result: false,
87
+ actual: this.totalExecutedAssertions,
88
+ expected: this.expectedAssertionCount,
89
+ message: `Expected ${this.expectedAssertionCount} assertions, but ${this.totalExecutedAssertions} were run for test: ${this.name}`,
90
+ });
91
+ }
92
+ }
93
+ }
94
+
95
+ class ModuleContext extends TestContext {
96
+ static currentModuleChain = [];
97
+
98
+ static get lastModule() {
99
+ return this.currentModuleChain[this.currentModuleChain.length - 1];
100
+ }
101
+
102
+ #tests = [];
103
+ get tests() {
104
+ return this.#tests;
105
+ }
106
+
107
+ #beforeEachHooks = [];
108
+ get beforeEachHooks() {
109
+ return this.#beforeEachHooks;
110
+ }
111
+
112
+ #afterEachHooks = [];
113
+ get afterEachHooks() {
114
+ return this.#afterEachHooks;
115
+ }
116
+
117
+ #moduleChain = [];
118
+ get moduleChain() {
119
+ return this.#moduleChain;
120
+ }
121
+ set moduleChain(value) {
122
+ this.#moduleChain = value;
123
+ }
124
+
125
+ constructor(name) {
126
+ super(name);
127
+
128
+ let parentModule = ModuleContext.currentModuleChain[ModuleContext.currentModuleChain.length - 1];
129
+
130
+ ModuleContext.currentModuleChain.push(this);
131
+
132
+ this.moduleChain = ModuleContext.currentModuleChain.slice(0);
133
+ this.name = parentModule ? `${parentModule.name} > ${name}` : name;
134
+ this.assert = new Assert(this);
135
+ }
136
+ }
137
+
138
+ export const module = (moduleName, runtimeOptions, moduleContent) => {
139
+ let targetRuntimeOptions = moduleContent ? runtimeOptions : {};
140
+ let targetModuleContent = moduleContent ? moduleContent : runtimeOptions;
141
+ let moduleContext = new ModuleContext(moduleName);
142
+
143
+ return describe(moduleName, { concurrency: true, ...targetRuntimeOptions }, async function () {
144
+ let beforeHooks = [];
145
+ let afterHooks = [];
146
+
147
+ beforeAll(async function () {
148
+ Object.assign(moduleContext, moduleContext.moduleChain.reduce((result, module) => {
149
+ const { name, ...moduleWithoutName } = module;
150
+
151
+ return Object.assign(result, moduleWithoutName, {
152
+ steps: result.steps.concat(module.steps),
153
+ expectedAssertionCount: module.expectedAssertionCount
154
+ ? module.expectedAssertionCount
155
+ : result.expectedAssertionCount
156
+ });
157
+ }, { steps: [], expectedAssertionCount: undefined }));
158
+
159
+ for (let hook of beforeHooks) {
160
+ await hook.call(moduleContext, moduleContext.assert);
161
+ }
162
+
163
+ moduleContext.tests.forEach((testContext) => {
164
+ const { name, ...moduleContextWithoutName } = moduleContext;
165
+
166
+ Object.assign(testContext, moduleContextWithoutName, {
167
+ steps: moduleContext.steps,
168
+ totalExecutedAssertions: moduleContext.totalExecutedAssertions,
169
+ expectedAssertionCount: moduleContext.expectedAssertionCount,
170
+ });
171
+ });
172
+ });
173
+ afterAll(async () => {
174
+ for (const assert of moduleContext.tests.map(testContext => testContext.assert)) {
175
+ await assert.waitForAsyncOps();
176
+ }
177
+
178
+ let targetContext = moduleContext.tests[moduleContext.tests.length - 1];
179
+ for (let j = afterHooks.length - 1; j >= 0; j--) {
180
+ await afterHooks[j].call(targetContext, targetContext.assert);
181
+ }
182
+
183
+ moduleContext.tests.forEach(testContext => testContext.finish());
20
184
  });
185
+
186
+ targetModuleContent.call(moduleContext, {
187
+ before(beforeFn) {
188
+ return beforeHooks.push(beforeFn);
189
+ },
190
+ beforeEach(beforeEachFn) {
191
+ return moduleContext.beforeEachHooks.push(beforeEachFn);
192
+ },
193
+ afterEach(afterEachFn) {
194
+ return moduleContext.afterEachHooks.push(afterEachFn);
195
+ },
196
+ after(afterFn) {
197
+ return afterHooks.push(afterFn);
198
+ }
199
+ }, { moduleName, options: runtimeOptions });
200
+
201
+ ModuleContext.currentModuleChain.pop();
21
202
  });
22
203
  }
23
204
 
24
- export const test = async function(testName, runtimeOptions, testContent) {
25
- let targetRuntimeOptions = testContent ? Object.assign(runtimeOptions, { name: testName }) : { name: testName };
205
+ export const test = (testName, runtimeOptions, testContent) => {
206
+ let moduleContext = ModuleContext.lastModule;
207
+ if (!moduleContext) {
208
+ throw new Error(`Test '${testName}' called outside of module context.`);
209
+ }
210
+
211
+ let targetRuntimeOptions = testContent ? runtimeOptions : {};
26
212
  let targetTestContent = testContent ? testContent : runtimeOptions;
213
+ let context = new TestContext(testName, moduleContext);
27
214
 
28
- return it(targetRuntimeOptions, async function() {
29
- let metadata = { testName, options: targetRuntimeOptions, expectedTestCount: undefined };
30
- return await targetTestContent(assert, metadata);
215
+ return it(testName, { concurrency: true, ...targetRuntimeOptions }, async function () {
216
+ let result;
217
+ for (let module of context.module.moduleChain) {
218
+ for (let hook of module.beforeEachHooks) {
219
+ await hook.call(context, context.assert);
220
+ }
221
+ }
31
222
 
32
- if (expectedTestCount) {
223
+ result = await targetTestContent.call(context, context.assert, { testName, options: runtimeOptions });
33
224
 
34
- }
35
- });
36
- }
225
+ await context.assert.waitForAsyncOps();
37
226
 
38
- function assignDefaultValues(options, defaultValues) {
39
- for (let key in defaultValues) {
40
- if (options[key] === undefined) {
41
- options[key] = defaultValues[key];
227
+ for (let i = context.module.moduleChain.length - 1; i >= 0; i--) {
228
+ let module = context.module.moduleChain[i];
229
+ for (let j = module.afterEachHooks.length - 1; j >= 0; j--) {
230
+ await module.afterEachHooks[j].call(context, context.assert);
231
+ }
42
232
  }
43
- }
44
233
 
45
- return options;
234
+ return result;
235
+ });
46
236
  }
47
237
 
48
- export default { module, test, assert };
238
+ export default { module, test, config: {} };
@@ -1,41 +1,88 @@
1
1
  import QUnit from '../../vendor/qunit.js';
2
2
  import { objectValues, objectValuesSubset, validateExpectedExceptionArgs, validateException } from '../shared/index.js';
3
- import assert, { AssertionError } from 'node:assert';
3
+ import assert, { AssertionError as _AssertionError } from 'node:assert';
4
4
  import util from 'node:util';
5
5
 
6
- // NOTE: Maybe do the expect, steps in some object, and also do timeout and async(?)
7
- export default {
8
- _steps: [],
9
- timeout() {
10
- return true; // NOTE: NOT implemented
11
- },
12
- step(value = '') {
13
- this._steps.push(value);
14
- },
15
- verifySteps(steps, message = 'Verify steps failed!') {
16
- const result = this.deepEqual(this._steps, steps, message);
6
+ export const AssertionError = _AssertionError;
7
+
8
+ // More: contexts needed for timeout
9
+ // NOTE: QUnit API provides assert on hooks, which makes it hard to make it concurrent
10
+ // NOTE: Another approach for a global report Make this._assertions.set(this.currentTest, (this._assertions.get(this.currentTest) || 0) + 1); for pushResult
11
+ // 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.
12
+
13
+ export default class Assert {
14
+ AssertionError = _AssertionError;
15
+
16
+ #asyncOps = [];
17
+
18
+ constructor(module, test) {
19
+ this.test = test || module;
20
+ }
21
+ _incrementAssertionCount() {
22
+ this.test.totalExecutedAssertions++;
23
+ }
24
+ timeout(number) {
25
+ if (!Number.isInteger(number) || number < 0) {
26
+ throw new Error('assert.timeout() expects a positive integer.');
27
+ }
28
+
29
+ this.test.timeout = number;
30
+ }
31
+ step(message) {
32
+ let assertionMessage = message;
33
+ let result = !!message;
17
34
 
18
- this._steps.length = 0;
35
+ this.test.steps.push(message);
19
36
 
20
- return result;
21
- },
22
- expect() {
23
- return () => {}; // NOTE: NOT implemented
24
- },
37
+ if (typeof message === 'undefined' || message === '') {
38
+ assertionMessage = 'You must provide a message to assert.step';
39
+ } else if (typeof message !== 'string') {
40
+ assertionMessage = 'You must provide a string value to assert.step';
41
+ result = false;
42
+ }
43
+
44
+ this.pushResult({
45
+ result,
46
+ message: assertionMessage
47
+ });
48
+ }
49
+ verifySteps(steps, message = 'Verify steps failed!') {
50
+ this.deepEqual(this.test.steps, steps, message);
51
+ this.test.steps.length = 0;
52
+ }
53
+ expect(number) {
54
+ if (!Number.isInteger(number) || number < 0) {
55
+ throw new Error('assert.expect() expects a positive integer.');
56
+ }
57
+
58
+ this.test.expectedAssertionCount = number;
59
+ }
25
60
  async() {
26
- return () => {}; // NOTE: noop, node should have sanitizeResources
27
- },
61
+ let resolveFn;
62
+ let done = new Promise(resolve => { resolveFn = resolve; });
63
+
64
+ this.#asyncOps.push(done);
65
+
66
+ return () => { resolveFn(); };
67
+ }
68
+ async waitForAsyncOps() {
69
+ return Promise.all(this.#asyncOps);
70
+ }
28
71
  pushResult(resultInfo = {}) {
29
- if (!result) {
72
+ this._incrementAssertionCount();
73
+ if (!resultInfo.result) {
30
74
  throw new AssertionError({
31
75
  actual: resultInfo.actual,
32
76
  expected: resultInfo.expected,
33
- message: result.Infomessage || 'Custom assertion failed!',
77
+ message: resultInfo.message || 'Custom assertion failed!',
34
78
  stackStartFn: this.pushResult,
35
79
  });
36
80
  }
37
- },
81
+
82
+ return this;
83
+ }
38
84
  ok(state, message) {
85
+ this._incrementAssertionCount();
39
86
  if (!state) {
40
87
  throw new AssertionError({
41
88
  actual: state,
@@ -44,8 +91,9 @@ export default {
44
91
  stackStartFn: this.ok,
45
92
  });
46
93
  }
47
- },
94
+ }
48
95
  notOk(state, message) {
96
+ this._incrementAssertionCount();
49
97
  if (state) {
50
98
  throw new AssertionError({
51
99
  actual: state,
@@ -54,8 +102,9 @@ export default {
54
102
  stackStartFn: this.notOk,
55
103
  });
56
104
  }
57
- },
105
+ }
58
106
  true(state, message) {
107
+ this._incrementAssertionCount();
59
108
  if (state !== true) {
60
109
  throw new AssertionError({
61
110
  actual: state,
@@ -64,8 +113,9 @@ export default {
64
113
  stackStartFn: this.true,
65
114
  });
66
115
  }
67
- },
116
+ }
68
117
  false(state, message) {
118
+ this._incrementAssertionCount();
69
119
  if (state !== false) {
70
120
  throw new AssertionError({
71
121
  actual: state,
@@ -74,8 +124,9 @@ export default {
74
124
  stackStartFn: this.false,
75
125
  });
76
126
  }
77
- },
127
+ }
78
128
  equal(actual, expected, message) {
129
+ this._incrementAssertionCount();
79
130
  if (actual != expected) {
80
131
  throw new AssertionError({
81
132
  actual,
@@ -85,8 +136,9 @@ export default {
85
136
  stackStartFn: this.equal,
86
137
  });
87
138
  }
88
- },
139
+ }
89
140
  notEqual(actual, expected, message) {
141
+ this._incrementAssertionCount();
90
142
  if (actual == expected) {
91
143
  throw new AssertionError({
92
144
  actual,
@@ -96,8 +148,9 @@ export default {
96
148
  stackStartFn: this.notEqual,
97
149
  });
98
150
  }
99
- },
151
+ }
100
152
  propEqual(actual, expected, message) {
153
+ this._incrementAssertionCount();
101
154
  let targetActual = objectValues(actual);
102
155
  let targetExpected = objectValues(expected);
103
156
  if (!QUnit.equiv(targetActual, targetExpected)) {
@@ -108,8 +161,9 @@ export default {
108
161
  stackStartFn: this.propEqual,
109
162
  });
110
163
  }
111
- },
164
+ }
112
165
  notPropEqual(actual, expected, message) {
166
+ this._incrementAssertionCount();
113
167
  let targetActual = objectValues(actual);
114
168
  let targetExpected = objectValues(expected);
115
169
  if (QUnit.equiv(targetActual, targetExpected)) {
@@ -120,8 +174,9 @@ export default {
120
174
  stackStartFn: this.notPropEqual,
121
175
  });
122
176
  }
123
- },
177
+ }
124
178
  propContains(actual, expected, message) {
179
+ this._incrementAssertionCount();
125
180
  let targetActual = objectValuesSubset(actual, expected);
126
181
  let targetExpected = objectValues(expected, false);
127
182
  if (!QUnit.equiv(targetActual, targetExpected)) {
@@ -132,8 +187,9 @@ export default {
132
187
  stackStartFn: this.propContains,
133
188
  });
134
189
  }
135
- },
190
+ }
136
191
  notPropContains(actual, expected, message) {
192
+ this._incrementAssertionCount();
137
193
  let targetActual = objectValuesSubset(actual, expected);
138
194
  let targetExpected = objectValues(expected);
139
195
  if (QUnit.equiv(targetActual, targetExpected)) {
@@ -144,8 +200,9 @@ export default {
144
200
  stackStartFn: this.notPropContains,
145
201
  });
146
202
  }
147
- },
203
+ }
148
204
  deepEqual(actual, expected, message) {
205
+ this._incrementAssertionCount();
149
206
  if (!QUnit.equiv(actual, expected)) {
150
207
  throw new AssertionError({
151
208
  actual,
@@ -155,8 +212,9 @@ export default {
155
212
  stackStartFn: this.deepEqual,
156
213
  });
157
214
  }
158
- },
215
+ }
159
216
  notDeepEqual(actual, expected, message) {
217
+ this._incrementAssertionCount();
160
218
  if (QUnit.equiv(actual, expected)) {
161
219
  throw new AssertionError({
162
220
  actual,
@@ -166,8 +224,9 @@ export default {
166
224
  stackStartFn: this.notDeepEqual,
167
225
  });
168
226
  }
169
- },
227
+ }
170
228
  strictEqual(actual, expected, message) {
229
+ this._incrementAssertionCount();
171
230
  if (actual !== expected) {
172
231
  throw new AssertionError({
173
232
  actual,
@@ -177,8 +236,9 @@ export default {
177
236
  stackStartFn: this.strictEqual,
178
237
  });
179
238
  }
180
- },
239
+ }
181
240
  notStrictEqual(actual, expected, message) {
241
+ this._incrementAssertionCount();
182
242
  if (actual === expected) {
183
243
  throw new AssertionError({
184
244
  actual,
@@ -188,8 +248,9 @@ export default {
188
248
  stackStartFn: this.notStrictEqual,
189
249
  });
190
250
  }
191
- },
251
+ }
192
252
  throws(blockFn, expectedInput, assertionMessage) {
253
+ this?._incrementAssertionCount();
193
254
  let [expected, message] = validateExpectedExceptionArgs(expectedInput, assertionMessage, 'rejects');
194
255
  if (typeof blockFn !== 'function') {
195
256
  throw new AssertionError({
@@ -222,8 +283,9 @@ export default {
222
283
  message: 'Function passed to `assert.throws` did not throw an exception!',
223
284
  stackStartFn: this.throws,
224
285
  });
225
- },
286
+ }
226
287
  async rejects(promise, expectedInput, assertionMessage) {
288
+ this._incrementAssertionCount();
227
289
  let [expected, message] = validateExpectedExceptionArgs(expectedInput, assertionMessage, 'rejects');
228
290
  let then = promise && promise.then;
229
291
  if (typeof then !== 'function') {
@@ -1,75 +1,237 @@
1
- import { run, describe, it, before, after, beforeEach, afterEach } from 'node:test';
2
- import assert from './assert.js';
1
+ import { describe, it, before as beforeAll, after as afterAll } from 'node:test';
2
+ import Assert from './assert.js';
3
3
 
4
- export const module = async function(moduleName, runtimeOptions, moduleContent) {
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
+ class TestContext {
9
+ name;
10
+
11
+ #module;
12
+ get module() {
13
+ return this.#module;
14
+ }
15
+ set module(value) {
16
+ this.#module = value;
17
+ }
18
+
19
+ #assert;
20
+ get assert() {
21
+ return this.#assert;
22
+ }
23
+ set assert(value) {
24
+ this.#assert = value;
25
+ }
26
+
27
+ #timeout;
28
+ get timeout() {
29
+ return this.#timeout;
30
+ }
31
+ set timeout(value) {
32
+ this.#timeout = value;
33
+ }
34
+
35
+ #steps = [];
36
+ get steps() {
37
+ return this.#steps;
38
+ }
39
+ set steps(value) {
40
+ this.#steps = value;
41
+ }
42
+
43
+ #expectedAssertionCount;
44
+ get expectedAssertionCount() {
45
+ return this.#expectedAssertionCount;
46
+ }
47
+ set expectedAssertionCount(value) {
48
+ this.#expectedAssertionCount = value;
49
+ }
50
+
51
+ #totalExecutedAssertions = 0;
52
+ get totalExecutedAssertions() {
53
+ return this.#totalExecutedAssertions;
54
+ }
55
+ set totalExecutedAssertions(value) {
56
+ this.#totalExecutedAssertions = value;
57
+ }
58
+
59
+ constructor(name, moduleContext) {
60
+ if (moduleContext) {
61
+ this.name = `${moduleContext.name} | ${name}`;
62
+ this.module = moduleContext;
63
+ this.module.tests.push(this);
64
+ this.assert = new Assert(moduleContext, this);
65
+ }
66
+ }
67
+
68
+ finish() {
69
+ if (this.totalExecutedAssertions === 0) {
70
+ this.assert.pushResult({
71
+ result: false,
72
+ actual: this.totalExecutedAssertions,
73
+ expected: '> 0',
74
+ message: `Expected at least one assertion to be run for test: ${this.name}`,
75
+ });
76
+ } else if (this.steps.length > 0) {
77
+ this.assert.pushResult({
78
+ result: false,
79
+ actual: this.steps,
80
+ expected: [],
81
+ message: `Expected assert.verifySteps() to be called before end of test after using assert.step(). Unverified steps: ${this.steps.join(', ')}`,
82
+ });
83
+ } else if (this.expectedAssertionCount && this.expectedAssertionCount !== this.totalExecutedAssertions) {
84
+ this.assert.pushResult({
85
+ result: false,
86
+ actual: this.totalExecutedAssertions,
87
+ expected: this.expectedAssertionCount,
88
+ message: `Expected ${this.expectedAssertionCount} assertions, but ${this.totalExecutedAssertions} were run for test: ${this.name}`,
89
+ });
90
+ }
91
+ }
92
+ }
93
+
94
+ class ModuleContext extends TestContext {
95
+ static currentModuleChain = [];
96
+
97
+ static get lastModule() {
98
+ return this.currentModuleChain[this.currentModuleChain.length - 1];
99
+ }
100
+
101
+ #tests = [];
102
+ get tests() {
103
+ return this.#tests;
104
+ }
105
+
106
+ #beforeEachHooks = [];
107
+ get beforeEachHooks() {
108
+ return this.#beforeEachHooks;
109
+ }
110
+
111
+ #afterEachHooks = [];
112
+ get afterEachHooks() {
113
+ return this.#afterEachHooks;
114
+ }
115
+
116
+ #moduleChain = [];
117
+ get moduleChain() {
118
+ return this.#moduleChain;
119
+ }
120
+ set moduleChain(value) {
121
+ this.#moduleChain = value;
122
+ }
123
+
124
+ constructor(name) {
125
+ super(name);
126
+
127
+ let parentModule = ModuleContext.currentModuleChain[ModuleContext.currentModuleChain.length - 1];
128
+
129
+ ModuleContext.currentModuleChain.push(this);
130
+
131
+ this.moduleChain = ModuleContext.currentModuleChain.slice(0);
132
+ this.name = parentModule ? `${parentModule.name} > ${name}` : name;
133
+ this.assert = new Assert(this);
134
+ }
135
+ }
136
+
137
+ export const module = (moduleName, runtimeOptions, moduleContent) => {
5
138
  let targetRuntimeOptions = moduleContent ? runtimeOptions : {};
6
139
  let targetModuleContent = moduleContent ? moduleContent : runtimeOptions;
140
+ let moduleContext = new ModuleContext(moduleName);
141
+
142
+ return describe(moduleName, { concurrency: true, ...targetRuntimeOptions }, async function () {
143
+ let beforeHooks = [];
144
+ let afterHooks = [];
145
+
146
+ beforeAll(async function () {
147
+ Object.assign(moduleContext, moduleContext.moduleChain.reduce((result, module) => {
148
+ const { name, ...moduleWithoutName } = module;
149
+
150
+ return Object.assign(result, moduleWithoutName, {
151
+ steps: result.steps.concat(module.steps),
152
+ expectedAssertionCount: module.expectedAssertionCount
153
+ ? module.expectedAssertionCount
154
+ : result.expectedAssertionCount
155
+ });
156
+ }, { steps: [], expectedAssertionCount: undefined }));
157
+
158
+ for (let hook of beforeHooks) {
159
+ await hook.call(moduleContext, moduleContext.assert);
160
+ }
7
161
 
8
- return describe(moduleName, assignDefaultValues(targetRuntimeOptions, { concurrency: true }), async function() {
9
- return await targetModuleContent({ before, after, beforeEach, afterEach }, {
10
- moduleName,
11
- options: runtimeOptions
162
+ moduleContext.tests.forEach((testContext) => {
163
+ const { name, ...moduleContextWithoutName } = moduleContext;
164
+
165
+ Object.assign(testContext, moduleContextWithoutName, {
166
+ steps: moduleContext.steps,
167
+ totalExecutedAssertions: moduleContext.totalExecutedAssertions,
168
+ expectedAssertionCount: moduleContext.expectedAssertionCount,
169
+ });
170
+ });
171
+ });
172
+ afterAll(async () => {
173
+ for (const assert of moduleContext.tests.map(testContext => testContext.assert)) {
174
+ await assert.waitForAsyncOps();
175
+ }
176
+
177
+ let targetContext = moduleContext.tests[moduleContext.tests.length - 1];
178
+ for (let j = afterHooks.length - 1; j >= 0; j--) {
179
+ await afterHooks[j].call(targetContext, targetContext.assert);
180
+ }
181
+
182
+ moduleContext.tests.forEach(testContext => testContext.finish());
12
183
  });
184
+
185
+ targetModuleContent.call(moduleContext, {
186
+ before(beforeFn) {
187
+ return beforeHooks.push(beforeFn);
188
+ },
189
+ beforeEach(beforeEachFn) {
190
+ return moduleContext.beforeEachHooks.push(beforeEachFn);
191
+ },
192
+ afterEach(afterEachFn) {
193
+ return moduleContext.afterEachHooks.push(afterEachFn);
194
+ },
195
+ after(afterFn) {
196
+ return afterHooks.push(afterFn);
197
+ }
198
+ }, { moduleName, options: runtimeOptions });
199
+
200
+ ModuleContext.currentModuleChain.pop();
13
201
  });
14
202
  }
15
203
 
16
- export const test = async function(testName, runtimeOptions, testContent) {
204
+ export const test = (testName, runtimeOptions, testContent) => {
205
+ let moduleContext = ModuleContext.lastModule;
206
+ if (!moduleContext) {
207
+ throw new Error(`Test '${testName}' called outside of module context.`);
208
+ }
209
+
17
210
  let targetRuntimeOptions = testContent ? runtimeOptions : {};
18
211
  let targetTestContent = testContent ? testContent : runtimeOptions;
212
+ let context = new TestContext(testName, moduleContext);
19
213
 
20
- return it(testName, assignDefaultValues(targetRuntimeOptions, { concurrency: true }), async function() {
21
- return await targetTestContent(assert, { testName, options: runtimeOptions });
22
- });
23
- }
214
+ return it(testName, { concurrency: true, ...targetRuntimeOptions }, async function () {
215
+ let result;
216
+ for (let module of context.module.moduleChain) {
217
+ for (let hook of module.beforeEachHooks) {
218
+ await hook.call(context, context.assert);
219
+ }
220
+ }
221
+
222
+ result = await targetTestContent.call(context, context.assert, { testName, options: runtimeOptions });
24
223
 
25
- function assignDefaultValues(options, defaultValues) {
26
- for (let key in defaultValues) {
27
- if (options[key] === undefined) {
28
- options[key] = defaultValues[key];
224
+ await context.assert.waitForAsyncOps();
225
+
226
+ for (let i = context.module.moduleChain.length - 1; i >= 0; i--) {
227
+ let module = context.module.moduleChain[i];
228
+ for (let j = module.afterEachHooks.length - 1; j >= 0; j--) {
229
+ await module.afterEachHooks[j].call(context, context.assert);
230
+ }
29
231
  }
30
- }
31
232
 
32
- return options;
233
+ return result;
234
+ });
33
235
  }
34
236
 
35
237
  export default { module, test, config: {} };
36
-
37
- // NOTE: later maybe expose these as well:
38
-
39
- // import QUnit from './vendor/qunit.js';
40
-
41
- // QUnit.config.autostart = false;
42
-
43
- // export const isLocal = QUnit.isLocal;
44
- // export const on = QUnit.on;
45
- // export const test = QUnit.test;
46
- // export const skip = QUnit.skip;
47
- // export const start = QUnit.start;
48
- // export const is = QUnit.is;
49
- // export const extend = QUnit.extend;
50
- // export const stack = QUnit.stack;
51
- // export const onUnhandledRejection = QUnit.onUnhandledRejection;
52
- // export const assert = QUnit.assert;
53
- // export const dump = QUnit.dump;
54
- // export const done = QUnit.done;
55
- // export const testStart = QUnit.testStart;
56
- // export const moduleStart = QUnit.moduleStart;
57
- // export const version = QUnit.version;
58
- // export const module = QUnit.module;
59
- // export const todo = QUnit.todo;
60
- // export const only = QUnit.only;
61
- // export const config = QUnit.config;
62
- // export const objectType = QUnit.objectType;
63
- // export const load = QUnit.load;
64
- // export const onError = QUnit.onError;
65
- // export const pushFailure = QUnit.pushFailure;
66
- // export const equiv = QUnit.equiv;
67
- // export const begin = QUnit.begin;
68
- // export const log = QUnit.log;
69
- // export const testDone = QUnit.testDone;
70
- // export const moduleDone = QUnit.moduleDone;
71
- // export const diff = QUnit.diff;
72
-
73
- // export default Object.assign(QUnit, {
74
- // QUnitxVersion: '0.0.1'
75
- // });