qunitx-cli 0.9.10 → 0.10.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 +36 -9
- package/dist/cli.js +311 -72
- package/package.json +3 -3
- package/templates/setup/tests.hbs +1 -1
package/README.md
CHANGED
|
@@ -22,9 +22,12 @@ output to the terminal.
|
|
|
22
22
|
- `--watch` mode re-runs affected tests on file change
|
|
23
23
|
- `--failFast` stops the run after the first failing test
|
|
24
24
|
- `--debug` prints the local server URL and pipes browser console to stdout
|
|
25
|
+
- `--open` / `-o` opens the test output in the same browser the tests run in as soon as the bundle is ready; `--open=brave` opens in a specific binary instead
|
|
25
26
|
- `--before` / `--after` hook scripts for server setup and teardown
|
|
26
27
|
- `--timeout` controls the maximum ms to wait for the full suite to finish
|
|
28
|
+
- `--port` defaults to 1234 and auto-increments if taken; fails fast if an explicit port is unavailable
|
|
27
29
|
- `--browser` flag to run tests in Chromium, Firefox, or WebKit
|
|
30
|
+
- `--version` / `-v` prints the installed version
|
|
28
31
|
- Docker image for zero-install CI usage
|
|
29
32
|
|
|
30
33
|
## Installation
|
|
@@ -74,6 +77,13 @@ qunitx test/**/*.js --failFast
|
|
|
74
77
|
# Print the server URL and pipe browser console to stdout
|
|
75
78
|
qunitx test/**/*.js --debug
|
|
76
79
|
|
|
80
|
+
# Open output in the test browser as soon as the bundle is ready
|
|
81
|
+
qunitx test/**/*.js --open
|
|
82
|
+
|
|
83
|
+
# Open output in a specific browser binary instead
|
|
84
|
+
qunitx test/**/*.js --open=brave
|
|
85
|
+
qunitx test/**/*.js --open=google-chrome-lts
|
|
86
|
+
|
|
77
87
|
# Custom timeout (ms)
|
|
78
88
|
qunitx test/**/*.js --timeout=30000
|
|
79
89
|
|
|
@@ -89,6 +99,7 @@ qunitx test/**/*.js --browser=webkit
|
|
|
89
99
|
```
|
|
90
100
|
|
|
91
101
|
> **Prerequisite for Firefox / WebKit:** install the Playwright browser binaries once:
|
|
102
|
+
>
|
|
92
103
|
> ```sh
|
|
93
104
|
> npx playwright install firefox
|
|
94
105
|
> npx playwright install webkit
|
|
@@ -153,6 +164,7 @@ All CLI flags can also be set in `package.json` under the `qunitx` key, so you d
|
|
|
153
164
|
{
|
|
154
165
|
"qunitx": {
|
|
155
166
|
"inputs": ["test/**/*-test.js", "test/**/*-test.ts"],
|
|
167
|
+
"htmlPaths": ["test/tests.html"],
|
|
156
168
|
"extensions": ["js", "ts"],
|
|
157
169
|
"output": "tmp",
|
|
158
170
|
"timeout": 20000,
|
|
@@ -163,18 +175,31 @@ All CLI flags can also be set in `package.json` under the `qunitx` key, so you d
|
|
|
163
175
|
}
|
|
164
176
|
```
|
|
165
177
|
|
|
166
|
-
| Key
|
|
167
|
-
|
|
168
|
-
| `inputs`
|
|
169
|
-
| `
|
|
170
|
-
| `
|
|
171
|
-
| `
|
|
172
|
-
| `
|
|
173
|
-
| `
|
|
174
|
-
| `
|
|
178
|
+
| Key | Default | Description |
|
|
179
|
+
| ------------ | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
180
|
+
| `inputs` | `[]` | Glob patterns, file paths, or directories to use as test entry points. Merged with any paths given on the CLI. |
|
|
181
|
+
| `htmlPaths` | `[]` | Optional HTML templates to run tests inside. Any listed `.html` file that contains `{{qunitxScript}}` or other handlebars-style tokens is treated as a test runner template. |
|
|
182
|
+
| `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. |
|
|
183
|
+
| `output` | `"tmp"` | Directory where compiled test bundles are written. |
|
|
184
|
+
| `timeout` | `20000` | Maximum milliseconds to wait for the full test suite before timing out. |
|
|
185
|
+
| `failFast` | `false` | Stop the run after the first failing test. |
|
|
186
|
+
| `port` | `1234` | Preferred HTTP server port. qunitx auto-selects a free port if this one is taken. |
|
|
187
|
+
| `browser` | `"chromium"` | Browser engine to use: `"chromium"`, `"firefox"`, or `"webkit"`. Overridden by `--browser` on the CLI. |
|
|
175
188
|
|
|
176
189
|
CLI flags always override `package.json` values when both are present.
|
|
177
190
|
|
|
191
|
+
If you do not provide any HTML template, qunitx falls back to its built-in `test/tests.html` boilerplate internally, so `qunitx init` is optional.
|
|
192
|
+
|
|
193
|
+
You can also pass a custom HTML file on the CLI:
|
|
194
|
+
|
|
195
|
+
```sh
|
|
196
|
+
qunitx test/**/*.js custom.html
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
If that file contains `{{qunitxScript}}`, qunitx injects the runner script block at that exact spot. If it contains other handlebars-style tokens (e.g. `{{applicationName}}`), qunitx still treats it as a custom runner template and injects the runner before `</body>`.
|
|
200
|
+
|
|
201
|
+
The `{{qunitxScript}}` placeholder is replaced with a `<script>` tag containing the WebSocket runtime, QUnit event hooks, and the bundled test code.
|
|
202
|
+
|
|
178
203
|
## CLI Reference
|
|
179
204
|
|
|
180
205
|
```
|
|
@@ -189,6 +214,8 @@ Options:
|
|
|
189
214
|
--extensions=<...> Comma-separated file extensions to track [default: js,ts]
|
|
190
215
|
--before=<file> Script to run (and optionally await) before tests start
|
|
191
216
|
--after=<file> Script to run (and optionally await) after tests finish
|
|
217
|
+
--open, -o Open output in the test browser as soon as the bundle is ready
|
|
218
|
+
--open=<binary> Open output in a specific browser binary (e.g. brave, google-chrome-lts)
|
|
192
219
|
--port=<n> HTTP server port (auto-selects a free port if taken)
|
|
193
220
|
--browser=<name> Browser engine: chromium (default), firefox, or webkit
|
|
194
221
|
```
|
package/dist/cli.js
CHANGED
|
@@ -39,10 +39,11 @@ var init_find_chrome = __esm({
|
|
|
39
39
|
|
|
40
40
|
// lib/utils/pre-launch-chrome.ts
|
|
41
41
|
import { spawn } from "node:child_process";
|
|
42
|
-
function preLaunchChrome(chromePath, args) {
|
|
42
|
+
function preLaunchChrome(chromePath, args, headless = true) {
|
|
43
43
|
if (!chromePath) return Promise.resolve(null);
|
|
44
|
+
const headlessArgs = headless ? ["--headless=new"] : [];
|
|
44
45
|
return new Promise((resolve) => {
|
|
45
|
-
const proc = spawn(chromePath, ["--remote-debugging-port=0",
|
|
46
|
+
const proc = spawn(chromePath, ["--remote-debugging-port=0", ...headlessArgs, ...args], {
|
|
46
47
|
stdio: ["ignore", "ignore", "pipe"]
|
|
47
48
|
});
|
|
48
49
|
let buffer = "";
|
|
@@ -107,7 +108,7 @@ var init_perf_logger = __esm({
|
|
|
107
108
|
});
|
|
108
109
|
|
|
109
110
|
// lib/utils/early-chrome.ts
|
|
110
|
-
var NON_RUN_COMMANDS, isRunCommand, browserFromArgv, earlyChromeProcRef, earlyBrowserPromise;
|
|
111
|
+
var NON_RUN_COMMANDS, isRunCommand, browserFromArgv, openFromArgv, watchFromArgv, openWatchMode, earlyChromeProcRef, earlyBrowserPromise;
|
|
111
112
|
var init_early_chrome = __esm({
|
|
112
113
|
"lib/utils/early-chrome.ts"() {
|
|
113
114
|
init_find_chrome();
|
|
@@ -116,13 +117,24 @@ var init_early_chrome = __esm({
|
|
|
116
117
|
init_perf_logger();
|
|
117
118
|
NON_RUN_COMMANDS = /* @__PURE__ */ new Set(["help", "h", "p", "print", "new", "n", "g", "generate", "init"]);
|
|
118
119
|
isRunCommand = Boolean(process.argv[2]) && !NON_RUN_COMMANDS.has(process.argv[2]);
|
|
119
|
-
browserFromArgv = process.argv.
|
|
120
|
+
({ browserFromArgv, openFromArgv, watchFromArgv } = process.argv.reduce(
|
|
121
|
+
(flags, arg) => {
|
|
122
|
+
if (arg.startsWith("--browser=")) flags.browserFromArgv = arg.slice(10);
|
|
123
|
+
else if (arg === "--open" || arg === "-o") flags.openFromArgv = true;
|
|
124
|
+
else if (arg === "--watch" || arg === "-w") flags.watchFromArgv = true;
|
|
125
|
+
return flags;
|
|
126
|
+
},
|
|
127
|
+
{ browserFromArgv: "chromium", openFromArgv: false, watchFromArgv: false }
|
|
128
|
+
));
|
|
129
|
+
openWatchMode = openFromArgv && watchFromArgv;
|
|
120
130
|
earlyChromeProcRef = null;
|
|
121
|
-
|
|
131
|
+
if (!openWatchMode) {
|
|
132
|
+
process.on("exit", () => earlyChromeProcRef?.kill());
|
|
133
|
+
}
|
|
122
134
|
perfLog("early-chrome.js: module evaluated");
|
|
123
135
|
earlyBrowserPromise = isRunCommand && browserFromArgv === "chromium" ? findChrome().then((chromePath) => {
|
|
124
136
|
perfLog("early-chrome.js: findChrome resolved", chromePath);
|
|
125
|
-
return preLaunchChrome(chromePath, chromium_args_default);
|
|
137
|
+
return preLaunchChrome(chromePath, chromium_args_default, !openWatchMode);
|
|
126
138
|
}).then((info) => {
|
|
127
139
|
perfLog("early-chrome.js: Chrome CDP ready", info?.cdpEndpoint ?? null);
|
|
128
140
|
if (info) earlyChromeProcRef = info.proc;
|
|
@@ -220,6 +232,37 @@ var init_find_internal_assets_from_html = __esm({
|
|
|
220
232
|
}
|
|
221
233
|
});
|
|
222
234
|
|
|
235
|
+
// lib/utils/html-content-marker.ts
|
|
236
|
+
function findHTMLContentMarker(html) {
|
|
237
|
+
return html.includes(HTML_CONTENT_MARKER) ? HTML_CONTENT_MARKER : void 0;
|
|
238
|
+
}
|
|
239
|
+
function htmlHasDynamicContentMarker(html) {
|
|
240
|
+
return !!findHTMLContentMarker(html) || HANDLEBARS_TOKEN_REGEX.test(html);
|
|
241
|
+
}
|
|
242
|
+
function replaceHTMLContentMarker(html, content) {
|
|
243
|
+
const marker = findHTMLContentMarker(html);
|
|
244
|
+
if (marker) {
|
|
245
|
+
return html.replace(marker, content);
|
|
246
|
+
}
|
|
247
|
+
if (htmlHasDynamicContentMarker(html)) {
|
|
248
|
+
if (html.includes("</body>")) {
|
|
249
|
+
return html.replace("</body>", `${content}</body>`);
|
|
250
|
+
}
|
|
251
|
+
if (html.includes("</html>")) {
|
|
252
|
+
return html.replace("</html>", `${content}</html>`);
|
|
253
|
+
}
|
|
254
|
+
return `${html}${content}`;
|
|
255
|
+
}
|
|
256
|
+
return html;
|
|
257
|
+
}
|
|
258
|
+
var HTML_CONTENT_MARKER, HANDLEBARS_TOKEN_REGEX;
|
|
259
|
+
var init_html_content_marker = __esm({
|
|
260
|
+
"lib/utils/html-content-marker.ts"() {
|
|
261
|
+
HTML_CONTENT_MARKER = "{{qunitxScript}}";
|
|
262
|
+
HANDLEBARS_TOKEN_REGEX = /{{\s*[^}]+\s*}}/;
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
223
266
|
// lib/tap/dump-yaml.ts
|
|
224
267
|
function needsQuoting(str) {
|
|
225
268
|
return NEEDS_QUOTING.test(str);
|
|
@@ -354,7 +397,19 @@ var init_display_test_result = __esm({
|
|
|
354
397
|
|
|
355
398
|
// lib/setup/bind-server-to-port.ts
|
|
356
399
|
async function bindServerToPort(server, config) {
|
|
357
|
-
|
|
400
|
+
let port = config.port;
|
|
401
|
+
while (true) {
|
|
402
|
+
try {
|
|
403
|
+
await server.listen(port);
|
|
404
|
+
break;
|
|
405
|
+
} catch (err) {
|
|
406
|
+
if (err.code === "EADDRINUSE" && !config.portExplicit) {
|
|
407
|
+
port++;
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
throw err;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
358
413
|
config.port = server._server.address().port;
|
|
359
414
|
return server;
|
|
360
415
|
}
|
|
@@ -440,6 +495,7 @@ var init_http = __esm({
|
|
|
440
495
|
});
|
|
441
496
|
this.wss = new WebSocketServer({ server: this._server });
|
|
442
497
|
this.wss.on("error", (error) => {
|
|
498
|
+
if (error.code === "EADDRINUSE") return;
|
|
443
499
|
console.log("# [WebSocketServer] Error:");
|
|
444
500
|
console.log(error);
|
|
445
501
|
});
|
|
@@ -611,10 +667,17 @@ import path3 from "node:path";
|
|
|
611
667
|
function setupWebServer(config, cachedContent) {
|
|
612
668
|
const STATIC_FILES_PATH = path3.join(config.projectRoot, config.output);
|
|
613
669
|
const server = new HTTPServer();
|
|
670
|
+
const mainHTMLWithReplacedAssets = replaceAssetPaths(
|
|
671
|
+
cachedContent.mainHTML.html,
|
|
672
|
+
cachedContent.mainHTML.filePath,
|
|
673
|
+
config.projectRoot
|
|
674
|
+
);
|
|
614
675
|
server.wss.on("connection", function connection(socket) {
|
|
615
676
|
socket.on("message", function message(data) {
|
|
616
677
|
const { event, details, abort } = JSON.parse(data);
|
|
617
|
-
if (event === "
|
|
678
|
+
if (event === "wsOpen") {
|
|
679
|
+
config._onWsOpen?.();
|
|
680
|
+
} else if (event === "connection") {
|
|
618
681
|
if (!config._groupMode) console.log("TAP version 13");
|
|
619
682
|
config._resetTestTimeout?.();
|
|
620
683
|
} else if (event === "testEnd" && !abort) {
|
|
@@ -631,17 +694,22 @@ function setupWebServer(config, cachedContent) {
|
|
|
631
694
|
}
|
|
632
695
|
});
|
|
633
696
|
});
|
|
697
|
+
server.get("/tests.js", (_req, res) => {
|
|
698
|
+
res.writeHead(200, { "Content-Type": "application/javascript", "Cache-Control": "no-store" });
|
|
699
|
+
res.end(cachedContent.allTestCode);
|
|
700
|
+
});
|
|
701
|
+
server.get("/filtered-tests.js", (_req, res) => {
|
|
702
|
+
res.writeHead(200, { "Content-Type": "application/javascript", "Cache-Control": "no-store" });
|
|
703
|
+
res.end(cachedContent.filteredTestCode);
|
|
704
|
+
});
|
|
634
705
|
server.get("/", async (_req, res) => {
|
|
635
706
|
const TEST_RUNTIME_TO_INJECT = testRuntimeToInject(config.port, config);
|
|
636
707
|
const htmlContent = escapeAndInjectTestsToHTML(
|
|
637
|
-
|
|
638
|
-
cachedContent.mainHTML.html,
|
|
639
|
-
cachedContent.mainHTML.filePath,
|
|
640
|
-
config.projectRoot
|
|
641
|
-
),
|
|
708
|
+
mainHTMLWithReplacedAssets,
|
|
642
709
|
TEST_RUNTIME_TO_INJECT,
|
|
643
|
-
|
|
710
|
+
"./tests.js"
|
|
644
711
|
);
|
|
712
|
+
res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
|
|
645
713
|
res.write(htmlContent);
|
|
646
714
|
res.end();
|
|
647
715
|
return await fsPromise.writeFile(
|
|
@@ -652,14 +720,11 @@ function setupWebServer(config, cachedContent) {
|
|
|
652
720
|
server.get("/qunitx.html", async (_req, res) => {
|
|
653
721
|
const TEST_RUNTIME_TO_INJECT = testRuntimeToInject(config.port, config);
|
|
654
722
|
const htmlContent = escapeAndInjectTestsToHTML(
|
|
655
|
-
|
|
656
|
-
cachedContent.mainHTML.html,
|
|
657
|
-
cachedContent.mainHTML.filePath,
|
|
658
|
-
config.projectRoot
|
|
659
|
-
),
|
|
723
|
+
mainHTMLWithReplacedAssets,
|
|
660
724
|
TEST_RUNTIME_TO_INJECT,
|
|
661
|
-
|
|
725
|
+
"./filtered-tests.js"
|
|
662
726
|
);
|
|
727
|
+
res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
|
|
663
728
|
res.write(htmlContent);
|
|
664
729
|
res.end();
|
|
665
730
|
return await fsPromise.writeFile(
|
|
@@ -674,8 +739,9 @@ function setupWebServer(config, cachedContent) {
|
|
|
674
739
|
const htmlContent = escapeAndInjectTestsToHTML(
|
|
675
740
|
possibleDynamicHTML,
|
|
676
741
|
TEST_RUNTIME_TO_INJECT,
|
|
677
|
-
|
|
742
|
+
"/tests.js"
|
|
678
743
|
);
|
|
744
|
+
res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
|
|
679
745
|
res.write(htmlContent);
|
|
680
746
|
res.end();
|
|
681
747
|
return await fsPromise.writeFile(
|
|
@@ -715,8 +781,31 @@ function testRuntimeToInject(port, config) {
|
|
|
715
781
|
}, 1000);
|
|
716
782
|
|
|
717
783
|
(function() {
|
|
784
|
+
// wsOpenStatus: true once the WebSocket 'open' event fires (or immediately for static files).
|
|
785
|
+
// testsLoaded: true once tests.js has executed and dispatched 'qunitx:tests-ready'.
|
|
786
|
+
// Both must be true before setupQUnit() is called, ensuring QUnit.start() only runs
|
|
787
|
+
// after all test modules are registered.
|
|
788
|
+
let wsOpenStatus = window.location.protocol === 'file:';
|
|
789
|
+
let testsLoaded = false;
|
|
790
|
+
|
|
791
|
+
function maybeStart() {
|
|
792
|
+
if (wsOpenStatus && testsLoaded) setupQUnit();
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// tests.js (loaded as an async external script) dispatches this event after registering
|
|
796
|
+
// all test modules. Decoupled from WS so Chrome can compile tests.js in a background
|
|
797
|
+
// thread while the main thread handles the WebSocket handshake.
|
|
798
|
+
window.addEventListener('qunitx:tests-ready', function() {
|
|
799
|
+
testsLoaded = true;
|
|
800
|
+
maybeStart();
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
// For static files (file:// protocol) there is no WebSocket server.
|
|
804
|
+
// wsOpenStatus is already true above; setupQUnit fires when tests load.
|
|
805
|
+
if (window.location.protocol === 'file:') return;
|
|
806
|
+
|
|
718
807
|
let wsRetryCount = 0;
|
|
719
|
-
const WS_MAX_RETRIES =
|
|
808
|
+
const WS_MAX_RETRIES = Math.ceil(${config.timeout} / 10); // retry for the full test timeout window
|
|
720
809
|
|
|
721
810
|
function setupWebSocket() {
|
|
722
811
|
try {
|
|
@@ -728,7 +817,14 @@ function testRuntimeToInject(port, config) {
|
|
|
728
817
|
}
|
|
729
818
|
|
|
730
819
|
window.socket.addEventListener('open', function() {
|
|
731
|
-
|
|
820
|
+
wsOpenStatus = true;
|
|
821
|
+
// Notify Node.js that the WS socket is open. This fires immediately (< 1 s) because
|
|
822
|
+
// this runtime script is tiny \u2014 tests.js background compilation hasn't finished yet.
|
|
823
|
+
// Node.js uses this to distinguish "WS never connected" from "WS connected but bundle slow".
|
|
824
|
+
if (window.IS_PLAYWRIGHT) {
|
|
825
|
+
window.socket.send(JSON.stringify({ event: 'wsOpen' }));
|
|
826
|
+
}
|
|
827
|
+
maybeStart();
|
|
732
828
|
});
|
|
733
829
|
window.socket.addEventListener('error', function() {
|
|
734
830
|
retryOrFail();
|
|
@@ -757,8 +853,6 @@ function testRuntimeToInject(port, config) {
|
|
|
757
853
|
setupWebSocket();
|
|
758
854
|
})();
|
|
759
855
|
|
|
760
|
-
{{allTestCode}}
|
|
761
|
-
|
|
762
856
|
function getCircularReplacer() {
|
|
763
857
|
const ancestors = [];
|
|
764
858
|
return function (key, value) {
|
|
@@ -829,17 +923,18 @@ function testRuntimeToInject(port, config) {
|
|
|
829
923
|
}
|
|
830
924
|
</script>`;
|
|
831
925
|
}
|
|
832
|
-
function escapeAndInjectTestsToHTML(html, testRuntimeCode,
|
|
833
|
-
return
|
|
834
|
-
|
|
835
|
-
testRuntimeCode
|
|
836
|
-
|
|
926
|
+
function escapeAndInjectTestsToHTML(html, testRuntimeCode, testBundleUrl) {
|
|
927
|
+
return replaceHTMLContentMarker(
|
|
928
|
+
html,
|
|
929
|
+
`${testRuntimeCode}
|
|
930
|
+
<script src="${testBundleUrl}" async></script>`
|
|
837
931
|
);
|
|
838
932
|
}
|
|
839
933
|
var fsPromise;
|
|
840
934
|
var init_web_server = __esm({
|
|
841
935
|
"lib/setup/web-server.ts"() {
|
|
842
936
|
init_find_internal_assets_from_html();
|
|
937
|
+
init_html_content_marker();
|
|
843
938
|
init_display_test_result();
|
|
844
939
|
init_path_exists();
|
|
845
940
|
init_http();
|
|
@@ -869,12 +964,28 @@ async function launchBrowser(config) {
|
|
|
869
964
|
return browser;
|
|
870
965
|
}
|
|
871
966
|
const executablePath = await findChrome();
|
|
872
|
-
const launchOptions = {
|
|
967
|
+
const launchOptions = {
|
|
968
|
+
args: chromium_args_default,
|
|
969
|
+
headless: true,
|
|
970
|
+
// Disable Playwright's async SIGTERM/SIGHUP handlers. When the CLI is killed by an
|
|
971
|
+
// external signal (e.g. exec() timeout in tests), those handlers start an async browser
|
|
972
|
+
// graceful-close that can hang indefinitely on CI, preventing the process from exiting
|
|
973
|
+
// and blocking the test runner. With these disabled, Node.js's default signal behaviour
|
|
974
|
+
// (synchronous process.exit) runs instead, and Playwright's synchronous exitHandler
|
|
975
|
+
// (registered via process.on('exit')) still kills the browser correctly.
|
|
976
|
+
handleSIGTERM: false,
|
|
977
|
+
handleSIGHUP: false
|
|
978
|
+
};
|
|
873
979
|
if (executablePath) launchOptions.executablePath = executablePath;
|
|
874
980
|
return playwrightCore2.chromium.launch(launchOptions);
|
|
875
981
|
}
|
|
876
982
|
const playwrightCore = await playwrightCorePromise;
|
|
877
|
-
return playwrightCore[browserName].launch({
|
|
983
|
+
return playwrightCore[browserName].launch({
|
|
984
|
+
headless: !(config.open && config.watch),
|
|
985
|
+
// See comment in the chromium fallback path above for why these are disabled.
|
|
986
|
+
handleSIGTERM: false,
|
|
987
|
+
handleSIGHUP: false
|
|
988
|
+
});
|
|
878
989
|
}
|
|
879
990
|
async function setupBrowser(config, cachedContent, existingBrowser = null) {
|
|
880
991
|
const setupStart = Date.now();
|
|
@@ -919,6 +1030,42 @@ var init_browser = __esm({
|
|
|
919
1030
|
}
|
|
920
1031
|
});
|
|
921
1032
|
|
|
1033
|
+
// lib/utils/open-output-in-browser.ts
|
|
1034
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
1035
|
+
async function openOutputInBrowser(config) {
|
|
1036
|
+
try {
|
|
1037
|
+
const outputFile = config.watch ? `http://localhost:${config.port}` : `file://${config.projectRoot}/${config.output}/index.html`;
|
|
1038
|
+
if (typeof config.open === "string") {
|
|
1039
|
+
spawnDetached(config.open, [outputFile]);
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
const browserName = config.browser || "chromium";
|
|
1043
|
+
if (browserName === "firefox") {
|
|
1044
|
+
spawnDetached("firefox", [outputFile]);
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
if (browserName === "webkit") {
|
|
1048
|
+
if (process.platform === "darwin") spawnDetached("open", ["-a", "Safari", outputFile]);
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
const chromePath = await findChrome() ?? (await import("playwright-core")).chromium.executablePath();
|
|
1052
|
+
if (chromePath) spawnDetached(chromePath, [outputFile]);
|
|
1053
|
+
} catch (err) {
|
|
1054
|
+
console.error("# Warning: --open could not launch browser:", err);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
function spawnDetached(cmd, args) {
|
|
1058
|
+
const child = spawn2(cmd, args, { detached: true, stdio: "ignore" });
|
|
1059
|
+
child.on("error", () => {
|
|
1060
|
+
});
|
|
1061
|
+
child.unref();
|
|
1062
|
+
}
|
|
1063
|
+
var init_open_output_in_browser = __esm({
|
|
1064
|
+
"lib/utils/open-output-in-browser.ts"() {
|
|
1065
|
+
init_find_chrome();
|
|
1066
|
+
}
|
|
1067
|
+
});
|
|
1068
|
+
|
|
922
1069
|
// lib/utils/time-counter.ts
|
|
923
1070
|
function timeCounter() {
|
|
924
1071
|
const startTime = /* @__PURE__ */ new Date();
|
|
@@ -984,7 +1131,12 @@ async function buildTestBundle(config, cachedContent) {
|
|
|
984
1131
|
logLevel: "error",
|
|
985
1132
|
outfile: `${projectRoot}/${output}/tests.js`,
|
|
986
1133
|
keepNames: true,
|
|
987
|
-
sourcemap: config.debug
|
|
1134
|
+
sourcemap: config.debug ? "inline" : config.watch ? "linked" : false,
|
|
1135
|
+
// Signal the runtime that all test modules are registered. The runtime's maybeStart()
|
|
1136
|
+
// waits for both this event and the WebSocket 'open' event before calling QUnit.start().
|
|
1137
|
+
// Dispatching from the bundle (rather than from a script onload attr) is reliable across
|
|
1138
|
+
// all browsers and does not require changes to user test code.
|
|
1139
|
+
footer: { js: 'window.dispatchEvent(new CustomEvent("qunitx:tests-ready"));' }
|
|
988
1140
|
}),
|
|
989
1141
|
Promise.all(
|
|
990
1142
|
cachedContent.htmlPathsToRunTests.map(async (htmlPath) => {
|
|
@@ -1060,26 +1212,41 @@ function buildFilteredTests(filteredTests, outputPath, config) {
|
|
|
1060
1212
|
bundle: true,
|
|
1061
1213
|
logLevel: "error",
|
|
1062
1214
|
outfile: outputPath,
|
|
1063
|
-
sourcemap: config.debug
|
|
1215
|
+
sourcemap: config.debug ? "inline" : config.watch ? "linked" : false,
|
|
1216
|
+
footer: { js: 'window.dispatchEvent(new CustomEvent("qunitx:tests-ready"));' }
|
|
1064
1217
|
});
|
|
1065
1218
|
}
|
|
1066
1219
|
async function runTestInsideHTMLFile(filePath, { page, server, browser }, config) {
|
|
1067
1220
|
let QUNIT_RESULT;
|
|
1068
1221
|
let targetError;
|
|
1069
1222
|
let timeoutHandle;
|
|
1223
|
+
let wsConnected = false;
|
|
1070
1224
|
try {
|
|
1071
1225
|
console.log("#", blue(`QUnitX running: http://localhost:${config.port}${filePath}`));
|
|
1226
|
+
let resolveTestRace;
|
|
1072
1227
|
const testRaceResult = new Promise((resolve) => {
|
|
1073
|
-
|
|
1074
|
-
config._resetTestTimeout = () => {
|
|
1075
|
-
clearTimeout(timeoutHandle);
|
|
1076
|
-
timeoutHandle = setTimeout(() => resolve(true), config.timeout);
|
|
1077
|
-
};
|
|
1228
|
+
resolveTestRace = resolve;
|
|
1078
1229
|
});
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1230
|
+
config._testRunDone = resolveTestRace;
|
|
1231
|
+
config._onWsOpen = () => {
|
|
1232
|
+
wsConnected = true;
|
|
1233
|
+
clearTimeout(timeoutHandle);
|
|
1234
|
+
timeoutHandle = setTimeout(resolveTestRace, config.timeout * 3);
|
|
1235
|
+
};
|
|
1236
|
+
config._resetTestTimeout = () => {
|
|
1237
|
+
wsConnected = true;
|
|
1238
|
+
clearTimeout(timeoutHandle);
|
|
1239
|
+
timeoutHandle = setTimeout(resolveTestRace, config.timeout);
|
|
1240
|
+
};
|
|
1241
|
+
const targetUrl = `http://localhost:${config.port}${filePath}`;
|
|
1242
|
+
const navOptions = { timeout: config.timeout + 1e4, waitUntil: "commit" };
|
|
1243
|
+
if (page.url().split("?")[0] === targetUrl) {
|
|
1244
|
+
await page.reload(navOptions);
|
|
1245
|
+
} else {
|
|
1246
|
+
await page.goto(targetUrl, navOptions);
|
|
1247
|
+
}
|
|
1248
|
+
clearTimeout(timeoutHandle);
|
|
1249
|
+
timeoutHandle = setTimeout(resolveTestRace, config.timeout * 3);
|
|
1083
1250
|
await testRaceResult;
|
|
1084
1251
|
QUNIT_RESULT = await page.evaluate(() => window.QUNIT_RESULT);
|
|
1085
1252
|
} catch (error) {
|
|
@@ -1088,15 +1255,22 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
|
|
|
1088
1255
|
console.error(error);
|
|
1089
1256
|
} finally {
|
|
1090
1257
|
clearTimeout(timeoutHandle);
|
|
1258
|
+
config._onWsOpen = null;
|
|
1091
1259
|
config._resetTestTimeout = null;
|
|
1260
|
+
config._testRunDone = null;
|
|
1092
1261
|
}
|
|
1093
1262
|
if (!QUNIT_RESULT || QUNIT_RESULT.totalTests === 0) {
|
|
1094
|
-
console.log(targetError);
|
|
1263
|
+
if (targetError) console.log(targetError);
|
|
1264
|
+
const wsReason = !wsConnected ? "WebSocket connection never received \u2014 Chrome may be CPU-starved or the page failed to load" : "WebSocket connected but no tests ran \u2014 QUnit may have failed to start";
|
|
1265
|
+
console.log(`# TIMEOUT: ${wsReason}`);
|
|
1095
1266
|
console.log("BROWSER: runtime error thrown during executing tests");
|
|
1096
1267
|
console.error("BROWSER: runtime error thrown during executing tests");
|
|
1097
1268
|
await failOnNonWatchMode(config.watch, { server, browser }, config._groupMode);
|
|
1098
1269
|
} else if (QUNIT_RESULT.totalTests > QUNIT_RESULT.finishedTests) {
|
|
1099
|
-
console.log(targetError);
|
|
1270
|
+
if (targetError) console.log(targetError);
|
|
1271
|
+
console.log(
|
|
1272
|
+
`# TIMEOUT: test stalled after ${QUNIT_RESULT.finishedTests}/${QUNIT_RESULT.totalTests} finished \u2014 last active: ${QUNIT_RESULT.currentTest}`
|
|
1273
|
+
);
|
|
1100
1274
|
console.log(`BROWSER: TEST TIMED OUT: ${QUNIT_RESULT.currentTest}`);
|
|
1101
1275
|
console.error(`BROWSER: TEST TIMED OUT: ${QUNIT_RESULT.currentTest}`);
|
|
1102
1276
|
await failOnNonWatchMode(config.watch, { server, browser }, config._groupMode);
|
|
@@ -1139,12 +1313,21 @@ import { stat } from "node:fs/promises";
|
|
|
1139
1313
|
import path4 from "node:path";
|
|
1140
1314
|
function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFunc) {
|
|
1141
1315
|
const extensions = config.extensions || ["js", "ts"];
|
|
1316
|
+
const readyPromises = [];
|
|
1317
|
+
const parentWatchers = [];
|
|
1142
1318
|
const fileWatchers = testFileLookupPaths.reduce((watchers, watchPath) => {
|
|
1143
1319
|
let ready = false;
|
|
1144
|
-
const
|
|
1320
|
+
const lastChangeMs = {};
|
|
1321
|
+
const CHANGE_DEDUPE_MS = 30;
|
|
1322
|
+
const childWatcher = fs9.watch(watchPath, { recursive: true }, async (eventType, filename) => {
|
|
1145
1323
|
if (!ready || !filename) return;
|
|
1146
1324
|
const fullPath = path4.join(watchPath, filename);
|
|
1147
1325
|
if (eventType === "change") {
|
|
1326
|
+
if (!config._building) {
|
|
1327
|
+
const now = Date.now();
|
|
1328
|
+
if (now - (lastChangeMs[fullPath] ?? 0) < CHANGE_DEDUPE_MS) return;
|
|
1329
|
+
lastChangeMs[fullPath] = now;
|
|
1330
|
+
}
|
|
1148
1331
|
return handleWatchEvent(config, extensions, "change", fullPath, onEventFunc, onFinishFunc);
|
|
1149
1332
|
}
|
|
1150
1333
|
try {
|
|
@@ -1158,19 +1341,41 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
1158
1341
|
onFinishFunc
|
|
1159
1342
|
);
|
|
1160
1343
|
} catch {
|
|
1161
|
-
|
|
1162
|
-
handleWatchEvent(config, extensions,
|
|
1344
|
+
if (!(config.fsTree && fullPath in config.fsTree)) return;
|
|
1345
|
+
handleWatchEvent(config, extensions, "unlink", fullPath, onEventFunc, onFinishFunc);
|
|
1163
1346
|
}
|
|
1164
1347
|
});
|
|
1165
|
-
|
|
1166
|
-
|
|
1348
|
+
const parentDir = path4.dirname(watchPath);
|
|
1349
|
+
const watchedBasename = path4.basename(watchPath);
|
|
1350
|
+
const parentWatcher = fs9.watch(parentDir, async (eventType, filename) => {
|
|
1351
|
+
if (!ready || filename !== watchedBasename || eventType !== "rename") return;
|
|
1352
|
+
try {
|
|
1353
|
+
await stat(watchPath);
|
|
1354
|
+
} catch {
|
|
1355
|
+
handleWatchEvent(config, extensions, "unlinkDir", watchPath, onEventFunc, onFinishFunc);
|
|
1356
|
+
childWatcher.close();
|
|
1357
|
+
parentWatcher.close();
|
|
1358
|
+
delete watchers[watchPath];
|
|
1359
|
+
}
|
|
1167
1360
|
});
|
|
1168
|
-
|
|
1361
|
+
parentWatchers.push(parentWatcher);
|
|
1362
|
+
readyPromises.push(
|
|
1363
|
+
new Promise(
|
|
1364
|
+
(resolve) => setImmediate(() => {
|
|
1365
|
+
ready = true;
|
|
1366
|
+
resolve();
|
|
1367
|
+
})
|
|
1368
|
+
)
|
|
1369
|
+
);
|
|
1370
|
+
return Object.assign(watchers, { [watchPath]: childWatcher });
|
|
1169
1371
|
}, {});
|
|
1170
1372
|
return {
|
|
1171
1373
|
fileWatchers,
|
|
1374
|
+
ready: Promise.all(readyPromises).then(() => {
|
|
1375
|
+
}),
|
|
1172
1376
|
killFileWatchers() {
|
|
1173
|
-
Object.keys(fileWatchers).forEach((
|
|
1377
|
+
Object.keys(fileWatchers).forEach((key) => fileWatchers[key].close());
|
|
1378
|
+
parentWatchers.forEach((pw) => pw.close());
|
|
1174
1379
|
return fileWatchers;
|
|
1175
1380
|
}
|
|
1176
1381
|
};
|
|
@@ -1199,7 +1404,16 @@ function handleWatchEvent(config, extensions, event, filePath, onEventFunc, onFi
|
|
|
1199
1404
|
onFinishFunc ? onFinishFunc(event, filePath) : null;
|
|
1200
1405
|
}).catch((error) => {
|
|
1201
1406
|
console.error("#", red("Build error:"), error.message || error);
|
|
1202
|
-
}).finally(() =>
|
|
1407
|
+
}).finally(() => {
|
|
1408
|
+
config._building = false;
|
|
1409
|
+
if (config._pendingBuildTrigger) {
|
|
1410
|
+
const trigger = config._pendingBuildTrigger;
|
|
1411
|
+
config._pendingBuildTrigger = null;
|
|
1412
|
+
trigger();
|
|
1413
|
+
}
|
|
1414
|
+
});
|
|
1415
|
+
} else {
|
|
1416
|
+
config._pendingBuildTrigger = () => handleWatchEvent(config, extensions, event, filePath, onEventFunc, onFinishFunc);
|
|
1203
1417
|
}
|
|
1204
1418
|
}
|
|
1205
1419
|
function mutateFSTree(fsTree, event, path5) {
|
|
@@ -1346,6 +1560,9 @@ async function run(config) {
|
|
|
1346
1560
|
]);
|
|
1347
1561
|
config.expressApp = connections.server;
|
|
1348
1562
|
setupKeyboardEvents(config, cachedContent, connections);
|
|
1563
|
+
if (config.open) {
|
|
1564
|
+
void openOutputInBrowser(config);
|
|
1565
|
+
}
|
|
1349
1566
|
if (config.before) {
|
|
1350
1567
|
await runUserModule(`${process.cwd()}/${config.before}`, config, "before");
|
|
1351
1568
|
}
|
|
@@ -1358,19 +1575,24 @@ async function run(config) {
|
|
|
1358
1575
|
]);
|
|
1359
1576
|
throw error;
|
|
1360
1577
|
}
|
|
1578
|
+
if (config.watch) {
|
|
1579
|
+
const { ready: watcherReady } = setupFileWatchers(
|
|
1580
|
+
config.testFileLookupPaths,
|
|
1581
|
+
config,
|
|
1582
|
+
async (event, file) => {
|
|
1583
|
+
if (event === "addDir") return;
|
|
1584
|
+
if (["change", "unlink", "unlinkDir"].includes(event)) {
|
|
1585
|
+
if (event === "change" && !(file in config.fsTree)) return;
|
|
1586
|
+
cachedContent.allTestCode = null;
|
|
1587
|
+
return await runTestsInBrowser(config, cachedContent, connections);
|
|
1588
|
+
}
|
|
1589
|
+
await runTestsInBrowser(config, cachedContent, connections, [file]);
|
|
1590
|
+
},
|
|
1591
|
+
(_path, _event) => connections.server.publish("refresh", "refresh")
|
|
1592
|
+
);
|
|
1593
|
+
await watcherReady;
|
|
1594
|
+
}
|
|
1361
1595
|
logWatcherAndKeyboardShortcutInfo(config, connections.server);
|
|
1362
|
-
await setupFileWatchers(
|
|
1363
|
-
config.testFileLookupPaths,
|
|
1364
|
-
config,
|
|
1365
|
-
async (event, file) => {
|
|
1366
|
-
if (event === "addDir") return;
|
|
1367
|
-
if (["unlink", "unlinkDir"].includes(event)) {
|
|
1368
|
-
return await runTestsInBrowser(config, cachedContent, connections);
|
|
1369
|
-
}
|
|
1370
|
-
await runTestsInBrowser(config, cachedContent, connections, [file]);
|
|
1371
|
-
},
|
|
1372
|
-
(_path, _event) => connections.server.publish("refresh", "refresh")
|
|
1373
|
-
);
|
|
1374
1596
|
} else {
|
|
1375
1597
|
const allFiles = Object.keys(config.fsTree);
|
|
1376
1598
|
const groupCount = Math.min(allFiles.length, availableParallelism());
|
|
@@ -1397,6 +1619,9 @@ async function run(config) {
|
|
|
1397
1619
|
)
|
|
1398
1620
|
)
|
|
1399
1621
|
]);
|
|
1622
|
+
if (config.open) {
|
|
1623
|
+
void openOutputInBrowser(config);
|
|
1624
|
+
}
|
|
1400
1625
|
const TIME_COUNTER = timeCounter();
|
|
1401
1626
|
const GROUP_TIMEOUT_MS = 3 * 60 * 1e3;
|
|
1402
1627
|
const keepAlive = setInterval(() => {
|
|
@@ -1473,14 +1698,14 @@ async function buildCachedContent(config, htmlPaths) {
|
|
|
1473
1698
|
if (buffer === null) return result;
|
|
1474
1699
|
const filePath = config.htmlPaths[index];
|
|
1475
1700
|
const html = buffer.toString();
|
|
1476
|
-
if (html
|
|
1701
|
+
if (htmlHasDynamicContentMarker(html)) {
|
|
1477
1702
|
result.dynamicContentHTMLs[filePath] = html;
|
|
1478
1703
|
result.htmlPathsToRunTests.push(filePath.replace(config.projectRoot, ""));
|
|
1479
1704
|
} else {
|
|
1480
1705
|
console.log(
|
|
1481
1706
|
"#",
|
|
1482
1707
|
yellow(
|
|
1483
|
-
`WARNING: Static html file with no {{
|
|
1708
|
+
`WARNING: Static html file with no {{qunitxScript}} or handlebars-style tokens detected. Therefore ignoring ${filePath}`
|
|
1484
1709
|
)
|
|
1485
1710
|
);
|
|
1486
1711
|
result.staticHTMLs[filePath] = html;
|
|
@@ -1524,9 +1749,10 @@ function splitIntoGroups(files, groupCount) {
|
|
|
1524
1749
|
return groups.filter((g) => g.length > 0);
|
|
1525
1750
|
}
|
|
1526
1751
|
function logWatcherAndKeyboardShortcutInfo(config, _server) {
|
|
1752
|
+
const prefix = "Watching files...";
|
|
1527
1753
|
console.log(
|
|
1528
1754
|
"#",
|
|
1529
|
-
blue(
|
|
1755
|
+
blue(`${prefix} You can browse the tests on http://localhost:${config.port} ...`)
|
|
1530
1756
|
);
|
|
1531
1757
|
console.log(
|
|
1532
1758
|
"#",
|
|
@@ -1542,6 +1768,7 @@ function normalizeInternalAssetPathFromHTML(projectRoot, assetPath, htmlPath) {
|
|
|
1542
1768
|
var init_run = __esm({
|
|
1543
1769
|
"lib/commands/run.ts"() {
|
|
1544
1770
|
init_browser();
|
|
1771
|
+
init_open_output_in_browser();
|
|
1545
1772
|
init_color();
|
|
1546
1773
|
init_tests_in_browser();
|
|
1547
1774
|
init_file_watcher();
|
|
@@ -1552,6 +1779,7 @@ var init_run = __esm({
|
|
|
1552
1779
|
init_time_counter();
|
|
1553
1780
|
init_display_final_result();
|
|
1554
1781
|
init_read_boilerplate();
|
|
1782
|
+
init_html_content_marker();
|
|
1555
1783
|
}
|
|
1556
1784
|
});
|
|
1557
1785
|
|
|
@@ -1566,7 +1794,7 @@ init_color();
|
|
|
1566
1794
|
var package_default = {
|
|
1567
1795
|
name: "qunitx-cli",
|
|
1568
1796
|
type: "module",
|
|
1569
|
-
version: "0.
|
|
1797
|
+
version: "0.10.0",
|
|
1570
1798
|
description: "Browser runner for QUnitx: run your qunitx tests in google-chrome",
|
|
1571
1799
|
author: "Izel Nakri",
|
|
1572
1800
|
license: "MIT",
|
|
@@ -1595,7 +1823,7 @@ var package_default = {
|
|
|
1595
1823
|
"changelog:unreleased": "git-cliff --unreleased --strip all",
|
|
1596
1824
|
"changelog:preview": "git-cliff",
|
|
1597
1825
|
"changelog:update": "git-cliff --output CHANGELOG.md",
|
|
1598
|
-
postinstall: "deno install --allow-scripts=npm:playwright-core || true",
|
|
1826
|
+
postinstall: "PLAYWRIGHT_SKIP_DOWNLOAD=true deno install --allow-scripts=npm:playwright-core || true",
|
|
1599
1827
|
test: `node --experimental-strip-types test/setup.ts && FORCE_COLOR=0 node --experimental-strip-types --test --test-concurrency=$(node -p 'require("os").availableParallelism()') test/**/*-test.ts`,
|
|
1600
1828
|
"test:browser": `node --experimental-strip-types test/setup.ts && FORCE_COLOR=0 node --experimental-strip-types --test --test-concurrency=$(node -p 'require("os").availableParallelism()') test/flags/*-test.ts test/inputs/*-test.ts`,
|
|
1601
1829
|
"test:sanity-first": "./cli.ts test/helpers/failing-tests.js test/helpers/failing-tests.ts",
|
|
@@ -1622,7 +1850,7 @@ var package_default = {
|
|
|
1622
1850
|
express: "^5.2.1",
|
|
1623
1851
|
"js-yaml": "^4.1.1",
|
|
1624
1852
|
prettier: "^3.8.1",
|
|
1625
|
-
qunitx: "^1.1
|
|
1853
|
+
qunitx: "^1.2.1",
|
|
1626
1854
|
typescript: "^6.0.2"
|
|
1627
1855
|
},
|
|
1628
1856
|
volta: {
|
|
@@ -1654,6 +1882,7 @@ ${highlight("Input options:")}
|
|
|
1654
1882
|
${highlight("Optional flags:")}
|
|
1655
1883
|
${color("--debug")} : print console output when tests run in browser
|
|
1656
1884
|
${color("--watch")} : run the target file or folders, watch them for continuous run and expose http server under localhost
|
|
1885
|
+
${color("--open")} : run tests in a visible browser window instead of headless; keeps the server alive (short: ${color("-o")})
|
|
1657
1886
|
${color("--timeout")} : change default timeout per test case
|
|
1658
1887
|
${color("--output")} : folder to distribute built qunitx html and js that a webservers can run[default: tmp]
|
|
1659
1888
|
${color("--failFast")} : run the target file or folders with immediate abort if a single test fails
|
|
@@ -1902,6 +2131,10 @@ function parseCliFlags(projectRoot) {
|
|
|
1902
2131
|
return Object.assign(result, { debug: parseBoolean(arg.split("=")[1]) });
|
|
1903
2132
|
} else if (arg.startsWith("--watch")) {
|
|
1904
2133
|
return Object.assign(result, { watch: parseBoolean(arg.split("=")[1]) });
|
|
2134
|
+
} else if (arg === "-o" || arg.startsWith("-o=") || arg.startsWith("--open")) {
|
|
2135
|
+
const value = arg.split("=")[1];
|
|
2136
|
+
const open = value === void 0 || value === "true" ? true : value === "false" ? false : value;
|
|
2137
|
+
return Object.assign(result, { open });
|
|
1905
2138
|
} else if (arg.startsWith("--failfast") || arg.startsWith("--failFast")) {
|
|
1906
2139
|
return Object.assign(result, { failFast: parseBoolean(arg.split("=")[1]) });
|
|
1907
2140
|
} else if (arg.startsWith("--timeout")) {
|
|
@@ -1916,7 +2149,7 @@ function parseCliFlags(projectRoot) {
|
|
|
1916
2149
|
}
|
|
1917
2150
|
return result;
|
|
1918
2151
|
} else if (arg.startsWith("--port")) {
|
|
1919
|
-
return Object.assign(result, { port: Number(arg.split("=")[1]) });
|
|
2152
|
+
return Object.assign(result, { port: Number(arg.split("=")[1]), portExplicit: true });
|
|
1920
2153
|
} else if (arg.startsWith("--extensions")) {
|
|
1921
2154
|
return Object.assign(result, {
|
|
1922
2155
|
extensions: arg.split("=")[1].split(",").map((e) => e.trim())
|
|
@@ -1937,6 +2170,10 @@ function parseCliFlags(projectRoot) {
|
|
|
1937
2170
|
} else if (arg === "--trace-perf") {
|
|
1938
2171
|
return result;
|
|
1939
2172
|
}
|
|
2173
|
+
if (arg.startsWith("-")) {
|
|
2174
|
+
console.warn(`# Warning: Unknown flag "${arg}" \u2014 ignored`);
|
|
2175
|
+
return result;
|
|
2176
|
+
}
|
|
1940
2177
|
result.inputs.add(arg.startsWith(projectRoot) ? arg : `${process.cwd()}/${arg}`);
|
|
1941
2178
|
return result;
|
|
1942
2179
|
},
|
|
@@ -2000,6 +2237,8 @@ process4.title = "qunitx";
|
|
|
2000
2237
|
(async () => {
|
|
2001
2238
|
if (!process4.argv[2]) {
|
|
2002
2239
|
return await displayHelpOutput();
|
|
2240
|
+
} else if (["--version", "-v", "version"].includes(process4.argv[2])) {
|
|
2241
|
+
return process4.stdout.write(package_default.version + "\n");
|
|
2003
2242
|
} else if (["help", "h", "p", "print"].includes(process4.argv[2])) {
|
|
2004
2243
|
return await displayHelpOutput();
|
|
2005
2244
|
} else if (["new", "n", "g", "generate"].includes(process4.argv[2])) {
|
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.10.0",
|
|
5
5
|
"description": "Browser runner for QUnitx: run your qunitx tests in google-chrome",
|
|
6
6
|
"author": "Izel Nakri",
|
|
7
7
|
"license": "MIT",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"changelog:unreleased": "git-cliff --unreleased --strip all",
|
|
31
31
|
"changelog:preview": "git-cliff",
|
|
32
32
|
"changelog:update": "git-cliff --output CHANGELOG.md",
|
|
33
|
-
"postinstall": "deno install --allow-scripts=npm:playwright-core || true",
|
|
33
|
+
"postinstall": "PLAYWRIGHT_SKIP_DOWNLOAD=true deno install --allow-scripts=npm:playwright-core || true",
|
|
34
34
|
"test": "node --experimental-strip-types test/setup.ts && FORCE_COLOR=0 node --experimental-strip-types --test --test-concurrency=$(node -p 'require(\"os\").availableParallelism()') test/**/*-test.ts",
|
|
35
35
|
"test:browser": "node --experimental-strip-types test/setup.ts && FORCE_COLOR=0 node --experimental-strip-types --test --test-concurrency=$(node -p 'require(\"os\").availableParallelism()') test/flags/*-test.ts test/inputs/*-test.ts",
|
|
36
36
|
"test:sanity-first": "./cli.ts test/helpers/failing-tests.js test/helpers/failing-tests.ts",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"express": "^5.2.1",
|
|
58
58
|
"js-yaml": "^4.1.1",
|
|
59
59
|
"prettier": "^3.8.1",
|
|
60
|
-
"qunitx": "^1.1
|
|
60
|
+
"qunitx": "^1.2.1",
|
|
61
61
|
"typescript": "^6.0.2"
|
|
62
62
|
},
|
|
63
63
|
"volta": {
|