qunitx 0.12.0 → 0.12.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,156 +1,235 @@
1
- ![docker-based-ci](https://github.com/izelnakri/qunitx/workflows/docker-based-ci/badge.svg)
2
- [![npm version](https://badge.fury.io/js/qunitx.svg)](https://badge.fury.io/js/qunitx)
3
- [![Ask me anything](https://img.shields.io/badge/ask%20me-anything-1abc9c.svg)](https://github.com/izelnakri/qunitx/issues)
1
+ <div align="center">
4
2
 
5
3
  # QUnitX
6
4
 
7
- Mature, fast, 0 dependency & flexible test API for JavaScript/TypeScript.
5
+ [![CI](https://github.com/izelnakri/qunitx/actions/workflows/ci.yml/badge.svg)](https://github.com/izelnakri/qunitx/actions/workflows/ci.yml)
6
+ [![codecov](https://codecov.io/gh/izelnakri/qunitx/branch/main/graph/badge.svg)](https://codecov.io/gh/izelnakri/qunitx)
7
+ [![npm](https://img.shields.io/npm/v/qunitx)](https://www.npmjs.com/package/qunitx)
8
+ [![npm downloads](https://img.shields.io/npm/dm/qunitx)](https://www.npmjs.com/package/qunitx)
9
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
10
+ [![Ask me anything](https://img.shields.io/badge/ask%20me-anything-1abc9c.svg)](https://github.com/izelnakri/qunitx/issues)
11
+ [![Sponsor](https://img.shields.io/badge/sponsor-%E2%99%A5-pink)](https://github.com/sponsors/izelnakri)
8
12
 
9
- ***Universal testing for JavaScript with the oldest, most mature & flexible
10
- testing API in the JavaScript ecosystem. Run the same test file in node.js, deno or in the browser***
13
+ </div>
11
14
 
12
- Your test JS/TS file(s) can now run interchangably in different runtimes with
13
- the default test runner of node.js or deno, or with a browser runner of your
14
- choice!
15
+ **The oldest, most battle-tested JavaScript test API now universal.**
15
16
 
16
- [![asciicast](https://asciinema.org/a/597066.svg)](https://asciinema.org/a/597066?autoplay=1)
17
+ Run the **same test file** in Node.js, Deno, and the browser without changes.
18
+ Zero dependencies. No config needed for Node. TypeScript works out of the box.
17
19
 
18
- In the browser you can use the same browser test/filter UI of
19
- [QUnit](https://github.com/qunitjs/qunit) and share the web links with your
20
- colleagues thanks to the test filters through query params feature of QUnit:
20
+ ---
21
21
 
22
- [QUnit Test Suite Example](https://objectmodel.js.org/test/?moduleId=6e15ed5f&moduleId=950ec9c5)
22
+ ## Why QUnit?
23
23
 
24
- **UI visual automated tests also possible with QUnit!**
24
+ QUnit was created in 2008 by the jQuery team. While newer frameworks come and go,
25
+ QUnit has quietly accumulated 16+ years of real-world edge-case handling that younger
26
+ tools are still catching up to. Its assertion API is the most mature in the JavaScript
27
+ ecosystem:
25
28
 
26
- ![QunitX terminal output](https://raw.githubusercontent.com/izelnakri/qunitx/main/docs/qunitx-help-stdout.png)
29
+ - **`assert.deepEqual`** — handles circular references, prototype chains, Sets, Maps,
30
+ typed arrays, Dates, RegExps, and getters correctly
31
+ - **`assert.throws` / `assert.rejects`** — match errors by constructor, regex, or custom validator
32
+ - **`assert.step` / `assert.verifySteps`** — declarative execution-order verification;
33
+ catches missing async callbacks that other frameworks silently swallow
34
+ - **`assert.expect(n)`** — fails the test if exactly _n_ assertions didn't run;
35
+ invaluable for async code where missing assertions would otherwise pass silently
36
+ - **Hooks** — `before`, `beforeEach`, `afterEach`, `after` with correct FIFO/LIFO ordering,
37
+ properly scoped across nested modules
38
+ - **Shareable browser URLs** — the QUnit browser UI filters tests via query params, so you can
39
+ share `https://yourapp.test/?moduleId=abc123` with a colleague and they see exactly the same view
27
40
 
28
- ### Installation: Node & Deno
41
+ QUnitX wraps this API to work with **Node.js's built-in `node:test` runner** and
42
+ **Deno's native test runner** — no Jest, Vitest, or other framework needed.
29
43
 
30
- This is a 0-dependency test library that runs code in your target runtime(node,
31
- deno or browser) test runner. Since a default test runner is a new feature of node.js, please use node.js v20.3+.
44
+ QUnit includes the fastest assertion and test runtime in JS world. I've previously contributed to some [speed optimizations](https://qunitjs.com/blog/2022/02/15/qunit-2-18-0/) to QUnit, we benchmark every possible thing to make it the fastest test
45
+ runtime, faster than node.js and deno default assertions in most cases. Therefore I consider myself very objective
46
+ when I say QUnit(X) is the best JS/TS testing tool out there.
32
47
 
33
- In order to use qunitx to convert qunit tests files please change:
48
+ ---
34
49
 
35
- ```js
36
- import { module, test } from 'qunit';
50
+ ## Demo
37
51
 
38
- // to:
39
- import { module, test } from 'qunitx';
52
+ > Left window: `node --test` and `deno test` running the same file.
53
+ > Right window: QUnit browser UI with filterable, shareable test results.
54
+
55
+ <!-- Demo GIF: see docs/demo.tape (VHS script) for terminal portion.
56
+ For the combined terminal + browser recording, see "Recording the demo" below. -->
57
+ ![QUnitX demo](https://raw.githubusercontent.com/izelnakri/qunitx/main/docs/demo.gif)
58
+
59
+ Live browser UI example (click to see filterable QUnit test suite):
60
+
61
+ [objectmodel.js.org/test/?moduleId=6e15ed5f](https://objectmodel.js.org/test/?moduleId=6e15ed5f&moduleId=950ec9c5)
62
+
63
+ ---
64
+
65
+ ## Installation
66
+
67
+ ```sh
68
+ npm install qunitx --save-dev
40
69
  ```
41
70
 
42
- Example:
71
+ Requires **Node.js >= 22** (LTS) or **Deno >= 2**.
72
+
73
+ ---
74
+
75
+ ## Quick start
43
76
 
44
77
  ```js
45
- // in some-test.js: (typescript also works)
78
+ // math-test.js (works in Node, Deno, and browser unchanged)
46
79
  import { module, test } from 'qunitx';
47
- import $ from 'jquery';
48
80
 
49
- module('Basic sanity check', function (hooks) {
50
- test('it works', function (assert) {
51
- assert.equal(true, true);
81
+ module('Math utilities', (hooks) => {
82
+ hooks.before((assert) => {
83
+ assert.step('setup complete');
52
84
  });
53
85
 
54
- module('More advanced cases', function (hooks) {
55
- test('deepEqual works', function (assert) {
56
- assert.deepEqual({ username: 'izelnakri' }, { username: 'izelnakri' });
57
- });
58
- test('can import ES & npm modules', function (assert) {
59
- assert.ok(Object.keys($));
86
+ test('addition', (assert) => {
87
+ assert.equal(2 + 2, 4);
88
+ assert.notEqual(2 + 2, 5);
89
+ });
90
+
91
+ test('deepEqual', (assert) => {
92
+ assert.deepEqual({ a: 1, b: [2, 3] }, { a: 1, b: [2, 3] });
93
+ });
94
+
95
+ module('Async', () => {
96
+ test('resolves correctly', async (assert) => {
97
+ const result = await Promise.resolve(42);
98
+ assert.strictEqual(result, 42);
60
99
  });
61
100
  });
62
101
  });
63
102
  ```
64
103
 
65
- ```zsh
66
- # you can run the test in node with ES modules package.json{ "type": "module" }
67
- $ node --test some-test.js
104
+ ### Node.js
105
+
106
+ ```sh
107
+ # No extra dependencies — uses the Node built-in test runner
108
+ node --test math-test.js
109
+
110
+ # Watch mode (re-runs on save)
111
+ node --test --watch math-test.js
112
+
113
+ # Glob pattern
114
+ node --test --watch 'test/**/*.js'
115
+
116
+ # TypeScript (tsconfig.json with moduleResolution: NodeNext required)
117
+ node --import=tsx/esm --test math-test.ts
118
+
119
+ # Code coverage
120
+ npx c8 node --test math-test.js
121
+ ```
68
122
 
69
- # TypeScript also works, make sure on node.js mode, tsconfig.json exists with compilerOptions.module & compilerOptions.moduleResolution set to "NodeNext":
70
- $ node --loader=ts-node/esm/transpile-only --test some-test.ts
123
+ ### Deno
71
124
 
72
- # You can use the new watch mode of node.js to watch for files or folder patterns
73
- $ node --test --watch some-test.js some-folder/*.js
125
+ ```sh
126
+ # One-time: create a deno.json import map
127
+ echo '{"imports": {"qunitx": "https://esm.sh/qunitx/shims/deno/index.js"}}' > deno.json
74
128
 
75
- # You can also run this test on deno. Unfortunately today deno requires one extra step to create a deno.json file:
76
- $ echo '{"imports": { "qunitx": "https://esm.sh/qunitx/shims/deno/index.js" } }' > deno.json
129
+ # Run
130
+ deno test math-test.js
77
131
 
78
- # then run the tests in default deno test runner:
79
- $ deno test some-test.js
132
+ # With explicit permissions
133
+ deno test --allow-read --allow-env math-test.js
80
134
  ```
81
135
 
82
- ### Installation: Browser
136
+ ### Browser
83
137
 
84
- QUnitX mainly proxies to [QUnit
85
- API](https://api.qunitjs.com/QUnit/module/#hooks-on-nested-modules) in browser.
86
- You can use [QUnitX CLI](https://github.com/izelnakri/qunitx-cli) to get your
87
- browser tests to stdout/CI or use the watch mode during the development.
138
+ Use [qunitx-cli](https://github.com/izelnakri/qunitx-cli) to get browser test output
139
+ in your terminal / CI, or to open the live QUnit UI during development:
88
140
 
89
- ```zsh
90
- # Install QUnitX browser runner/cli:
91
- $ npm install -g qunitx-cli
92
- $ qunitx
93
- $ qunitx some-test.js
141
+ ```sh
142
+ npm install -g qunitx-cli
94
143
 
95
- # with browser output enabled:
96
- $ qunitx some-test.js --debug
144
+ # Headless (CI-friendly outputs TAP to stdout)
145
+ qunitx math-test.js
97
146
 
147
+ # Open QUnit browser UI alongside terminal output
148
+ qunitx math-test.js --debug
98
149
  ```
99
150
 
100
- ### Concurrency options
151
+ The browser UI lets you:
152
+ - Filter by module or test name (filter state is preserved in the URL)
153
+ - Share a link that reproduces the exact filtered view with a colleague
154
+ - Re-run individual tests by clicking them
155
+ - See full assertion diffs inline
101
156
 
102
- QUnitX API accepts an optional options object as 2nd argument to:
103
- - `QUnit.module(testName, optionsOrHandler?, handler?)`
104
- - `QUnit.test(testName, optionsOrHandler?, handler?)`
157
+ ---
105
158
 
106
- So you can run tests in parallel(default) or in series. You can even run them
107
- through the [node.js test runner run()
108
- api](https://nodejs.org/api/test.html#runoptions):
159
+ ## Migrating from QUnit
160
+
161
+ One import line is all that changes:
109
162
 
110
163
  ```js
111
- // in some-test.js: (typescript also works)
164
+ // Before:
165
+ import { module, test } from 'qunit';
166
+
167
+ // After:
112
168
  import { module, test } from 'qunitx';
113
- import $ from 'jquery';
169
+ ```
114
170
 
115
- module('Basic sanity check', function (hooks) {
116
- test('it works', { concurrency: false }, function (assert) {
117
- assert.equal(true, true);
118
- });
171
+ ---
119
172
 
120
- module('More advanced cases', { concurrency: false, permissions: { read: true }, sanitizeExit: false }, function (hooks) {
121
- test('deepEqual works', function (assert) {
122
- assert.deepEqual({ username: 'izelnakri' }, { username: 'izelnakri' });
123
- });
124
- test('can import ES & npm modules', function (assert) {
125
- assert.ok(Object.keys($));
126
- });
173
+ ## Concurrency options
174
+
175
+ `module()` and `test()` accept an optional options object forwarded directly to the underlying
176
+ Node / Deno test runner:
177
+
178
+ ```js
179
+ import { module, test } from 'qunitx';
180
+
181
+ // Run tests in this module serially
182
+ module('Serial suite', { concurrency: false }, (hooks) => {
183
+ test('first', (assert) => { assert.ok(true); });
184
+ test('second', (assert) => { assert.ok(true); });
185
+ });
186
+
187
+ // Deno-specific: permissions, sanitizeExit, etc.
188
+ module('Deno file access', { permissions: { read: true }, sanitizeExit: false }, (hooks) => {
189
+ test('reads a file', async (assert) => {
190
+ const text = await Deno.readTextFile('./README.md');
191
+ assert.ok(text.length > 0);
127
192
  });
128
193
  });
129
194
  ```
130
195
 
131
- ### Code coverage
196
+ ---
197
+
198
+ ## How it works
199
+
200
+ | Runtime | Adapter |
201
+ |---------|---------|
202
+ | Node.js | Wraps `node:test` `describe` / `it` with QUnit lifecycle |
203
+ | Deno | Wraps Deno BDD helpers with the same QUnit lifecycle |
204
+ | Browser | Thin re-export of QUnit's native browser API |
205
+
206
+ The browser path is literally QUnit itself, so you get full QUnit compatibility:
207
+ plugins, custom reporters, the event API (`QUnit.on`, `QUnit.done`, etc.), and the
208
+ familiar browser UI with zero extra layers.
209
+
210
+ ---
132
211
 
133
- Since QUnitX proxies to default node.js test runner in when executed with node,
134
- you can use any code coverage tool you like. When running the tests in
135
- `qunit`(the browser mode) code coverage support is limited.
212
+ ## Code coverage
136
213
 
137
- ```zsh
138
- $ c8 node --test test/attachments test/user
214
+ Probably c8 isn't even needed since qunitx runs as a dependency(rather than runtime) on node.js and deno.
215
+
216
+ ```sh
217
+ # Node (any c8-compatible reporter)
218
+ npx c8 node --test test/
219
+
220
+ # View HTML report
221
+ npx c8 --reporter=html node --test test/ && open coverage/index.html
139
222
  ```
140
223
 
141
- You can browse [c8 documentation](https://github.com/bcoe/c8) for all
142
- configuration options.
143
-
144
- Implementing code coverage for the browser mode is currently not possible
145
- because we use esbuild --bundle feature to create a JS bundles for testing in
146
- the browser, this could be instrumented with `puppeteer-to-istanbul` however
147
- instrumentation includes transpiled npm imports of `qunitx` and other potential
148
- npm imports developer includes in the code, this cannot be filtered since
149
- potential filtering can only occur after the `esbuild` bundling. When chrome
150
- browser and puppeteer fully supports ES asset maps we can remove esbuild from
151
- the browser mode, run everything in deno and make instrumentation for code
152
- coverage possible with the default v8 instrumentation.
153
-
154
- Esbuild plugin interface is an ongoing development, we might be able to figure
155
- out a way to generate this instrumentation with esbuild in the future, which
156
- could allow code coverage for --browser mode.
224
+ Browser-mode coverage is limited because qunitx-cli bundles test files with esbuild.
225
+ Native ES import maps support in Puppeteer/Chrome would eliminate the bundling step
226
+ and unlock v8 instrumentation for browser coverage.
227
+
228
+ ---
229
+
230
+ ## Links
231
+
232
+ - [QUnit API reference](https://api.qunitjs.com)
233
+ - [qunitx-cli](https://github.com/izelnakri/qunitx-cli) browser runner / CI reporter
234
+ - [Node.js test runner docs](https://nodejs.org/api/test.html)
235
+ - [Deno testing docs](https://docs.deno.com/runtime/fundamentals/testing/)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "qunitx",
3
3
  "type": "module",
4
- "version": "0.12.0",
4
+ "version": "0.12.4",
5
5
  "description": "A universal test framework for testing any js file on node.js, browser or deno with QUnit API",
6
6
  "author": "Izel Nakri",
7
7
  "license": "MIT",
@@ -44,7 +44,7 @@
44
44
  "browser"
45
45
  ],
46
46
  "engines": {
47
- "node": ">=24.0.0"
47
+ "node": ">=22.0.0"
48
48
  },
49
49
  "imports": {
50
50
  "qunitx": {
@@ -60,11 +60,13 @@
60
60
  },
61
61
  "repository": {
62
62
  "type": "git",
63
- "url": "https://github.com/izelnakri/qunitx.git"
63
+ "url": "git+https://github.com/izelnakri/qunitx.git"
64
64
  },
65
65
  "scripts": {
66
- "lint": "prettier --check \"test/**/*.js\" \"*.js\" \"package.json\"",
67
- "lint:fix": "prettier --write \"test/**/*.js\" \"*.js\" \"package.json\"",
66
+ "format": "prettier --check \"test/**/*.js\" \"*.js\" \"package.json\"",
67
+ "format:fix": "prettier --write \"test/**/*.js\" \"*.js\" \"package.json\"",
68
+ "lint": "deno lint shims/",
69
+ "lint:docs": "deno doc --lint shims/deno/module.js shims/deno/test.js",
68
70
  "build": "node build.js",
69
71
  "run:all": "npm run run:node && npm run run:deno",
70
72
  "run:node": "node --test test/helpers/passing-tests.js && node --test test/helpers/failing-tests.js",
@@ -75,9 +77,13 @@
75
77
  "prepack": "npm run build",
76
78
  "test": "npm run test:browser && npm run test:node && npm run test:deno",
77
79
  "test:dev": "npm run test | tee test-output.log",
78
- "test:browser": "node_modules/.bin/qunitx test/index.js --debug",
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:node": "node --test test/index.js"
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
+ }
@@ -1,4 +1,4 @@
1
- import { AssertionError as DenoAssertionError } from "https://deno.land/std@0.192.0/testing/asserts.ts";
1
+ import { AssertionError as DenoAssertionError } from "jsr:@std/assert";
2
2
  import '../../vendor/qunit.js';
3
3
  import Assert from '../shared/assert.js';
4
4
  import ModuleContext from '../shared/module-context.js';
@@ -1,18 +1,45 @@
1
- import { describe, beforeAll, afterAll } from "https://deno.land/std@0.192.0/testing/bdd.ts";
1
+ import { describe, beforeAll, afterAll } from "jsr:@std/testing/bdd";
2
2
  import ModuleContext from '../shared/module-context.js';
3
3
 
4
4
  // NOTE: node.js beforeEach & afterEach is buggy because the TestContext it has is NOT correct reference when called, it gets the last context
5
5
  // NOTE: QUnit expect() logic is buggy in nested modules
6
6
  // NOTE: after gets the last direct children test of the module, not last defined context of a module(last defined context is a module)
7
7
 
8
+ /**
9
+ * Defines a test module (suite) for Deno's BDD test runner.
10
+ *
11
+ * Wraps `describe()` from `@std/testing/bdd` and sets up the QUnit lifecycle
12
+ * (before/beforeEach/afterEach/after hooks, assertion counting, steps tracking).
13
+ *
14
+ * @param {string} moduleName - Name of the test suite
15
+ * @param {object} [runtimeOptions] - Optional Deno BDD options forwarded to `describe()`
16
+ * (e.g. `{ concurrency: false }`, `{ permissions: { read: true } }`)
17
+ * @param {function} moduleContent - Callback that defines tests and hooks via `hooks.before`,
18
+ * `hooks.beforeEach`, `hooks.afterEach`, `hooks.after`
19
+ * @returns {void}
20
+ * @example
21
+ * ```js ignore
22
+ * import { module, test } from "qunitx";
23
+ *
24
+ * module("Math", (hooks) => {
25
+ * hooks.before((assert) => {
26
+ * assert.step("before hook ran");
27
+ * });
28
+ *
29
+ * test("addition", (assert) => {
30
+ * assert.equal(2 + 2, 4);
31
+ * });
32
+ * });
33
+ * ```
34
+ */
8
35
  export default function module(moduleName, runtimeOptions, moduleContent) {
9
- let targetRuntimeOptions = moduleContent ? runtimeOptions : {};
10
- let targetModuleContent = moduleContent ? moduleContent : runtimeOptions;
11
- let moduleContext = new ModuleContext(moduleName);
36
+ const targetRuntimeOptions = moduleContent ? runtimeOptions : {};
37
+ const targetModuleContent = moduleContent ? moduleContent : runtimeOptions;
38
+ const moduleContext = new ModuleContext(moduleName);
12
39
 
13
- return describe(moduleName, { concurrency: true, ...targetRuntimeOptions }, async function () {
14
- let beforeHooks = [];
15
- let afterHooks = [];
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 (let hook of beforeHooks) {
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
- let targetContext = moduleContext.tests[moduleContext.tests.length - 1];
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
  }
@@ -1,30 +1,59 @@
1
- import { it } from "https://deno.land/std@0.192.0/testing/bdd.ts";
1
+ import { it } from "jsr:@std/testing/bdd";
2
2
  import TestContext from '../shared/test-context.js';
3
3
  import ModuleContext from '../shared/module-context.js';
4
4
 
5
+ /**
6
+ * Defines an individual test within a module for Deno's BDD test runner.
7
+ *
8
+ * Wraps `it()` from `@std/testing/bdd` and handles the full QUnit lifecycle:
9
+ * beforeEach/afterEach hooks, async assertion waiting, and step verification.
10
+ *
11
+ * Must be called inside a `module()` callback.
12
+ *
13
+ * @param {string} testName - Name of the test
14
+ * @param {object} [runtimeOptions] - Optional Deno BDD options forwarded to `it()`
15
+ * (e.g. `{ concurrency: false }`, `{ sanitizeExit: false }`)
16
+ * @param {function} testContent - Test callback receiving `(assert, { testName, options })`
17
+ * @returns {void}
18
+ * @example
19
+ * ```js ignore
20
+ * import { module, test } from "qunitx";
21
+ *
22
+ * module("Math", () => {
23
+ * test("addition", (assert) => {
24
+ * assert.equal(1 + 1, 2);
25
+ * });
26
+ *
27
+ * test("async resolves correctly", async (assert) => {
28
+ * const result = await Promise.resolve(42);
29
+ * assert.strictEqual(result, 42);
30
+ * });
31
+ * });
32
+ * ```
33
+ */
5
34
  export default function test(testName, runtimeOptions, testContent) {
6
- let moduleContext = ModuleContext.lastModule;
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
- let targetRuntimeOptions = testContent ? runtimeOptions : {};
12
- let targetTestContent = testContent ? testContent : runtimeOptions;
13
- let context = new TestContext(testName, moduleContext);
40
+ const targetRuntimeOptions = testContent ? runtimeOptions : {};
41
+ const targetTestContent = testContent ? testContent : runtimeOptions;
42
+ const context = new TestContext(testName, moduleContext);
14
43
 
15
- return it(testName, { concurrency: true, ...targetRuntimeOptions }, async function () {
16
- for (let module of context.module.moduleChain) {
17
- for (let hook of module.beforeEachHooks) {
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
- let result = await targetTestContent.call(context, context.assert, { testName, options: runtimeOptions });
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
- let module = context.module.moduleChain[i];
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
  }
@@ -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
- let targetRuntimeOptions = moduleContent ? runtimeOptions : {};
10
- let targetModuleContent = moduleContent ? moduleContent : runtimeOptions;
11
- let moduleContext = new ModuleContext(moduleName);
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 }, async function () {
14
- let beforeHooks = [];
15
- let afterHooks = [];
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 (let hook of beforeHooks) {
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
- let targetContext = moduleContext.tests[moduleContext.tests.length - 1];
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
  }
@@ -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
- let moduleContext = ModuleContext.lastModule;
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
- let targetRuntimeOptions = testContent ? runtimeOptions : {};
12
- let targetTestContent = testContent ? testContent : runtimeOptions;
13
- let context = new TestContext(testName, moduleContext);
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 (let module of context.module.moduleChain) {
17
- for (let hook of module.beforeEachHooks) {
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
- let result = await targetTestContent.call(context, context.assert, { testName, options: runtimeOptions });
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
- let module = context.module.moduleChain[i];
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
  }
@@ -55,13 +55,13 @@ export default class Assert {
55
55
  }
56
56
  async() {
57
57
  let resolveFn;
58
- let done = new Promise(resolve => { resolveFn = resolve; });
58
+ const done = new Promise(resolve => { resolveFn = resolve; });
59
59
 
60
60
  this.test.asyncOps.push(done);
61
61
 
62
62
  return () => { resolveFn(); };
63
63
  }
64
- async waitForAsyncOps() {
64
+ waitForAsyncOps() {
65
65
  return Promise.all(this.test.asyncOps);
66
66
  }
67
67
  pushResult(resultInfo = {}) {
@@ -147,8 +147,8 @@ export default class Assert {
147
147
  }
148
148
  propEqual(actual, expected, message) {
149
149
  this._incrementAssertionCount();
150
- let targetActual = objectValues(actual);
151
- let targetExpected = objectValues(expected);
150
+ const targetActual = objectValues(actual);
151
+ const targetExpected = objectValues(expected);
152
152
  if (!Assert.QUnit.equiv(targetActual, targetExpected)) {
153
153
  throw new Assert.AssertionError({
154
154
  actual: targetActual,
@@ -160,8 +160,8 @@ export default class Assert {
160
160
  }
161
161
  notPropEqual(actual, expected, message) {
162
162
  this._incrementAssertionCount();
163
- let targetActual = objectValues(actual);
164
- let targetExpected = objectValues(expected);
163
+ const targetActual = objectValues(actual);
164
+ const targetExpected = objectValues(expected);
165
165
  if (Assert.QUnit.equiv(targetActual, targetExpected)) {
166
166
  throw new Assert.AssertionError({
167
167
  actual: targetActual,
@@ -173,8 +173,8 @@ export default class Assert {
173
173
  }
174
174
  propContains(actual, expected, message) {
175
175
  this._incrementAssertionCount();
176
- let targetActual = objectValuesSubset(actual, expected);
177
- let targetExpected = objectValues(expected, false);
176
+ const targetActual = objectValuesSubset(actual, expected);
177
+ const targetExpected = objectValues(expected, false);
178
178
  if (!Assert.QUnit.equiv(targetActual, targetExpected)) {
179
179
  throw new Assert.AssertionError({
180
180
  actual: targetActual,
@@ -186,8 +186,8 @@ export default class Assert {
186
186
  }
187
187
  notPropContains(actual, expected, message) {
188
188
  this._incrementAssertionCount();
189
- let targetActual = objectValuesSubset(actual, expected);
190
- let targetExpected = objectValues(expected);
189
+ const targetActual = objectValuesSubset(actual, expected);
190
+ const targetExpected = objectValues(expected);
191
191
  if (Assert.QUnit.equiv(targetActual, targetExpected)) {
192
192
  throw new Assert.AssertionError({
193
193
  actual: targetActual,
@@ -247,7 +247,7 @@ export default class Assert {
247
247
  }
248
248
  throws(blockFn, expectedInput, assertionMessage) {
249
249
  this?._incrementAssertionCount();
250
- let [expected, message] = validateExpectedExceptionArgs(expectedInput, assertionMessage, 'rejects');
250
+ const [expected, message] = validateExpectedExceptionArgs(expectedInput, assertionMessage, 'rejects');
251
251
  if (typeof blockFn !== 'function') {
252
252
  throw new Assert.AssertionError({
253
253
  actual: blockFn,
@@ -260,7 +260,7 @@ export default class Assert {
260
260
  try {
261
261
  blockFn();
262
262
  } catch (error) {
263
- let validation = validateException(error, expected, message);
263
+ const validation = validateException(error, expected, message);
264
264
  if (validation.result === false) {
265
265
  throw new Assert.AssertionError({
266
266
  actual: validation.result,
@@ -282,8 +282,8 @@ export default class Assert {
282
282
  }
283
283
  async rejects(promise, expectedInput, assertionMessage) {
284
284
  this._incrementAssertionCount();
285
- let [expected, message] = validateExpectedExceptionArgs(expectedInput, assertionMessage, 'rejects');
286
- let then = promise && promise.then;
285
+ const [expected, message] = validateExpectedExceptionArgs(expectedInput, assertionMessage, 'rejects');
286
+ const then = promise && promise.then;
287
287
  if (typeof then !== 'function') {
288
288
  throw new Assert.AssertionError({
289
289
  actual: promise,
@@ -302,7 +302,7 @@ export default class Assert {
302
302
  stackStartFn: this.rejects,
303
303
  });
304
304
  } catch (error) {
305
- let validation = validateException(error, expected, message);
305
+ const validation = validateException(error, expected, message);
306
306
  if (validation.result === false) {
307
307
  throw new Assert.AssertionError({
308
308
  actual: validation.result,
@@ -1,14 +1,8 @@
1
1
  const hasOwn = Object.prototype.hasOwnProperty
2
2
 
3
- function _typeof(obj) {
4
- "@babel/helpers - typeof";
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
- var match = toString.call(obj).match(/^\[object\s(.*)\]$/);
23
- var type = match && match[1];
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
- let allowArray = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
51
- let vals = allowArray && is('array', obj) ? [] : {};
44
+ const allowArray = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
45
+ const vals = allowArray && is('array', obj) ? [] : {};
52
46
 
53
- for (var key in obj) {
47
+ for (const key in obj) {
54
48
  if (hasOwn.call(obj, key)) {
55
- let val = obj[key];
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
- var subset = {};
84
- for (var key in model) {
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
- var expectedType = objectType(expected);
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
- var valid = !expected ||
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
- var result = false;
116
- var expectedType = objectType(expected);
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
- var resultErrorString = String(error);
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
- let parentModule = ModuleContext.currentModuleChain[ModuleContext.currentModuleChain.length - 1];
19
+ const parentModule = ModuleContext.currentModuleChain[ModuleContext.currentModuleChain.length - 1];
20
20
 
21
21
  ModuleContext.currentModuleChain.push(this);
22
22
 
package/Makefile DELETED
@@ -1,26 +0,0 @@
1
- .PHONY: check test lint build release
2
-
3
- check: lint test
4
-
5
- lint:
6
- npm run lint
7
-
8
- test:
9
- npm test
10
-
11
- build:
12
- npm run build
13
-
14
- # Lint, bump version, update changelog, commit, tag, push, publish to npm.
15
- # CI then creates the GitHub release.
16
- # Usage: make release LEVEL=patch|minor|major
17
- release:
18
- @test -n "$(LEVEL)" || (echo "Usage: make release LEVEL=patch|minor|major" && exit 1)
19
- npm run lint
20
- npm version $(LEVEL) --no-git-tag-version
21
- npm run changelog:update
22
- git add package.json package-lock.json CHANGELOG.md
23
- git commit -m "Release $$(node -p 'require("./package.json").version')"
24
- git tag "v$$(node -p 'require("./package.json").version')"
25
- git push && git push --tags
26
- npm publish --access public