qunitx-cli 0.5.7 → 0.6.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/README.md +29 -0
- package/deno.json +5 -0
- package/deno.lock +6 -6
- package/lib/commands/generate.js +2 -2
- package/lib/commands/help.js +4 -3
- package/lib/commands/run/tests-in-browser.js +2 -2
- package/lib/commands/run.js +4 -4
- package/lib/setup/default-project-config-values.js +2 -1
- package/lib/setup/file-watcher.js +55 -48
- package/lib/setup/fs-tree.js +14 -8
- package/lib/setup/keyboard-events.js +2 -5
- package/lib/tap/display-test-result.js +2 -3
- package/lib/tap/dump-yaml.js +70 -0
- package/lib/utils/color.js +53 -0
- package/lib/utils/parse-cli-flags.js +7 -0
- package/lib/utils/run-user-module.js +2 -2
- package/package.json +1 -4
- package/lib/setup/recursive-lookup.d.ts +0 -7
package/README.md
CHANGED
|
@@ -133,6 +133,34 @@ qunitx some-test.js --debug
|
|
|
133
133
|
qunitx some-test.ts
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
+
## Configuration
|
|
137
|
+
|
|
138
|
+
All CLI flags can also be set in `package.json` under the `qunitx` key, so you don't have to repeat them on every invocation:
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
{
|
|
142
|
+
"qunitx": {
|
|
143
|
+
"inputs": ["test/**/*-test.js", "test/**/*-test.ts"],
|
|
144
|
+
"extensions": ["js", "ts"],
|
|
145
|
+
"output": "tmp",
|
|
146
|
+
"timeout": 20000,
|
|
147
|
+
"failFast": false,
|
|
148
|
+
"port": 1234
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
| Key | Default | Description |
|
|
154
|
+
|-----|---------|-------------|
|
|
155
|
+
| `inputs` | `[]` | Glob patterns, file paths, or directories to use as test entry points. Merged with any paths given on the CLI. |
|
|
156
|
+
| `extensions` | `["js", "ts"]` | File extensions tracked for test discovery (directory scans) and watch-mode rebuild triggers. Add `"mjs"`, `"cjs"`, or any other extension your project uses. |
|
|
157
|
+
| `output` | `"tmp"` | Directory where compiled test bundles are written. |
|
|
158
|
+
| `timeout` | `20000` | Maximum milliseconds to wait for the full test suite before timing out. |
|
|
159
|
+
| `failFast` | `false` | Stop the run after the first failing test. |
|
|
160
|
+
| `port` | `1234` | Preferred HTTP server port. qunitx auto-selects a free port if this one is taken. |
|
|
161
|
+
|
|
162
|
+
CLI flags always override `package.json` values when both are present.
|
|
163
|
+
|
|
136
164
|
## CLI Reference
|
|
137
165
|
|
|
138
166
|
```
|
|
@@ -144,6 +172,7 @@ Options:
|
|
|
144
172
|
--debug Print the server URL; pipe browser console to stdout
|
|
145
173
|
--timeout=<ms> Max ms to wait for the suite to finish [default: 20000]
|
|
146
174
|
--output=<dir> Directory for compiled test assets [default: ./tmp]
|
|
175
|
+
--extensions=<...> Comma-separated file extensions to track [default: js,ts]
|
|
147
176
|
--before=<file> Script to run (and optionally await) before tests start
|
|
148
177
|
--after=<file> Script to run (and optionally await) after tests finish
|
|
149
178
|
--port=<n> HTTP server port (auto-selects a free port if taken)
|
package/deno.json
CHANGED
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
"recursive-lookup": "npm:recursive-lookup",
|
|
12
12
|
"ws": "npm:ws"
|
|
13
13
|
},
|
|
14
|
+
"tasks": {
|
|
15
|
+
"bench": "deno bench --allow-all benches/esbuild.bench.ts benches/server.bench.ts benches/tap.bench.ts benches/e2e.bench.ts",
|
|
16
|
+
"bench:check": "deno bench --allow-all --json benches/esbuild.bench.ts benches/server.bench.ts benches/tap.bench.ts benches/e2e.bench.ts | deno run --allow-all scripts/check-benchmarks.ts",
|
|
17
|
+
"bench:update": "deno bench --allow-all --json benches/esbuild.bench.ts benches/server.bench.ts benches/tap.bench.ts benches/e2e.bench.ts | deno run --allow-all scripts/check-benchmarks.ts --save"
|
|
18
|
+
},
|
|
14
19
|
"lint": {
|
|
15
20
|
"rules": {
|
|
16
21
|
"exclude": ["no-process-global", "no-node-globals", "no-window"]
|
package/deno.lock
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": "5",
|
|
3
3
|
"specifiers": {
|
|
4
|
+
"jsr:@std/fmt@*": "1.0.9",
|
|
4
5
|
"npm:@types/js-yaml@*": "4.0.9",
|
|
5
6
|
"npm:@types/picomatch@*": "4.0.2",
|
|
6
7
|
"npm:@types/ws@*": "8.18.1",
|
|
@@ -13,9 +14,7 @@
|
|
|
13
14
|
"npm:esbuild@~0.27.3": "0.27.4",
|
|
14
15
|
"npm:express@^5.2.1": "5.2.1",
|
|
15
16
|
"npm:js-yaml@*": "4.1.1",
|
|
16
|
-
"npm:js-yaml@^4.1.1": "4.1.1",
|
|
17
17
|
"npm:kleur@*": "4.1.5",
|
|
18
|
-
"npm:kleur@^4.1.5": "4.1.5",
|
|
19
18
|
"npm:picomatch@*": "4.0.3",
|
|
20
19
|
"npm:picomatch@^4.0.3": "4.0.3",
|
|
21
20
|
"npm:prettier@^3.8.1": "3.8.1",
|
|
@@ -24,10 +23,14 @@
|
|
|
24
23
|
"npm:qunit@^2.25.0": "2.25.0",
|
|
25
24
|
"npm:qunitx@1": "1.0.0",
|
|
26
25
|
"npm:recursive-lookup@*": "1.1.0",
|
|
27
|
-
"npm:recursive-lookup@1.1.0": "1.1.0",
|
|
28
26
|
"npm:ws@*": "8.19.0",
|
|
29
27
|
"npm:ws@^8.19.0": "8.19.0"
|
|
30
28
|
},
|
|
29
|
+
"jsr": {
|
|
30
|
+
"@std/fmt@1.0.9": {
|
|
31
|
+
"integrity": "2487343e8899fb2be5d0e3d35013e54477ada198854e52dd05ed0422eddcabe0"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
31
34
|
"npm": {
|
|
32
35
|
"@babel/code-frame@7.29.0": {
|
|
33
36
|
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
|
|
@@ -1326,14 +1329,11 @@
|
|
|
1326
1329
|
"npm:cors@^2.8.6",
|
|
1327
1330
|
"npm:esbuild@~0.27.3",
|
|
1328
1331
|
"npm:express@^5.2.1",
|
|
1329
|
-
"npm:js-yaml@^4.1.1",
|
|
1330
|
-
"npm:kleur@^4.1.5",
|
|
1331
1332
|
"npm:picomatch@^4.0.3",
|
|
1332
1333
|
"npm:prettier@^3.8.1",
|
|
1333
1334
|
"npm:puppeteer@^24.38.0",
|
|
1334
1335
|
"npm:qunit@^2.25.0",
|
|
1335
1336
|
"npm:qunitx@1",
|
|
1336
|
-
"npm:recursive-lookup@1.1.0",
|
|
1337
1337
|
"npm:ws@^8.19.0"
|
|
1338
1338
|
]
|
|
1339
1339
|
}
|
package/lib/commands/generate.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
-
import
|
|
2
|
+
import { green } from '../utils/color.js';
|
|
3
3
|
import findProjectRoot from '../utils/find-project-root.js';
|
|
4
4
|
import pathExists from '../utils/path-exists.js';
|
|
5
5
|
import readBoilerplate from '../utils/read-boilerplate.js';
|
|
@@ -29,5 +29,5 @@ export default async function generateTestFiles() {
|
|
|
29
29
|
await fs.mkdir(targetFolderPaths.join('/'), { recursive: true });
|
|
30
30
|
await fs.writeFile(path, testJSContent.replace('{{moduleName}}', moduleName));
|
|
31
31
|
|
|
32
|
-
console.log(
|
|
32
|
+
console.log(green(`${path} written`));
|
|
33
33
|
}
|
package/lib/commands/help.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { blue, magenta } from '../utils/color.js';
|
|
2
2
|
import pkg from '../../package.json' with { type: 'json' };
|
|
3
3
|
|
|
4
|
-
const highlight = (text) =>
|
|
5
|
-
const color = (text) =>
|
|
4
|
+
const highlight = (text) => magenta().bold(text);
|
|
5
|
+
const color = (text) => blue(text);
|
|
6
6
|
|
|
7
7
|
/** Prints qunitx-cli usage information to stdout. */
|
|
8
8
|
export default function displayHelpOutput() {
|
|
@@ -23,6 +23,7 @@ ${color('--timeout')} : change default timeout per test case
|
|
|
23
23
|
${color('--output')} : folder to distribute built qunitx html and js that a webservers can run[default: tmp]
|
|
24
24
|
${color('--failFast')} : run the target file or folders with immediate abort if a single test fails
|
|
25
25
|
${color('--port')} : HTTP server port (auto-selects a free port if the given port is taken)[default: 1234]
|
|
26
|
+
${color('--extensions')} : comma-separated file extensions to track for discovery and watch-mode rebuilds[default: js,ts]
|
|
26
27
|
${color('--before')} : run a script before the tests(i.e start a new web server before tests)
|
|
27
28
|
${color('--after')} : run a script after the tests(i.e save test results to a file)
|
|
28
29
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
-
import
|
|
2
|
+
import { blue } from '../../utils/color.js';
|
|
3
3
|
import esbuild from 'esbuild';
|
|
4
4
|
import timeCounter from '../../utils/time-counter.js';
|
|
5
5
|
import runUserModule from '../../utils/run-user-module.js';
|
|
@@ -141,7 +141,7 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
|
|
|
141
141
|
let QUNIT_RESULT;
|
|
142
142
|
let targetError;
|
|
143
143
|
try {
|
|
144
|
-
console.log('#',
|
|
144
|
+
console.log('#', blue(`QUnitX running: http://localhost:${config.port}${filePath}`));
|
|
145
145
|
|
|
146
146
|
const testsDone = new Promise((resolve) => {
|
|
147
147
|
config._testRunDone = resolve;
|
package/lib/commands/run.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import { normalize } from 'node:path';
|
|
3
3
|
import { availableParallelism } from 'node:os';
|
|
4
4
|
import Puppeteer from 'puppeteer';
|
|
5
|
-
import
|
|
5
|
+
import { blue, yellow } from '../utils/color.js';
|
|
6
6
|
import runTestsInBrowser, { buildTestBundle } from './run/tests-in-browser.js';
|
|
7
7
|
import setupBrowser from '../setup/browser.js';
|
|
8
8
|
import fileWatcher from '../setup/file-watcher.js';
|
|
@@ -170,7 +170,7 @@ async function buildCachedContent(config, htmlPaths) {
|
|
|
170
170
|
} else {
|
|
171
171
|
console.log(
|
|
172
172
|
'#',
|
|
173
|
-
|
|
173
|
+
yellow(
|
|
174
174
|
`WARNING: Static html file with no {{content}} detected. Therefore ignoring ${filePath}`,
|
|
175
175
|
),
|
|
176
176
|
);
|
|
@@ -225,11 +225,11 @@ function splitIntoGroups(files, groupCount) {
|
|
|
225
225
|
function logWatcherAndKeyboardShortcutInfo(config, _server) {
|
|
226
226
|
console.log(
|
|
227
227
|
'#',
|
|
228
|
-
|
|
228
|
+
blue(`Watching files... You can browse the tests on http://localhost:${config.port} ...`),
|
|
229
229
|
);
|
|
230
230
|
console.log(
|
|
231
231
|
'#',
|
|
232
|
-
|
|
232
|
+
blue(
|
|
233
233
|
`Shortcuts: Press "qq" to abort running tests, "qa" to run all the tests, "qf" to run last failing test, "ql" to repeat last test`,
|
|
234
234
|
),
|
|
235
235
|
);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
/** Default qunitx config values: build output directory, test timeout (ms), fail-fast flag,
|
|
1
|
+
/** Default qunitx config values: build output directory, test timeout (ms), fail-fast flag, HTTP server port, and tracked file extensions. */
|
|
2
2
|
export default {
|
|
3
3
|
output: 'tmp',
|
|
4
4
|
timeout: 20000,
|
|
5
5
|
failFast: false,
|
|
6
6
|
port: 1234,
|
|
7
|
+
extensions: ['js', 'ts'],
|
|
7
8
|
};
|
|
@@ -1,57 +1,19 @@
|
|
|
1
1
|
import chokidar from 'chokidar';
|
|
2
|
-
import
|
|
2
|
+
import { green, magenta, red, yellow } from '../utils/color.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Starts chokidar watchers for each lookup path and calls `onEventFunc` on JS/TS file changes, debounced via a global flag.
|
|
6
6
|
* @returns {object}
|
|
7
7
|
*/
|
|
8
8
|
export default function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFunc) {
|
|
9
|
-
const extensions = ['js', 'ts'];
|
|
9
|
+
const extensions = config.extensions || ['js', 'ts'];
|
|
10
10
|
const fileWatchers = testFileLookupPaths.reduce((watcher, watchPath) => {
|
|
11
11
|
return Object.assign(watcher, {
|
|
12
|
-
[watchPath]: chokidar
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
'#',
|
|
18
|
-
kleur
|
|
19
|
-
.magenta()
|
|
20
|
-
.bold('=================================================================='),
|
|
21
|
-
);
|
|
22
|
-
console.log('#', getEventColor(event), path.split(config.projectRoot)[1]);
|
|
23
|
-
console.log(
|
|
24
|
-
'#',
|
|
25
|
-
kleur
|
|
26
|
-
.magenta()
|
|
27
|
-
.bold('=================================================================='),
|
|
28
|
-
);
|
|
29
|
-
|
|
30
|
-
if (!global.chokidarBuild) {
|
|
31
|
-
global.chokidarBuild = true;
|
|
32
|
-
|
|
33
|
-
const result = extensions.some((extension) => path.endsWith(extension))
|
|
34
|
-
? onEventFunc(event, path)
|
|
35
|
-
: null;
|
|
36
|
-
|
|
37
|
-
if (!(result instanceof Promise)) {
|
|
38
|
-
global.chokidarBuild = false;
|
|
39
|
-
|
|
40
|
-
return result;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
result
|
|
44
|
-
.then(() => {
|
|
45
|
-
onFinishFunc ? onFinishFunc(event, path) : null;
|
|
46
|
-
})
|
|
47
|
-
.catch(() => {
|
|
48
|
-
// TODO: make an index.html to display the error
|
|
49
|
-
// error type has to be derived from the error!
|
|
50
|
-
})
|
|
51
|
-
.finally(() => (global.chokidarBuild = false));
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}),
|
|
12
|
+
[watchPath]: chokidar
|
|
13
|
+
.watch(watchPath, { ignoreInitial: true })
|
|
14
|
+
.on('all', (event, filePath) =>
|
|
15
|
+
handleWatchEvent(config, extensions, event, filePath, onEventFunc, onFinishFunc),
|
|
16
|
+
),
|
|
55
17
|
});
|
|
56
18
|
}, {});
|
|
57
19
|
|
|
@@ -65,6 +27,51 @@ export default function setupFileWatchers(testFileLookupPaths, config, onEventFu
|
|
|
65
27
|
};
|
|
66
28
|
}
|
|
67
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Routes a chokidar event to fsTree mutation and optional rebuild trigger.
|
|
32
|
+
* `unlinkDir` bypasses the extension filter so deleted directories always clean up fsTree.
|
|
33
|
+
* @returns {void}
|
|
34
|
+
*/
|
|
35
|
+
export function handleWatchEvent(config, extensions, event, filePath, onEventFunc, onFinishFunc) {
|
|
36
|
+
const isFileEvent = extensions.some((ext) => filePath.endsWith(`.${ext}`));
|
|
37
|
+
|
|
38
|
+
if (!isFileEvent && event !== 'unlinkDir') return;
|
|
39
|
+
|
|
40
|
+
mutateFSTree(config.fsTree, event, filePath);
|
|
41
|
+
|
|
42
|
+
console.log(
|
|
43
|
+
'#',
|
|
44
|
+
magenta().bold('=================================================================='),
|
|
45
|
+
);
|
|
46
|
+
console.log('#', getEventColor(event), filePath.split(config.projectRoot)[1]);
|
|
47
|
+
console.log(
|
|
48
|
+
'#',
|
|
49
|
+
magenta().bold('=================================================================='),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
if (!config._building) {
|
|
53
|
+
config._building = true;
|
|
54
|
+
|
|
55
|
+
const result = onEventFunc(event, filePath);
|
|
56
|
+
|
|
57
|
+
if (!(result instanceof Promise)) {
|
|
58
|
+
config._building = false;
|
|
59
|
+
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
result
|
|
64
|
+
.then(() => {
|
|
65
|
+
onFinishFunc ? onFinishFunc(event, filePath) : null;
|
|
66
|
+
})
|
|
67
|
+
.catch(() => {
|
|
68
|
+
// TODO: make an index.html to display the error
|
|
69
|
+
// error type has to be derived from the error!
|
|
70
|
+
})
|
|
71
|
+
.finally(() => (config._building = false));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
68
75
|
/**
|
|
69
76
|
* Mutates `fsTree` in place based on a chokidar file-system event.
|
|
70
77
|
* @returns {void}
|
|
@@ -83,10 +90,10 @@ export function mutateFSTree(fsTree, event, path) {
|
|
|
83
90
|
|
|
84
91
|
function getEventColor(event) {
|
|
85
92
|
if (event === 'change') {
|
|
86
|
-
return
|
|
93
|
+
return yellow('CHANGED:');
|
|
87
94
|
} else if (event === 'add' || event === 'addDir') {
|
|
88
|
-
return
|
|
95
|
+
return green('ADDED:');
|
|
89
96
|
} else if (event === 'unlink' || event === 'unlinkDir') {
|
|
90
|
-
return
|
|
97
|
+
return red('REMOVED:');
|
|
91
98
|
}
|
|
92
99
|
}
|
package/lib/setup/fs-tree.js
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
// @deno-types="npm:@types/picomatch"
|
|
3
4
|
import picomatch from 'picomatch';
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
async function readDirRecursive(dir, filter) {
|
|
7
|
+
const entries = await fs.readdir(dir, { recursive: true, withFileTypes: true });
|
|
8
|
+
return entries
|
|
9
|
+
.filter((e) => e.isFile() && filter(e.name))
|
|
10
|
+
.map((e) => path.join(e.parentPath, e.name));
|
|
11
|
+
}
|
|
6
12
|
|
|
7
13
|
/**
|
|
8
14
|
* Resolves an array of file paths, directories, or glob patterns into a flat `{ absolutePath: null }` map.
|
|
9
15
|
* @returns {Promise<object>}
|
|
10
16
|
*/
|
|
11
|
-
export default async function buildFSTree(fileAbsolutePaths,
|
|
12
|
-
const targetExtensions = ['js', 'ts'];
|
|
17
|
+
export default async function buildFSTree(fileAbsolutePaths, config = {}) {
|
|
18
|
+
const targetExtensions = config.extensions || ['js', 'ts'];
|
|
13
19
|
const fsTree = {};
|
|
14
20
|
|
|
15
21
|
await Promise.all(
|
|
@@ -20,8 +26,8 @@ export default async function buildFSTree(fileAbsolutePaths, _config = {}) {
|
|
|
20
26
|
|
|
21
27
|
try {
|
|
22
28
|
if (glob.isGlob) {
|
|
23
|
-
const fileNames = await
|
|
24
|
-
return targetExtensions.some((extension) =>
|
|
29
|
+
const fileNames = await readDirRecursive(glob.base, (name) => {
|
|
30
|
+
return targetExtensions.some((extension) => name.endsWith(`.${extension}`));
|
|
25
31
|
});
|
|
26
32
|
|
|
27
33
|
fileNames.forEach((fileName) => {
|
|
@@ -35,8 +41,8 @@ export default async function buildFSTree(fileAbsolutePaths, _config = {}) {
|
|
|
35
41
|
if (entry.isFile()) {
|
|
36
42
|
fsTree[fileAbsolutePath] = null;
|
|
37
43
|
} else if (entry.isDirectory()) {
|
|
38
|
-
const fileNames = await
|
|
39
|
-
return targetExtensions.some((extension) =>
|
|
44
|
+
const fileNames = await readDirRecursive(glob.base, (name) => {
|
|
45
|
+
return targetExtensions.some((extension) => name.endsWith(`.${extension}`));
|
|
40
46
|
});
|
|
41
47
|
|
|
42
48
|
fileNames.forEach((fileName) => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { blue } from '../utils/color.js';
|
|
2
2
|
import listenToKeyboardKey from '../utils/listen-to-keyboard-key.js';
|
|
3
3
|
import runTestsInBrowser from '../commands/run/tests-in-browser.js';
|
|
4
4
|
|
|
@@ -16,10 +16,7 @@ export default function setupKeyboardEvents(config, cachedContent, connections)
|
|
|
16
16
|
abortBrowserQUnit(config, connections);
|
|
17
17
|
|
|
18
18
|
if (!config.lastFailedTestFiles) {
|
|
19
|
-
console.log(
|
|
20
|
-
'#',
|
|
21
|
-
kleur.blue(`QUnitX: No tests failed so far, so repeating the last test run`),
|
|
22
|
-
);
|
|
19
|
+
console.log('#', blue(`QUnitX: No tests failed so far, so repeating the last test run`));
|
|
23
20
|
return runTestsInBrowser(config, cachedContent, connections, config.lastRanTestFiles);
|
|
24
21
|
}
|
|
25
22
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import yaml from 'js-yaml';
|
|
1
|
+
import dumpYaml from './dump-yaml.js';
|
|
3
2
|
import indentString from '../utils/indent-string.js';
|
|
4
3
|
|
|
5
4
|
// tape TAP output: ['operator', 'stack', 'at', 'expected', 'actual']
|
|
@@ -32,7 +31,7 @@ export default function TAPDisplayTestResult(COUNTER, details) {
|
|
|
32
31
|
console.log(' ---');
|
|
33
32
|
console.log(
|
|
34
33
|
indentString(
|
|
35
|
-
|
|
34
|
+
dumpYaml({
|
|
36
35
|
name: `Assertion #${index + 1}`, // TODO: check what happens on runtime errors
|
|
37
36
|
actual: assertion.actual
|
|
38
37
|
? JSON.parse(JSON.stringify(assertion.actual, getCircularReplacer()))
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal YAML serializer for TAP assertion failure blocks.
|
|
3
|
+
* Handles the fixed schema: { name, actual, expected, message, stack, at }.
|
|
4
|
+
* Values for actual/expected are pre-sanitized via JSON.parse(JSON.stringify(...)).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Single compiled regex covering all cases where a plain YAML scalar would be misread:
|
|
8
|
+
// - YAML reserved words (null, true, false, ~, yes, no, on, off — case-insensitive)
|
|
9
|
+
// - Starts with a YAML indicator: { [ ! | > ' " % @ `
|
|
10
|
+
// - Block indicators that need a space: - ? : at start of string
|
|
11
|
+
// - Document separator: ---
|
|
12
|
+
// - Looks like a number (integer, float, hex, octal, scientific)
|
|
13
|
+
// - Timestamp-like strings that YAML 1.1 auto-casts to Date
|
|
14
|
+
// - Contains ': ' (key–value) or '#' anywhere (comment)
|
|
15
|
+
// - Empty string
|
|
16
|
+
// Also covers single-letter YAML 1.1 booleans: y/Y → true, n/N → false
|
|
17
|
+
const NEEDS_QUOTING =
|
|
18
|
+
/^$|^(null|true|false|~|yes|no|on|off|y|n)$|^[{[!|>'"#%@`]|^[-?:](\s|$)|^---|^[-+]?(\d|\.\d)|^\d{4}-\d{2}-\d{2}|: |#/i;
|
|
19
|
+
|
|
20
|
+
function needsQuoting(str) {
|
|
21
|
+
return NEEDS_QUOTING.test(str);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function dumpString(str, indent) {
|
|
25
|
+
if (str === '') return "''";
|
|
26
|
+
if (str.includes('\n')) {
|
|
27
|
+
// Block scalar |- (strip trailing newline), each line indented by current indent + 2
|
|
28
|
+
return '|-\n' + str.replace(/^/gm, `${indent} `);
|
|
29
|
+
}
|
|
30
|
+
if (needsQuoting(str)) return `'${str.replace(/'/g, "''")}'`;
|
|
31
|
+
return str;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function dumpValue(value, indent) {
|
|
35
|
+
if (value === null || value === undefined) return 'null';
|
|
36
|
+
if (typeof value === 'boolean' || typeof value === 'number') return String(value);
|
|
37
|
+
if (typeof value === 'string') return dumpString(value, indent);
|
|
38
|
+
if (Array.isArray(value)) {
|
|
39
|
+
if (value.length === 0) return '[]';
|
|
40
|
+
const next = `${indent} `;
|
|
41
|
+
return '\n' + value.map((v) => `${next}- ${dumpValue(v, next)}`).join('\n');
|
|
42
|
+
}
|
|
43
|
+
// Plain object
|
|
44
|
+
const entries = Object.entries(value);
|
|
45
|
+
if (entries.length === 0) return '{}';
|
|
46
|
+
const next = `${indent} `;
|
|
47
|
+
return '\n' + entries.map(([k, v]) => `${next}${k}: ${dumpValue(v, next)}`).join('\n');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Emits `key: value\n` or `key:\n ...\n` — no trailing space before block scalars.
|
|
51
|
+
function yamlLine(key, value) {
|
|
52
|
+
const v = dumpValue(value, '');
|
|
53
|
+
return v[0] === '\n' ? `${key}:${v}\n` : `${key}: ${v}\n`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Serializes the fixed TAP assertion object to a YAML string.
|
|
58
|
+
* Uses a template literal (no Object.entries overhead) for the known top-level keys.
|
|
59
|
+
* @returns {string}
|
|
60
|
+
*/
|
|
61
|
+
export default function dumpYaml({ name, actual, expected, message, stack, at }) {
|
|
62
|
+
return (
|
|
63
|
+
`name: ${dumpString(name, '')}\n` +
|
|
64
|
+
yamlLine('actual', actual) +
|
|
65
|
+
yamlLine('expected', expected) +
|
|
66
|
+
yamlLine('message', message) +
|
|
67
|
+
yamlLine('stack', stack) +
|
|
68
|
+
yamlLine('at', at)
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal ANSI color helpers. Respects NO_COLOR, NODE_DISABLE_COLORS, FORCE_COLOR, and TTY
|
|
3
|
+
* detection — same logic as kleur.
|
|
4
|
+
*
|
|
5
|
+
* Use `createColors(enabled)` in tests to exercise both enabled and disabled branches directly.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export function createColors(enabled) {
|
|
9
|
+
const c = (open, close) => (text) =>
|
|
10
|
+
enabled ? `\x1b[${open}m${text}\x1b[${close}m` : String(text);
|
|
11
|
+
|
|
12
|
+
const red = c(31, 39);
|
|
13
|
+
const green = c(32, 39);
|
|
14
|
+
const yellow = c(33, 39);
|
|
15
|
+
const blue = c(34, 39);
|
|
16
|
+
|
|
17
|
+
/** `magenta(text)` — colored text. `magenta()` — chainable: `.bold(text)`. */
|
|
18
|
+
const magenta = (text) => {
|
|
19
|
+
if (text !== undefined) return enabled ? `\x1b[35m${text}\x1b[39m` : String(text);
|
|
20
|
+
return { bold: (t) => (enabled ? `\x1b[35m\x1b[1m${t}\x1b[22m\x1b[39m` : String(t)) };
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return { red, green, yellow, blue, magenta };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const enabled =
|
|
27
|
+
!process.env.NODE_DISABLE_COLORS &&
|
|
28
|
+
process.env.NO_COLOR == null &&
|
|
29
|
+
process.env.TERM !== 'dumb' &&
|
|
30
|
+
((process.env.FORCE_COLOR != null && process.env.FORCE_COLOR !== '0') || !!process.stdout?.isTTY);
|
|
31
|
+
|
|
32
|
+
const _c = createColors(enabled);
|
|
33
|
+
|
|
34
|
+
/** ANSI red text. */
|
|
35
|
+
export function red(text) {
|
|
36
|
+
return _c.red(text);
|
|
37
|
+
}
|
|
38
|
+
/** ANSI green text. */
|
|
39
|
+
export function green(text) {
|
|
40
|
+
return _c.green(text);
|
|
41
|
+
}
|
|
42
|
+
/** ANSI yellow text. */
|
|
43
|
+
export function yellow(text) {
|
|
44
|
+
return _c.yellow(text);
|
|
45
|
+
}
|
|
46
|
+
/** ANSI blue text. */
|
|
47
|
+
export function blue(text) {
|
|
48
|
+
return _c.blue(text);
|
|
49
|
+
}
|
|
50
|
+
/** ANSI magenta text. Call without arguments to chain: `magenta().bold(text)`. */
|
|
51
|
+
export function magenta(text) {
|
|
52
|
+
return _c.magenta(text);
|
|
53
|
+
}
|
|
@@ -26,6 +26,13 @@ export default function parseCliFlags(projectRoot) {
|
|
|
26
26
|
return result;
|
|
27
27
|
} else if (arg.startsWith('--port')) {
|
|
28
28
|
return Object.assign(result, { port: Number(arg.split('=')[1]) });
|
|
29
|
+
} else if (arg.startsWith('--extensions')) {
|
|
30
|
+
return Object.assign(result, {
|
|
31
|
+
extensions: arg
|
|
32
|
+
.split('=')[1]
|
|
33
|
+
.split(',')
|
|
34
|
+
.map((e) => e.trim()),
|
|
35
|
+
});
|
|
29
36
|
} else if (arg.startsWith('--before')) {
|
|
30
37
|
return Object.assign(result, { before: parseModule(arg.split('=')[1]) });
|
|
31
38
|
} else if (arg.startsWith('--after')) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { red } from './color.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Dynamically imports `modulePath` and calls its default export with `params`; exits with code 1 on error.
|
|
@@ -15,7 +15,7 @@ export default async function runUserModule(modulePath, params, scriptPosition)
|
|
|
15
15
|
: null;
|
|
16
16
|
}
|
|
17
17
|
} catch (error) {
|
|
18
|
-
console.log('#',
|
|
18
|
+
console.log('#', red(`QUnitX ${scriptPosition} script failed:`));
|
|
19
19
|
console.trace(error);
|
|
20
20
|
console.error(error);
|
|
21
21
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qunitx-cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.0",
|
|
5
5
|
"description": "Browser runner for QUnitx: run your qunitx tests in google-chrome",
|
|
6
6
|
"main": "cli.js",
|
|
7
7
|
"author": "Izel Nakri",
|
|
@@ -46,11 +46,8 @@
|
|
|
46
46
|
"cheerio": "^1.2.0",
|
|
47
47
|
"chokidar": "^5.0.0",
|
|
48
48
|
"esbuild": "^0.27.3",
|
|
49
|
-
"js-yaml": "^4.1.1",
|
|
50
|
-
"kleur": "^4.1.5",
|
|
51
49
|
"picomatch": "^4.0.3",
|
|
52
50
|
"puppeteer": "^24.38.0",
|
|
53
|
-
"recursive-lookup": "1.1.0",
|
|
54
51
|
"ws": "^8.19.0"
|
|
55
52
|
},
|
|
56
53
|
"devDependencies": {
|