qunitx 0.8.2 → 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.2",
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,
@@ -1,6 +1,6 @@
1
1
  import { AssertionError as DenoAssertionError, assertRejects, assertThrows } from "https://deno.land/std@0.192.0/testing/asserts.ts";
2
2
  import '../../vendor/qunit.js';
3
- import { objectValuesSubset } from '../shared/index.js';
3
+ import { objectValues, objectValuesSubset, validateExpectedExceptionArgs, validateException } from '../shared/index.js';
4
4
  import util from 'node:util';
5
5
 
6
6
  export class AssertionError extends DenoAssertionError {
@@ -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);
23
35
 
24
- this._steps.length = 0;
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
+ }
25
42
 
26
- return result;
27
- },
28
- expect() {
29
- return () => {}; // NOTE: NOT implemented
30
- },
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
+ }
56
+
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,9 +101,10 @@ export default {
60
101
  stackStartFn: this.notOk,
61
102
  });
62
103
  }
63
- },
104
+ }
64
105
  true(state, message) {
65
- if (state === true) {
106
+ this._incrementAssertionCount();
107
+ if (state !== true) {
66
108
  throw new AssertionError({
67
109
  actual: state,
68
110
  expected: true,
@@ -70,9 +112,10 @@ export default {
70
112
  stackStartFn: this.true,
71
113
  });
72
114
  }
73
- },
115
+ }
74
116
  false(state, message) {
75
- if (state === false) {
117
+ this._incrementAssertionCount();
118
+ if (state !== false) {
76
119
  throw new AssertionError({
77
120
  actual: state,
78
121
  expected: true,
@@ -80,9 +123,10 @@ export default {
80
123
  stackStartFn: this.false,
81
124
  });
82
125
  }
83
- },
126
+ }
84
127
  equal(actual, expected, message) {
85
- if (actual == expected) {
128
+ this._incrementAssertionCount();
129
+ if (actual != expected) {
86
130
  throw new AssertionError({
87
131
  actual,
88
132
  expected,
@@ -91,9 +135,10 @@ export default {
91
135
  stackStartFn: this.equal,
92
136
  });
93
137
  }
94
- },
138
+ }
95
139
  notEqual(actual, expected, message) {
96
- if (actual != expected) {
140
+ this._incrementAssertionCount();
141
+ if (actual == expected) {
97
142
  throw new AssertionError({
98
143
  actual,
99
144
  expected,
@@ -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.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,24 +247,76 @@ export default {
194
247
  stackStartFn: this.notStrictEqual,
195
248
  });
196
249
  }
197
- },
198
- throws: assertThrows,
199
- rejects: assertRejects,
200
- };
250
+ }
251
+ throws(blockFn, expectedInput, assertionMessage) {
252
+ this?._incrementAssertionCount();
253
+ let [expected, message] = validateExpectedExceptionArgs(expectedInput, assertionMessage, 'rejects');
254
+ if (typeof blockFn !== 'function') {
255
+ throw new AssertionError({
256
+ actual: blockFn,
257
+ expected: Function,
258
+ message: 'The value provided to `assert.throws` was not a function.',
259
+ stackStartFn: this.throws,
260
+ });
261
+ }
201
262
 
202
- function objectValues(obj) {
203
- let allowArray = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
204
- let vals = allowArray && is('array', obj) ? [] : {};
263
+ try {
264
+ blockFn();
265
+ } catch (error) {
266
+ let validation = validateException(error, expected, message);
267
+ if (validation.result === false) {
268
+ throw new AssertionError({
269
+ actual: validation.result,
270
+ expected: validation.expected,
271
+ message: validation.message,
272
+ stackStartFn: this.throws,
273
+ });
274
+ }
205
275
 
206
- for (var key in obj) {
207
- if (hasOwn$1.call(obj, key)) {
208
- let val = obj[key];
209
- vals[key] = val === Object(val) ? objectValues(val, allowArray) : val;
276
+ return;
210
277
  }
278
+
279
+ throw new AssertionError({
280
+ actual: blockFn,
281
+ expected: expected,
282
+ message: 'Function passed to `assert.throws` did not throw an exception!',
283
+ stackStartFn: this.throws,
284
+ });
211
285
  }
286
+ async rejects(promise, expectedInput, assertionMessage) {
287
+ this._incrementAssertionCount();
288
+ let [expected, message] = validateExpectedExceptionArgs(expectedInput, assertionMessage, 'rejects');
289
+ let then = promise && promise.then;
290
+ if (typeof then !== 'function') {
291
+ throw new AssertionError({
292
+ actual: promise,
293
+ expected: expected,
294
+ message: 'The value provided to `assert.rejects` was not a promise!',
295
+ stackStartFn: this.rejects,
296
+ });
297
+ }
212
298
 
213
- return vals;
214
- }
299
+ try {
300
+ await promise;
301
+ throw new AssertionError({
302
+ actual: promise,
303
+ expected: expected,
304
+ message: 'The promise returned by the `assert.rejects` callback did not reject!',
305
+ stackStartFn: this.rejects,
306
+ });
307
+ } catch (error) {
308
+ let validation = validateException(error, expected, message);
309
+ if (validation.result === false) {
310
+ throw new AssertionError({
311
+ actual: validation.result,
312
+ expected: validation.expected,
313
+ message: validation.message,
314
+ stackStartFn: this.rejects,
315
+ });
316
+ }
317
+ }
318
+ }
319
+ };
215
320
 
216
321
  function defaultMessage(actual, description, expected) {
217
322
  return `
@@ -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: {} };