qunitx-cli 0.11.0 → 0.16.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 +7 -0
- package/dist/cli.js +444 -212
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -188,6 +188,13 @@ All CLI flags can also be set in `package.json` under the `qunitx` key, so you d
|
|
|
188
188
|
|
|
189
189
|
CLI flags always override `package.json` values when both are present.
|
|
190
190
|
|
|
191
|
+
### Environment variables
|
|
192
|
+
|
|
193
|
+
| Variable | Description |
|
|
194
|
+
|------------------|---------------------------------------------------------------------------------------------------------------|
|
|
195
|
+
| `CHROME_BIN` | Path to the Chrome/Chromium executable. Required on systems where Chrome is not on `PATH` (e.g. many CI environments). Set automatically when using `browser-actions/setup-chrome` in GitHub Actions. |
|
|
196
|
+
| `QUNITX_BROWSER` | Browser engine to use (`chromium`, `firefox`, `webkit`). Equivalent to `--browser` on the CLI. Useful in CI matrix jobs. |
|
|
197
|
+
|
|
191
198
|
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
199
|
|
|
193
200
|
You can also pass a custom HTML file on the CLI:
|
package/dist/cli.js
CHANGED
|
@@ -39,26 +39,57 @@ var init_find_chrome = __esm({
|
|
|
39
39
|
|
|
40
40
|
// lib/utils/pre-launch-chrome.ts
|
|
41
41
|
import { spawn } from "node:child_process";
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
43
|
+
import os from "node:os";
|
|
44
|
+
import path from "node:path";
|
|
45
|
+
async function preLaunchChrome(chromePath, args, headless = true) {
|
|
46
|
+
if (!chromePath) return null;
|
|
47
|
+
const userDataDir = await mkdtemp(path.join(os.tmpdir(), "qunitx-chrome-"));
|
|
48
|
+
const cleanup = () => rm(userDataDir, { recursive: true, force: true }).catch(() => {
|
|
49
|
+
});
|
|
44
50
|
const headlessArgs = headless ? ["--headless=new"] : [];
|
|
51
|
+
const proc = spawn(
|
|
52
|
+
chromePath,
|
|
53
|
+
["--remote-debugging-port=0", `--user-data-dir=${userDataDir}`, ...headlessArgs, ...args],
|
|
54
|
+
{ stdio: ["ignore", "ignore", "pipe"] }
|
|
55
|
+
);
|
|
56
|
+
proc.on("close", () => {
|
|
57
|
+
cleanup();
|
|
58
|
+
resolveWith(null);
|
|
59
|
+
});
|
|
60
|
+
proc.on("error", () => resolveWith(null));
|
|
61
|
+
let resolveWith;
|
|
45
62
|
return new Promise((resolve) => {
|
|
46
|
-
|
|
47
|
-
stdio: ["ignore", "ignore", "pipe"]
|
|
48
|
-
});
|
|
63
|
+
resolveWith = resolve;
|
|
49
64
|
let buffer = "";
|
|
50
65
|
proc.stderr.on("data", (chunk) => {
|
|
51
66
|
buffer += chunk.toString();
|
|
52
67
|
const match = buffer.match(CDP_URL_REGEX);
|
|
53
|
-
if (match)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
68
|
+
if (!match) return;
|
|
69
|
+
proc.unref();
|
|
70
|
+
proc.stderr.unref();
|
|
71
|
+
resolve({
|
|
72
|
+
proc,
|
|
73
|
+
cdpEndpoint: match[1],
|
|
74
|
+
shutdown
|
|
75
|
+
});
|
|
58
76
|
});
|
|
59
|
-
proc.on("error", () => resolve(null));
|
|
60
|
-
proc.on("close", () => resolve(null));
|
|
61
77
|
});
|
|
78
|
+
async function shutdown() {
|
|
79
|
+
proc.ref();
|
|
80
|
+
const closed = new Promise((resolve) => {
|
|
81
|
+
if (proc.exitCode !== null) {
|
|
82
|
+
resolve();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
proc.once("close", resolve);
|
|
86
|
+
});
|
|
87
|
+
try {
|
|
88
|
+
if (proc.exitCode === null) proc.kill("SIGKILL");
|
|
89
|
+
} catch {
|
|
90
|
+
}
|
|
91
|
+
await closed.then(() => cleanup());
|
|
92
|
+
}
|
|
62
93
|
}
|
|
63
94
|
var CDP_URL_REGEX;
|
|
64
95
|
var init_pre_launch_chrome = __esm({
|
|
@@ -72,21 +103,73 @@ var chromium_args_default;
|
|
|
72
103
|
var init_chromium_args = __esm({
|
|
73
104
|
"lib/utils/chromium-args.ts"() {
|
|
74
105
|
chromium_args_default = [
|
|
106
|
+
// ── Sandbox / rendering ──────────────────────────────────────────────────────
|
|
75
107
|
"--no-sandbox",
|
|
108
|
+
// required in most CI/container environments
|
|
76
109
|
"--disable-gpu",
|
|
110
|
+
// no GPU in headless; avoids GPU process startup
|
|
111
|
+
// ── Window / UI ──────────────────────────────────────────────────────────────
|
|
77
112
|
"--window-size=1440,900",
|
|
78
|
-
"--
|
|
79
|
-
|
|
113
|
+
"--hide-scrollbars",
|
|
114
|
+
// no scrollbar rendering overhead
|
|
115
|
+
// ── Automation markers ────────────────────────────────────────────────────────
|
|
116
|
+
"--enable-automation",
|
|
117
|
+
// sets navigator.webdriver=true; disables some UX-only overhead
|
|
118
|
+
"--no-default-browser-check",
|
|
119
|
+
// skip the OS-level "set as default" check on startup
|
|
80
120
|
"--no-first-run",
|
|
81
|
-
|
|
82
|
-
|
|
121
|
+
// skip first-run wizard
|
|
122
|
+
// ── Network ───────────────────────────────────────────────────────────────────
|
|
83
123
|
"--disable-background-networking",
|
|
124
|
+
"--disable-sync",
|
|
125
|
+
"--disable-translate",
|
|
126
|
+
// ── Extensions / apps ─────────────────────────────────────────────────────────
|
|
127
|
+
"--disable-extensions",
|
|
128
|
+
"--disable-default-apps",
|
|
129
|
+
"--disable-component-update",
|
|
130
|
+
// no background update checks
|
|
131
|
+
"--disable-field-trial-config",
|
|
132
|
+
// no A/B experiment config fetches at startup
|
|
133
|
+
// ── Crash / diagnostics ───────────────────────────────────────────────────────
|
|
134
|
+
"--disable-breakpad",
|
|
135
|
+
// no crash reporter process spawned
|
|
136
|
+
"--disable-client-side-phishing-detection",
|
|
137
|
+
// no ML model loaded on startup
|
|
138
|
+
"--metrics-recording-only",
|
|
139
|
+
"--disable-hang-monitor",
|
|
140
|
+
// ── Timers / scheduling ───────────────────────────────────────────────────────
|
|
84
141
|
"--disable-background-timer-throttling",
|
|
85
142
|
"--disable-renderer-backgrounding",
|
|
143
|
+
// ── Memory ───────────────────────────────────────────────────────────────────
|
|
86
144
|
"--disable-dev-shm-usage",
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
"--disable-
|
|
145
|
+
// write to /tmp instead; avoids shm exhaustion with many Chromes
|
|
146
|
+
// ── Navigation ───────────────────────────────────────────────────────────────
|
|
147
|
+
"--disable-back-forward-cache",
|
|
148
|
+
// no BFCache state setup; qunitx never navigates back
|
|
149
|
+
// ── Audio ─────────────────────────────────────────────────────────────────────
|
|
150
|
+
"--mute-audio",
|
|
151
|
+
// ── Keychain / credentials ────────────────────────────────────────────────────
|
|
152
|
+
"--password-store=basic",
|
|
153
|
+
// avoids dbus/kwallet stalls on Linux
|
|
154
|
+
"--use-mock-keychain",
|
|
155
|
+
// avoids system keychain calls on macOS
|
|
156
|
+
// ── Feature flags ────────────────────────────────────────────────────────────
|
|
157
|
+
//
|
|
158
|
+
// Only features that are invisible to user test code are disabled here.
|
|
159
|
+
//
|
|
160
|
+
// PaintHolding — Chrome delays first paint by up to 500ms to prevent flash-of-
|
|
161
|
+
// unstyled-content. Pure dead time in headless; disabling it makes
|
|
162
|
+
// every page load return faster.
|
|
163
|
+
// HttpsUpgrades — Prevents Chrome from silently upgrading HTTP→HTTPS. Critical:
|
|
164
|
+
// qunitx's local test server runs on HTTP; an upgrade attempt would
|
|
165
|
+
// cause the connection to fail.
|
|
166
|
+
// DestroyProfileOnBrowserClose — avoids async profile teardown on exit.
|
|
167
|
+
// DialMediaRouteProvider, GlobalMediaControls, LensOverlay, MediaRouter — UI chrome with no
|
|
168
|
+
// test relevance.
|
|
169
|
+
// OptimizationHints — background network requests for Chrome's optimization service.
|
|
170
|
+
// Translate — translation UI.
|
|
171
|
+
// AvoidUnnecessaryBeforeUnloadCheckSync — reduces beforeunload handler overhead.
|
|
172
|
+
"--disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,OptimizationHints,PaintHolding,Translate"
|
|
90
173
|
];
|
|
91
174
|
}
|
|
92
175
|
});
|
|
@@ -108,7 +191,13 @@ var init_perf_logger = __esm({
|
|
|
108
191
|
});
|
|
109
192
|
|
|
110
193
|
// lib/utils/early-chrome.ts
|
|
111
|
-
|
|
194
|
+
async function shutdownEarlyBrowser() {
|
|
195
|
+
if (!earlyChrome) return;
|
|
196
|
+
const { shutdown } = earlyChrome;
|
|
197
|
+
earlyChrome = null;
|
|
198
|
+
await shutdown();
|
|
199
|
+
}
|
|
200
|
+
var NON_RUN_COMMANDS, isRunCommand, browserFromArgv, openFromArgv, watchFromArgv, openWatchMode, earlyChrome, earlyBrowserPromise;
|
|
112
201
|
var init_early_chrome = __esm({
|
|
113
202
|
"lib/utils/early-chrome.ts"() {
|
|
114
203
|
init_find_chrome();
|
|
@@ -127,12 +216,12 @@ var init_early_chrome = __esm({
|
|
|
127
216
|
{ browserFromArgv: "chromium", openFromArgv: false, watchFromArgv: false }
|
|
128
217
|
));
|
|
129
218
|
openWatchMode = openFromArgv && watchFromArgv;
|
|
130
|
-
|
|
219
|
+
earlyChrome = null;
|
|
131
220
|
if (!openWatchMode) {
|
|
132
221
|
process.on("exit", () => {
|
|
133
|
-
if (!
|
|
222
|
+
if (!earlyChrome) return;
|
|
134
223
|
try {
|
|
135
|
-
|
|
224
|
+
earlyChrome.proc.kill("SIGKILL");
|
|
136
225
|
} catch {
|
|
137
226
|
}
|
|
138
227
|
});
|
|
@@ -143,7 +232,7 @@ var init_early_chrome = __esm({
|
|
|
143
232
|
return preLaunchChrome(chromePath, chromium_args_default, !openWatchMode);
|
|
144
233
|
}).then((info) => {
|
|
145
234
|
perfLog("early-chrome.js: Chrome CDP ready", info?.cdpEndpoint ?? null);
|
|
146
|
-
if (info)
|
|
235
|
+
if (info) earlyChrome = info;
|
|
147
236
|
return info;
|
|
148
237
|
}) : Promise.resolve(null);
|
|
149
238
|
}
|
|
@@ -151,45 +240,47 @@ var init_early_chrome = __esm({
|
|
|
151
240
|
|
|
152
241
|
// lib/utils/color.ts
|
|
153
242
|
function createColors(enabled2) {
|
|
154
|
-
const
|
|
155
|
-
const red2 =
|
|
156
|
-
const green2 =
|
|
157
|
-
const yellow2 =
|
|
158
|
-
const blue2 =
|
|
243
|
+
const makeColor = (open, close) => (text) => enabled2 ? `\x1B[${open}m${text}\x1B[${close}m` : String(text);
|
|
244
|
+
const red2 = makeColor(31, 39);
|
|
245
|
+
const green2 = makeColor(32, 39);
|
|
246
|
+
const yellow2 = makeColor(33, 39);
|
|
247
|
+
const blue2 = makeColor(34, 39);
|
|
159
248
|
const magenta2 = ((text) => {
|
|
160
249
|
if (text !== void 0) return enabled2 ? `\x1B[35m${text}\x1B[39m` : String(text);
|
|
161
|
-
return {
|
|
250
|
+
return {
|
|
251
|
+
bold: (boldText) => enabled2 ? `\x1B[35m\x1B[1m${boldText}\x1B[22m\x1B[39m` : String(boldText)
|
|
252
|
+
};
|
|
162
253
|
});
|
|
163
254
|
return { red: red2, green: green2, yellow: yellow2, blue: blue2, magenta: magenta2 };
|
|
164
255
|
}
|
|
165
256
|
function red(text) {
|
|
166
|
-
return
|
|
257
|
+
return colors.red(text);
|
|
167
258
|
}
|
|
168
259
|
function green(text) {
|
|
169
|
-
return
|
|
260
|
+
return colors.green(text);
|
|
170
261
|
}
|
|
171
262
|
function yellow(text) {
|
|
172
|
-
return
|
|
263
|
+
return colors.yellow(text);
|
|
173
264
|
}
|
|
174
265
|
function blue(text) {
|
|
175
|
-
return
|
|
266
|
+
return colors.blue(text);
|
|
176
267
|
}
|
|
177
268
|
function magenta(text) {
|
|
178
|
-
return
|
|
269
|
+
return colors.magenta(text);
|
|
179
270
|
}
|
|
180
|
-
var enabled,
|
|
271
|
+
var enabled, colors;
|
|
181
272
|
var init_color = __esm({
|
|
182
273
|
"lib/utils/color.ts"() {
|
|
183
274
|
enabled = !process.env.NODE_DISABLE_COLORS && process.env.NO_COLOR == null && process.env.TERM !== "dumb" && (process.env.FORCE_COLOR != null && process.env.FORCE_COLOR !== "0" || !!process.stdout?.isTTY);
|
|
184
|
-
|
|
275
|
+
colors = createColors(enabled);
|
|
185
276
|
}
|
|
186
277
|
});
|
|
187
278
|
|
|
188
279
|
// lib/utils/path-exists.ts
|
|
189
280
|
import fs from "node:fs/promises";
|
|
190
|
-
async function pathExists(
|
|
281
|
+
async function pathExists(path6) {
|
|
191
282
|
try {
|
|
192
|
-
await fs.access(
|
|
283
|
+
await fs.access(path6);
|
|
193
284
|
return true;
|
|
194
285
|
} catch {
|
|
195
286
|
return false;
|
|
@@ -225,8 +316,8 @@ var init_read_boilerplate = __esm({
|
|
|
225
316
|
|
|
226
317
|
// lib/utils/find-internal-assets-from-html.ts
|
|
227
318
|
function findInternalAssetsFromHTML(htmlContent) {
|
|
228
|
-
const links = [...htmlContent.matchAll(LINK_HREF_REGEX)].map((
|
|
229
|
-
const scripts = [...htmlContent.matchAll(SCRIPT_SRC_REGEX)].map((
|
|
319
|
+
const links = [...htmlContent.matchAll(LINK_HREF_REGEX)].map((match) => match[1]).filter((uri) => !ABSOLUTE_URL_REGEX.test(uri));
|
|
320
|
+
const scripts = [...htmlContent.matchAll(SCRIPT_SRC_REGEX)].map((match) => match[1]).filter((uri) => !ABSOLUTE_URL_REGEX.test(uri));
|
|
230
321
|
return links.concat(scripts);
|
|
231
322
|
}
|
|
232
323
|
var ABSOLUTE_URL_REGEX, SCRIPT_SRC_REGEX, LINK_HREF_REGEX;
|
|
@@ -288,17 +379,17 @@ function dumpValue(value, indent) {
|
|
|
288
379
|
if (Array.isArray(value)) {
|
|
289
380
|
if (value.length === 0) return "[]";
|
|
290
381
|
const next2 = `${indent} `;
|
|
291
|
-
return "\n" + value.map((
|
|
382
|
+
return "\n" + value.map((item) => `${next2}- ${dumpValue(item, next2)}`).join("\n");
|
|
292
383
|
}
|
|
293
384
|
const entries = Object.entries(value);
|
|
294
385
|
if (entries.length === 0) return "{}";
|
|
295
386
|
const next = `${indent} `;
|
|
296
|
-
return "\n" + entries.map(([
|
|
387
|
+
return "\n" + entries.map(([entryKey, entryValue]) => `${next}${entryKey}: ${dumpValue(entryValue, next)}`).join("\n");
|
|
297
388
|
}
|
|
298
389
|
function yamlLine(key, value) {
|
|
299
|
-
const
|
|
300
|
-
return
|
|
301
|
-
` : `${key}: ${
|
|
390
|
+
const serialized = dumpValue(value, "");
|
|
391
|
+
return serialized[0] === "\n" ? `${key}:${serialized}
|
|
392
|
+
` : `${key}: ${serialized}
|
|
302
393
|
`;
|
|
303
394
|
}
|
|
304
395
|
function dumpYaml({
|
|
@@ -404,23 +495,31 @@ var init_display_test_result = __esm({
|
|
|
404
495
|
// lib/setup/bind-server-to-port.ts
|
|
405
496
|
async function bindServerToPort(server, config) {
|
|
406
497
|
let port = config.port;
|
|
498
|
+
let attempt = 0;
|
|
407
499
|
while (true) {
|
|
408
500
|
try {
|
|
409
501
|
await server.listen(port);
|
|
410
502
|
break;
|
|
411
503
|
} catch (err) {
|
|
412
|
-
|
|
504
|
+
const isEADDRINUSE = err.code === "EADDRINUSE";
|
|
505
|
+
if (!isEADDRINUSE) throw err;
|
|
506
|
+
if (config.portExplicit) {
|
|
507
|
+
if (attempt >= EXPLICIT_PORT_RETRIES) throw err;
|
|
508
|
+
attempt++;
|
|
509
|
+
await new Promise((resolve) => setTimeout(resolve, EXPLICIT_PORT_RETRY_DELAY_MS));
|
|
510
|
+
} else {
|
|
413
511
|
port++;
|
|
414
|
-
continue;
|
|
415
512
|
}
|
|
416
|
-
throw err;
|
|
417
513
|
}
|
|
418
514
|
}
|
|
419
515
|
config.port = server._server.address().port;
|
|
420
516
|
return server;
|
|
421
517
|
}
|
|
518
|
+
var EXPLICIT_PORT_RETRIES, EXPLICIT_PORT_RETRY_DELAY_MS;
|
|
422
519
|
var init_bind_server_to_port = __esm({
|
|
423
520
|
"lib/setup/bind-server-to-port.ts"() {
|
|
521
|
+
EXPLICIT_PORT_RETRIES = 5;
|
|
522
|
+
EXPLICIT_PORT_RETRY_DELAY_MS = 20;
|
|
424
523
|
}
|
|
425
524
|
});
|
|
426
525
|
|
|
@@ -516,8 +615,8 @@ var init_http = __esm({
|
|
|
516
615
|
return new Promise((resolve) => this._server.close(resolve));
|
|
517
616
|
}
|
|
518
617
|
/** Registers a GET route handler. */
|
|
519
|
-
get(
|
|
520
|
-
this.#registerRouteHandler("GET",
|
|
618
|
+
get(path6, handler) {
|
|
619
|
+
this.#registerRouteHandler("GET", path6, handler);
|
|
521
620
|
}
|
|
522
621
|
/**
|
|
523
622
|
* Starts listening on the given port (0 = OS-assigned).
|
|
@@ -548,30 +647,30 @@ var init_http = __esm({
|
|
|
548
647
|
});
|
|
549
648
|
}
|
|
550
649
|
/** Registers a POST route handler. */
|
|
551
|
-
post(
|
|
552
|
-
this.#registerRouteHandler("POST",
|
|
650
|
+
post(path6, handler) {
|
|
651
|
+
this.#registerRouteHandler("POST", path6, handler);
|
|
553
652
|
}
|
|
554
653
|
/** Registers a DELETE route handler. */
|
|
555
|
-
delete(
|
|
556
|
-
this.#registerRouteHandler("DELETE",
|
|
654
|
+
delete(path6, handler) {
|
|
655
|
+
this.#registerRouteHandler("DELETE", path6, handler);
|
|
557
656
|
}
|
|
558
657
|
/** Registers a PUT route handler. */
|
|
559
|
-
put(
|
|
560
|
-
this.#registerRouteHandler("PUT",
|
|
658
|
+
put(path6, handler) {
|
|
659
|
+
this.#registerRouteHandler("PUT", path6, handler);
|
|
561
660
|
}
|
|
562
661
|
/** Adds a middleware function to the chain. */
|
|
563
662
|
use(middleware) {
|
|
564
663
|
this.middleware.push(middleware);
|
|
565
664
|
}
|
|
566
|
-
#registerRouteHandler(method,
|
|
665
|
+
#registerRouteHandler(method, path6, handler) {
|
|
567
666
|
if (!this.routes[method]) {
|
|
568
667
|
this.routes[method] = {};
|
|
569
668
|
}
|
|
570
|
-
this.routes[method][
|
|
571
|
-
path:
|
|
669
|
+
this.routes[method][path6] = {
|
|
670
|
+
path: path6,
|
|
572
671
|
handler,
|
|
573
|
-
paramNames: this.#extractParamNames(
|
|
574
|
-
isWildcard:
|
|
672
|
+
paramNames: this.#extractParamNames(path6),
|
|
673
|
+
isWildcard: path6 === "/*"
|
|
575
674
|
};
|
|
576
675
|
}
|
|
577
676
|
#handleRequest(req, res) {
|
|
@@ -609,13 +708,13 @@ var init_http = __esm({
|
|
|
609
708
|
return null;
|
|
610
709
|
}
|
|
611
710
|
return routes[url] || Object.values(routes).find((route) => {
|
|
612
|
-
const { path:
|
|
613
|
-
if (!isWildcard && !
|
|
711
|
+
const { path: path6, isWildcard } = route;
|
|
712
|
+
if (!isWildcard && !path6.includes(":")) {
|
|
614
713
|
return false;
|
|
615
714
|
}
|
|
616
|
-
if (isWildcard || this.#matchPathSegments(
|
|
715
|
+
if (isWildcard || this.#matchPathSegments(path6, url)) {
|
|
617
716
|
if (route.paramNames.length > 0) {
|
|
618
|
-
const regexPattern = this.#buildRegexPattern(
|
|
717
|
+
const regexPattern = this.#buildRegexPattern(path6, route.paramNames);
|
|
619
718
|
const regex = new RegExp(`^${regexPattern}$`);
|
|
620
719
|
const regexMatches = regex.exec(url);
|
|
621
720
|
if (regexMatches) {
|
|
@@ -627,8 +726,8 @@ var init_http = __esm({
|
|
|
627
726
|
return false;
|
|
628
727
|
}) || routes["/*"] || null;
|
|
629
728
|
}
|
|
630
|
-
#matchPathSegments(
|
|
631
|
-
const pathSegments =
|
|
729
|
+
#matchPathSegments(path6, url) {
|
|
730
|
+
const pathSegments = path6.split("/");
|
|
632
731
|
const urlSegments = url.split("/");
|
|
633
732
|
if (pathSegments.length !== urlSegments.length) {
|
|
634
733
|
return false;
|
|
@@ -645,14 +744,14 @@ var init_http = __esm({
|
|
|
645
744
|
}
|
|
646
745
|
return true;
|
|
647
746
|
}
|
|
648
|
-
#buildRegexPattern(
|
|
649
|
-
let regexPattern =
|
|
747
|
+
#buildRegexPattern(path6, _paramNames) {
|
|
748
|
+
let regexPattern = path6.replace(/:[^/]+/g, "([^/]+)");
|
|
650
749
|
regexPattern = regexPattern.replace(/\//g, "\\/");
|
|
651
750
|
return regexPattern;
|
|
652
751
|
}
|
|
653
|
-
#extractParamNames(
|
|
752
|
+
#extractParamNames(path6) {
|
|
654
753
|
const paramRegex = /:(\w+)/g;
|
|
655
|
-
const paramMatches =
|
|
754
|
+
const paramMatches = path6.match(paramRegex);
|
|
656
755
|
return paramMatches ? paramMatches.map((match) => match.slice(1)) : [];
|
|
657
756
|
}
|
|
658
757
|
#extractParams(route, _url) {
|
|
@@ -669,9 +768,9 @@ var init_http = __esm({
|
|
|
669
768
|
|
|
670
769
|
// lib/setup/web-server.ts
|
|
671
770
|
import fs7 from "node:fs";
|
|
672
|
-
import
|
|
771
|
+
import path4 from "node:path";
|
|
673
772
|
function setupWebServer(config, cachedContent) {
|
|
674
|
-
const STATIC_FILES_PATH =
|
|
773
|
+
const STATIC_FILES_PATH = path4.join(config.projectRoot, config.output);
|
|
675
774
|
const server = new HTTPServer();
|
|
676
775
|
const mainHTMLWithReplacedAssets = replaceAssetPaths(
|
|
677
776
|
cachedContent.mainHTML.html,
|
|
@@ -682,17 +781,40 @@ function setupWebServer(config, cachedContent) {
|
|
|
682
781
|
socket.on("message", function message(data) {
|
|
683
782
|
const { event, details, abort } = JSON.parse(data);
|
|
684
783
|
if (event === "wsOpen") {
|
|
784
|
+
config._phase = "loading";
|
|
685
785
|
config._onWsOpen?.();
|
|
686
786
|
} else if (event === "connection") {
|
|
787
|
+
config._phase = "running";
|
|
687
788
|
if (!config._groupMode) console.log("TAP version 13");
|
|
789
|
+
if (config.debug && config._groupMode) {
|
|
790
|
+
const allFiles = Object.keys(config.fsTree);
|
|
791
|
+
const relFiles = allFiles.map(
|
|
792
|
+
(filePath) => filePath.replace(`${config.projectRoot}/`, "")
|
|
793
|
+
);
|
|
794
|
+
const shown = relFiles.slice(0, 2);
|
|
795
|
+
const rest = relFiles.length - shown.length;
|
|
796
|
+
const fileList = rest > 0 ? `${shown.join(" ")} +${rest} more` : shown.join(" ");
|
|
797
|
+
console.log("#", blue(`\u2500\u2500 ${fileList} \u2500\u2500`));
|
|
798
|
+
}
|
|
688
799
|
config._resetTestTimeout?.();
|
|
689
800
|
} else if (event === "testEnd" && !abort) {
|
|
690
801
|
if (details.status === "failed") {
|
|
691
802
|
config.lastFailedTestFiles = config.lastRanTestFiles;
|
|
692
803
|
}
|
|
804
|
+
if (config.debug && details.runtime > config.timeout * 0.8) {
|
|
805
|
+
console.log(
|
|
806
|
+
`# SLOW (${details.runtime.toFixed(0)}ms / ${config.timeout}ms timeout): ${details.fullName.join(" | ")}`
|
|
807
|
+
);
|
|
808
|
+
}
|
|
693
809
|
config._resetTestTimeout?.();
|
|
694
810
|
TAPDisplayTestResult(config.COUNTER, details);
|
|
695
811
|
} else if (event === "done") {
|
|
812
|
+
config._phase = "done";
|
|
813
|
+
if (config.debug && config._groupMode) {
|
|
814
|
+
console.log(
|
|
815
|
+
`# group done: ${details.passed} passed, ${details.failed} failed (${details.runtime}ms)`
|
|
816
|
+
);
|
|
817
|
+
}
|
|
696
818
|
if (typeof config._testRunDone === "function") {
|
|
697
819
|
config._testRunDone();
|
|
698
820
|
config._testRunDone = null;
|
|
@@ -784,7 +906,7 @@ function setupWebServer(config, cachedContent) {
|
|
|
784
906
|
const filePath = (url.endsWith("/") ? [STATIC_FILES_PATH, url, "index.html"] : [STATIC_FILES_PATH, url]).join("");
|
|
785
907
|
const statusCode = await pathExists(filePath) ? 200 : 404;
|
|
786
908
|
res.writeHead(statusCode, {
|
|
787
|
-
"Content-Type": req.headers.accept?.includes("text/html") ? MIME_TYPES.html : MIME_TYPES[
|
|
909
|
+
"Content-Type": req.headers.accept?.includes("text/html") ? MIME_TYPES.html : MIME_TYPES[path4.extname(filePath).substring(1).toLowerCase()] || MIME_TYPES.html
|
|
788
910
|
});
|
|
789
911
|
if (statusCode === 404) {
|
|
790
912
|
res.end();
|
|
@@ -799,7 +921,7 @@ function replaceAssetPaths(html, htmlPath, projectRoot) {
|
|
|
799
921
|
const assetPaths = findInternalAssetsFromHTML(html);
|
|
800
922
|
const htmlDirectory = htmlPath.split("/").slice(0, -1).join("/");
|
|
801
923
|
return assetPaths.reduce((result, assetPath) => {
|
|
802
|
-
const normalizedFullAbsolutePath =
|
|
924
|
+
const normalizedFullAbsolutePath = path4.normalize(`${htmlDirectory}/${assetPath}`);
|
|
803
925
|
return result.replace(assetPath, normalizedFullAbsolutePath.replace(projectRoot, "."));
|
|
804
926
|
}, html);
|
|
805
927
|
}
|
|
@@ -966,6 +1088,7 @@ var init_web_server = __esm({
|
|
|
966
1088
|
init_find_internal_assets_from_html();
|
|
967
1089
|
init_html_content_marker();
|
|
968
1090
|
init_display_test_result();
|
|
1091
|
+
init_color();
|
|
969
1092
|
init_path_exists();
|
|
970
1093
|
init_http();
|
|
971
1094
|
fsPromise = fs7.promises;
|
|
@@ -977,18 +1100,18 @@ async function launchBrowser(config) {
|
|
|
977
1100
|
const browserName = config.browser || "chromium";
|
|
978
1101
|
if (browserName === "chromium") {
|
|
979
1102
|
const waitStart = Date.now();
|
|
980
|
-
const [playwrightCore2,
|
|
1103
|
+
const [playwrightCore2, earlyChrome2] = await Promise.all([
|
|
981
1104
|
playwrightCorePromise,
|
|
982
1105
|
earlyBrowserPromise
|
|
983
1106
|
]);
|
|
984
1107
|
perfLog(
|
|
985
1108
|
`browser.js: playwright-core + earlyChrome resolved in ${Date.now() - waitStart}ms, earlyChrome:`,
|
|
986
|
-
|
|
1109
|
+
earlyChrome2?.cdpEndpoint ?? null
|
|
987
1110
|
);
|
|
988
|
-
if (
|
|
1111
|
+
if (earlyChrome2) {
|
|
989
1112
|
const connectStart = Date.now();
|
|
990
1113
|
const browser = await playwrightCore2.chromium.connectOverCDP({
|
|
991
|
-
endpointURL:
|
|
1114
|
+
endpointURL: earlyChrome2.cdpEndpoint
|
|
992
1115
|
});
|
|
993
1116
|
perfLog(`browser.js: connectOverCDP took ${Date.now() - connectStart}ms`);
|
|
994
1117
|
return browser;
|
|
@@ -1122,7 +1245,7 @@ async function runUserModule(modulePath, params, scriptPosition) {
|
|
|
1122
1245
|
console.log("#", red(`QUnitX ${scriptPosition} script failed:`));
|
|
1123
1246
|
console.trace(error);
|
|
1124
1247
|
console.error(error);
|
|
1125
|
-
|
|
1248
|
+
process.stdout.write("", () => process.exit(1));
|
|
1126
1249
|
}
|
|
1127
1250
|
}
|
|
1128
1251
|
var init_run_user_module = __esm({
|
|
@@ -1158,25 +1281,29 @@ async function buildTestBundle(config, cachedContent) {
|
|
|
1158
1281
|
return;
|
|
1159
1282
|
}
|
|
1160
1283
|
const outfile = `${projectRoot}/${output}/tests.js`;
|
|
1161
|
-
|
|
1284
|
+
const sourcemap = config.debug ? "inline" : config.watch ? "linked" : false;
|
|
1285
|
+
const needsDisk = true;
|
|
1286
|
+
const [allTestCode] = await Promise.all([
|
|
1162
1287
|
buildWithOverlayfsRetry(
|
|
1163
1288
|
{
|
|
1164
1289
|
stdin: {
|
|
1165
|
-
contents: allTestFilePaths.map((
|
|
1290
|
+
contents: allTestFilePaths.map((filePath) => `import "${filePath}";`).join(""),
|
|
1166
1291
|
resolveDir: process.cwd()
|
|
1167
1292
|
},
|
|
1168
1293
|
bundle: true,
|
|
1169
1294
|
logLevel: "error",
|
|
1170
1295
|
outfile,
|
|
1171
1296
|
keepNames: true,
|
|
1172
|
-
|
|
1297
|
+
legalComments: "none",
|
|
1298
|
+
target: esbuildTarget(config.browser),
|
|
1299
|
+
sourcemap,
|
|
1173
1300
|
// Signal the runtime that all test modules are registered. The runtime's maybeStart()
|
|
1174
1301
|
// waits for both this event and the WebSocket 'open' event before calling QUnit.start().
|
|
1175
1302
|
// Dispatching from the bundle (rather than from a script onload attr) is reliable across
|
|
1176
1303
|
// all browsers and does not require changes to user test code.
|
|
1177
1304
|
footer: { js: 'window.dispatchEvent(new CustomEvent("qunitx:tests-ready"));' }
|
|
1178
1305
|
},
|
|
1179
|
-
|
|
1306
|
+
needsDisk
|
|
1180
1307
|
),
|
|
1181
1308
|
Promise.all(
|
|
1182
1309
|
cachedContent.htmlPathsToRunTests.map(async (htmlPath) => {
|
|
@@ -1188,7 +1315,7 @@ async function buildTestBundle(config, cachedContent) {
|
|
|
1188
1315
|
})
|
|
1189
1316
|
)
|
|
1190
1317
|
]);
|
|
1191
|
-
cachedContent.allTestCode =
|
|
1318
|
+
cachedContent.allTestCode = allTestCode;
|
|
1192
1319
|
}
|
|
1193
1320
|
async function runTestsInBrowser(config, cachedContent = {}, connections, targetTestFilesToFilter) {
|
|
1194
1321
|
const { projectRoot, output } = config;
|
|
@@ -1207,8 +1334,11 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
1207
1334
|
}
|
|
1208
1335
|
if (runHasFilter) {
|
|
1209
1336
|
const outputPath = `${projectRoot}/${output}/filtered-tests.js`;
|
|
1210
|
-
await buildFilteredTests(
|
|
1211
|
-
|
|
1337
|
+
cachedContent.filteredTestCode = await buildFilteredTests(
|
|
1338
|
+
targetTestFilesToFilter,
|
|
1339
|
+
outputPath,
|
|
1340
|
+
config
|
|
1341
|
+
);
|
|
1212
1342
|
}
|
|
1213
1343
|
const TIME_COUNTER = timeCounter();
|
|
1214
1344
|
if (runHasFilter) {
|
|
@@ -1231,6 +1361,7 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
1231
1361
|
connections.server && connections.server.close(),
|
|
1232
1362
|
connections.browser && connections.browser.close()
|
|
1233
1363
|
]);
|
|
1364
|
+
await shutdownEarlyBrowser();
|
|
1234
1365
|
return process.exit(config.COUNTER.failCount > 0 ? 1 : 0);
|
|
1235
1366
|
}
|
|
1236
1367
|
}
|
|
@@ -1247,42 +1378,60 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
1247
1378
|
return connections;
|
|
1248
1379
|
}
|
|
1249
1380
|
function buildFilteredTests(filteredTests, outputPath, config) {
|
|
1381
|
+
const sourcemap = config.debug ? "inline" : config.watch ? "linked" : false;
|
|
1382
|
+
const needsDisk = sourcemap === "linked" || Boolean(config.open);
|
|
1250
1383
|
return buildWithOverlayfsRetry(
|
|
1251
1384
|
{
|
|
1252
1385
|
stdin: {
|
|
1253
|
-
contents: filteredTests.map((
|
|
1386
|
+
contents: filteredTests.map((filePath) => `import "${filePath}";`).join(""),
|
|
1254
1387
|
resolveDir: process.cwd()
|
|
1255
1388
|
},
|
|
1256
1389
|
bundle: true,
|
|
1257
1390
|
logLevel: "error",
|
|
1258
1391
|
outfile: outputPath,
|
|
1259
|
-
|
|
1392
|
+
legalComments: "none",
|
|
1393
|
+
target: esbuildTarget(config.browser),
|
|
1394
|
+
sourcemap,
|
|
1260
1395
|
footer: { js: 'window.dispatchEvent(new CustomEvent("qunitx:tests-ready"));' }
|
|
1261
1396
|
},
|
|
1262
|
-
|
|
1397
|
+
needsDisk
|
|
1263
1398
|
);
|
|
1264
1399
|
}
|
|
1265
|
-
async function buildWithOverlayfsRetry(options,
|
|
1400
|
+
async function buildWithOverlayfsRetry(options, needsDisk) {
|
|
1266
1401
|
const RETRY_DELAY_MS = 100;
|
|
1267
1402
|
const MAX_RETRIES = 3;
|
|
1268
1403
|
const EMPTY_BUNDLE_THRESHOLD = 500;
|
|
1269
|
-
|
|
1404
|
+
const buildOpts = { ...options, write: false };
|
|
1405
|
+
const getContents = async () => {
|
|
1406
|
+
const result2 = await esbuild.build(buildOpts);
|
|
1407
|
+
const jsFile = result2.outputFiles.find((outputFile) => !outputFile.path.endsWith(".map"));
|
|
1408
|
+
return { result: result2, js: Buffer.from(jsFile.contents) };
|
|
1409
|
+
};
|
|
1410
|
+
let { result, js } = await getContents();
|
|
1270
1411
|
for (let retry = 1; retry <= MAX_RETRIES; retry++) {
|
|
1271
|
-
|
|
1272
|
-
if (bytes2 >= EMPTY_BUNDLE_THRESHOLD) return result;
|
|
1412
|
+
if (js.length >= EMPTY_BUNDLE_THRESHOLD) break;
|
|
1273
1413
|
console.log(
|
|
1274
|
-
`# [buildWithOverlayfsRetry] bundle is ${
|
|
1414
|
+
`# [buildWithOverlayfsRetry] bundle is ${js.length} bytes (< ${EMPTY_BUNDLE_THRESHOLD}) on attempt ${retry}/${MAX_RETRIES} \u2014 overlayfs flush race, retrying in ${RETRY_DELAY_MS}ms`
|
|
1275
1415
|
);
|
|
1276
1416
|
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
|
|
1277
|
-
result = await
|
|
1417
|
+
({ result, js } = await getContents());
|
|
1278
1418
|
}
|
|
1279
|
-
|
|
1280
|
-
if (bytes < EMPTY_BUNDLE_THRESHOLD) {
|
|
1419
|
+
if (js.length < EMPTY_BUNDLE_THRESHOLD) {
|
|
1281
1420
|
console.log(
|
|
1282
|
-
`# [buildWithOverlayfsRetry] bundle is ${
|
|
1421
|
+
`# [buildWithOverlayfsRetry] bundle is ${js.length} bytes after ${MAX_RETRIES} retries \u2014 proceeding`
|
|
1283
1422
|
);
|
|
1284
1423
|
}
|
|
1285
|
-
|
|
1424
|
+
if (needsDisk) {
|
|
1425
|
+
await Promise.all(
|
|
1426
|
+
result.outputFiles.map((outputFile) => fs8.writeFile(outputFile.path, outputFile.contents))
|
|
1427
|
+
);
|
|
1428
|
+
}
|
|
1429
|
+
return js;
|
|
1430
|
+
}
|
|
1431
|
+
function esbuildTarget(browser) {
|
|
1432
|
+
if (browser === "firefox") return ["firefox115"];
|
|
1433
|
+
if (browser === "webkit") return ["safari16"];
|
|
1434
|
+
return ["chrome120"];
|
|
1286
1435
|
}
|
|
1287
1436
|
async function runTestInsideHTMLFile(filePath, { page, server, browser }, config) {
|
|
1288
1437
|
let QUNIT_RESULT;
|
|
@@ -1291,6 +1440,9 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
|
|
|
1291
1440
|
let wsConnected = false;
|
|
1292
1441
|
try {
|
|
1293
1442
|
console.log("#", blue(`QUnitX running: http://localhost:${config.port}${filePath}`));
|
|
1443
|
+
const navMs = config.timeout + 1e4;
|
|
1444
|
+
const startupMs = Math.max(config.timeout * 3, navMs);
|
|
1445
|
+
const testsJsMs = Math.max(config.timeout * 4, navMs);
|
|
1294
1446
|
let resolveTestRace;
|
|
1295
1447
|
const testRaceResult = new Promise((resolve) => {
|
|
1296
1448
|
resolveTestRace = resolve;
|
|
@@ -1299,11 +1451,11 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
|
|
|
1299
1451
|
config._onWsOpen = () => {
|
|
1300
1452
|
wsConnected = true;
|
|
1301
1453
|
clearTimeout(timeoutHandle);
|
|
1302
|
-
timeoutHandle = setTimeout(resolveTestRace,
|
|
1454
|
+
timeoutHandle = setTimeout(resolveTestRace, startupMs);
|
|
1303
1455
|
};
|
|
1304
1456
|
config._onTestsJsServed = () => {
|
|
1305
1457
|
clearTimeout(timeoutHandle);
|
|
1306
|
-
timeoutHandle = setTimeout(resolveTestRace,
|
|
1458
|
+
timeoutHandle = setTimeout(resolveTestRace, testsJsMs);
|
|
1307
1459
|
};
|
|
1308
1460
|
config._resetTestTimeout = () => {
|
|
1309
1461
|
wsConnected = true;
|
|
@@ -1311,14 +1463,14 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
|
|
|
1311
1463
|
timeoutHandle = setTimeout(resolveTestRace, config.timeout);
|
|
1312
1464
|
};
|
|
1313
1465
|
const targetUrl = `http://localhost:${config.port}${filePath}`;
|
|
1314
|
-
const navOptions = { timeout:
|
|
1466
|
+
const navOptions = { timeout: navMs, waitUntil: "commit" };
|
|
1315
1467
|
if (page.url().split("?")[0] === targetUrl) {
|
|
1316
1468
|
await page.reload(navOptions);
|
|
1317
1469
|
} else {
|
|
1318
1470
|
await page.goto(targetUrl, navOptions);
|
|
1319
1471
|
}
|
|
1320
1472
|
clearTimeout(timeoutHandle);
|
|
1321
|
-
timeoutHandle = setTimeout(resolveTestRace,
|
|
1473
|
+
timeoutHandle = setTimeout(resolveTestRace, startupMs);
|
|
1322
1474
|
await testRaceResult;
|
|
1323
1475
|
QUNIT_RESULT = await page.evaluate(() => window.QUNIT_RESULT);
|
|
1324
1476
|
} catch (error) {
|
|
@@ -1360,6 +1512,7 @@ async function failOnNonWatchMode(watchMode = false, connections = {}, groupMode
|
|
|
1360
1512
|
connections.server && connections.server.close(),
|
|
1361
1513
|
connections.browser && connections.browser.close()
|
|
1362
1514
|
]);
|
|
1515
|
+
await shutdownEarlyBrowser();
|
|
1363
1516
|
process.exit(1);
|
|
1364
1517
|
}
|
|
1365
1518
|
}
|
|
@@ -1367,6 +1520,7 @@ var BundleError;
|
|
|
1367
1520
|
var init_tests_in_browser = __esm({
|
|
1368
1521
|
"lib/commands/run/tests-in-browser.ts"() {
|
|
1369
1522
|
init_color();
|
|
1523
|
+
init_early_chrome();
|
|
1370
1524
|
init_time_counter();
|
|
1371
1525
|
init_run_user_module();
|
|
1372
1526
|
init_display_final_result();
|
|
@@ -1382,70 +1536,81 @@ var init_tests_in_browser = __esm({
|
|
|
1382
1536
|
|
|
1383
1537
|
// lib/setup/file-watcher.ts
|
|
1384
1538
|
import fs9 from "node:fs";
|
|
1385
|
-
import { stat } from "node:fs/promises";
|
|
1386
|
-
import
|
|
1539
|
+
import { stat, lstat } from "node:fs/promises";
|
|
1540
|
+
import path5 from "node:path";
|
|
1387
1541
|
function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFunc) {
|
|
1388
1542
|
const extensions = config.extensions || ["js", "ts"];
|
|
1389
1543
|
const readyPromises = [];
|
|
1390
1544
|
const parentWatchers = [];
|
|
1391
|
-
const fileWatchers =
|
|
1545
|
+
const fileWatchers = {};
|
|
1546
|
+
const symlinkPollers = /* @__PURE__ */ new Map();
|
|
1547
|
+
function trackSymlink(filePath) {
|
|
1548
|
+
if (symlinkPollers.has(filePath)) return;
|
|
1549
|
+
const handler = (curr) => {
|
|
1550
|
+
if (curr.nlink === 0) {
|
|
1551
|
+
fs9.unwatchFile(filePath, handler);
|
|
1552
|
+
symlinkPollers.delete(filePath);
|
|
1553
|
+
if (filePath in config.fsTree) {
|
|
1554
|
+
handleWatchEvent(config, extensions, "unlink", filePath, onEventFunc, onFinishFunc);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
};
|
|
1558
|
+
fs9.watchFile(filePath, { interval: 500, persistent: false }, handler);
|
|
1559
|
+
symlinkPollers.set(filePath, () => fs9.unwatchFile(filePath, handler));
|
|
1560
|
+
}
|
|
1561
|
+
function untrackSymlink(filePath) {
|
|
1562
|
+
symlinkPollers.get(filePath)?.();
|
|
1563
|
+
symlinkPollers.delete(filePath);
|
|
1564
|
+
}
|
|
1565
|
+
for (const watchPath of testFileLookupPaths) {
|
|
1392
1566
|
let ready = false;
|
|
1393
1567
|
const lastChangeMs = {};
|
|
1394
|
-
const CHANGE_DEDUPE_MS = 30;
|
|
1395
1568
|
const childWatcher = fs9.watch(watchPath, { recursive: true }, async (eventType, filename) => {
|
|
1396
1569
|
if (!ready || !filename) return;
|
|
1397
|
-
const fullPath =
|
|
1570
|
+
const fullPath = path5.join(watchPath, filename);
|
|
1398
1571
|
if (eventType === "change") {
|
|
1399
1572
|
if (!config._building) {
|
|
1400
1573
|
const now = Date.now();
|
|
1401
|
-
|
|
1574
|
+
const last = lastChangeMs[fullPath] ?? 0;
|
|
1575
|
+
if (now - last < CHANGE_DEDUPE_MS) {
|
|
1576
|
+
if (!config._lastBuildEndMs || config._lastBuildEndMs <= last) return;
|
|
1577
|
+
}
|
|
1402
1578
|
lastChangeMs[fullPath] = now;
|
|
1403
1579
|
}
|
|
1404
1580
|
return handleWatchEvent(config, extensions, "change", fullPath, onEventFunc, onFinishFunc);
|
|
1405
1581
|
}
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
config,
|
|
1410
|
-
extensions,
|
|
1411
|
-
s.isDirectory() ? "addDir" : "add",
|
|
1412
|
-
fullPath,
|
|
1413
|
-
onEventFunc,
|
|
1414
|
-
onFinishFunc
|
|
1415
|
-
);
|
|
1416
|
-
} catch {
|
|
1417
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1582
|
+
const event = await classifyRenameEvent(fullPath, config.fsTree);
|
|
1583
|
+
if (!event) return;
|
|
1584
|
+
if (event === "add") {
|
|
1418
1585
|
try {
|
|
1419
|
-
const
|
|
1420
|
-
|
|
1421
|
-
config,
|
|
1422
|
-
extensions,
|
|
1423
|
-
s.isDirectory() ? "addDir" : "add",
|
|
1424
|
-
fullPath,
|
|
1425
|
-
onEventFunc,
|
|
1426
|
-
onFinishFunc
|
|
1427
|
-
);
|
|
1428
|
-
return;
|
|
1586
|
+
const lstatResult = await lstat(fullPath);
|
|
1587
|
+
if (lstatResult.isSymbolicLink()) trackSymlink(fullPath);
|
|
1429
1588
|
} catch {
|
|
1430
1589
|
}
|
|
1431
|
-
|
|
1432
|
-
|
|
1590
|
+
} else if (event === "unlink") {
|
|
1591
|
+
untrackSymlink(fullPath);
|
|
1433
1592
|
}
|
|
1593
|
+
handleWatchEvent(config, extensions, event, fullPath, onEventFunc, onFinishFunc);
|
|
1434
1594
|
});
|
|
1435
|
-
const parentDir =
|
|
1436
|
-
const watchedBasename =
|
|
1595
|
+
const parentDir = path5.dirname(watchPath);
|
|
1596
|
+
const watchedBasename = path5.basename(watchPath);
|
|
1597
|
+
let parentUnlinkFired = false;
|
|
1437
1598
|
const parentWatcher = fs9.watch(parentDir, async (eventType, filename) => {
|
|
1438
1599
|
if (!ready || filename !== watchedBasename || eventType !== "rename") return;
|
|
1600
|
+
if (parentUnlinkFired) return;
|
|
1601
|
+
parentUnlinkFired = true;
|
|
1439
1602
|
try {
|
|
1440
1603
|
await stat(watchPath);
|
|
1604
|
+
parentUnlinkFired = false;
|
|
1441
1605
|
} catch {
|
|
1442
1606
|
handleWatchEvent(config, extensions, "unlinkDir", watchPath, onEventFunc, onFinishFunc);
|
|
1443
1607
|
childWatcher.close();
|
|
1444
1608
|
parentWatcher.close();
|
|
1445
|
-
delete
|
|
1609
|
+
delete fileWatchers[watchPath];
|
|
1446
1610
|
}
|
|
1447
1611
|
});
|
|
1448
1612
|
parentWatchers.push(parentWatcher);
|
|
1613
|
+
fileWatchers[watchPath] = childWatcher;
|
|
1449
1614
|
readyPromises.push(
|
|
1450
1615
|
new Promise(
|
|
1451
1616
|
(resolve) => setImmediate(() => {
|
|
@@ -1454,8 +1619,18 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
1454
1619
|
})
|
|
1455
1620
|
)
|
|
1456
1621
|
);
|
|
1457
|
-
|
|
1458
|
-
|
|
1622
|
+
}
|
|
1623
|
+
readyPromises.push(
|
|
1624
|
+
(async () => {
|
|
1625
|
+
for (const filePath of Object.keys(config.fsTree)) {
|
|
1626
|
+
try {
|
|
1627
|
+
const lstatResult = await lstat(filePath);
|
|
1628
|
+
if (lstatResult.isSymbolicLink()) trackSymlink(filePath);
|
|
1629
|
+
} catch {
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
})()
|
|
1633
|
+
);
|
|
1459
1634
|
return {
|
|
1460
1635
|
fileWatchers,
|
|
1461
1636
|
ready: Promise.all(readyPromises).then(() => {
|
|
@@ -1463,69 +1638,85 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
1463
1638
|
killFileWatchers() {
|
|
1464
1639
|
Object.keys(fileWatchers).forEach((key) => fileWatchers[key].close());
|
|
1465
1640
|
parentWatchers.forEach((pw) => pw.close());
|
|
1641
|
+
symlinkPollers.forEach((cancel) => cancel());
|
|
1642
|
+
symlinkPollers.clear();
|
|
1466
1643
|
return fileWatchers;
|
|
1467
1644
|
}
|
|
1468
1645
|
};
|
|
1469
1646
|
}
|
|
1647
|
+
async function classifyRenameEvent(fullPath, fsTree) {
|
|
1648
|
+
for (const delay of [0, 50]) {
|
|
1649
|
+
if (delay) await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1650
|
+
try {
|
|
1651
|
+
const statResult = await stat(fullPath);
|
|
1652
|
+
return statResult.isDirectory() ? "addDir" : "add";
|
|
1653
|
+
} catch {
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
if (!fsTree) return null;
|
|
1657
|
+
if (fullPath in fsTree) return "unlink";
|
|
1658
|
+
const dirPrefix = fullPath + "/";
|
|
1659
|
+
return Object.keys(fsTree).some((trackedPath) => trackedPath.startsWith(dirPrefix)) ? "unlinkDir" : null;
|
|
1660
|
+
}
|
|
1470
1661
|
function handleWatchEvent(config, extensions, event, filePath, onEventFunc, onFinishFunc) {
|
|
1471
|
-
|
|
1472
|
-
|
|
1662
|
+
if (event !== "unlinkDir" && !extensions.some((ext) => filePath.endsWith(`.${ext}`)))
|
|
1663
|
+
return Promise.resolve();
|
|
1664
|
+
if (event === "change" && config._building && config._justAddedFiles?.has(filePath))
|
|
1665
|
+
return Promise.resolve();
|
|
1473
1666
|
mutateFSTree(config.fsTree, event, filePath);
|
|
1474
1667
|
console.log(
|
|
1475
1668
|
"#",
|
|
1476
1669
|
magenta().bold("==================================================================")
|
|
1477
1670
|
);
|
|
1478
|
-
console.log("#",
|
|
1671
|
+
console.log("#", colorEvent(event), filePath.split(config.projectRoot)[1]);
|
|
1479
1672
|
console.log(
|
|
1480
1673
|
"#",
|
|
1481
1674
|
magenta().bold("==================================================================")
|
|
1482
1675
|
);
|
|
1483
|
-
if (
|
|
1484
|
-
config.
|
|
1485
|
-
const result = onEventFunc(event, filePath);
|
|
1486
|
-
if (!(result instanceof Promise)) {
|
|
1487
|
-
config._building = false;
|
|
1488
|
-
return result;
|
|
1489
|
-
}
|
|
1490
|
-
result.then(() => {
|
|
1491
|
-
onFinishFunc ? onFinishFunc(event, filePath) : null;
|
|
1492
|
-
}).catch((error) => {
|
|
1493
|
-
console.error("#", red("Build error:"), error.message || error);
|
|
1494
|
-
}).finally(() => {
|
|
1495
|
-
config._building = false;
|
|
1496
|
-
if (config._pendingBuildTrigger) {
|
|
1497
|
-
const trigger = config._pendingBuildTrigger;
|
|
1498
|
-
config._pendingBuildTrigger = null;
|
|
1499
|
-
trigger();
|
|
1500
|
-
}
|
|
1501
|
-
});
|
|
1502
|
-
} else {
|
|
1676
|
+
if (config._building) {
|
|
1677
|
+
if (event === "add") config._justAddedFiles?.add(filePath);
|
|
1503
1678
|
config._pendingBuildTrigger = () => handleWatchEvent(config, extensions, event, filePath, onEventFunc, onFinishFunc);
|
|
1504
|
-
|
|
1679
|
+
return Promise.resolve();
|
|
1680
|
+
}
|
|
1681
|
+
config._building = true;
|
|
1682
|
+
config._justAddedFiles = event === "add" ? /* @__PURE__ */ new Set([filePath]) : /* @__PURE__ */ new Set();
|
|
1683
|
+
const result = onEventFunc(event, filePath);
|
|
1684
|
+
if (!(result instanceof Promise)) {
|
|
1685
|
+
config._building = false;
|
|
1686
|
+
return Promise.resolve();
|
|
1687
|
+
}
|
|
1688
|
+
return result.then(() => onFinishFunc?.(filePath, event)).catch((error) => console.error("#", red("Build error:"), error.message || error)).finally(() => {
|
|
1689
|
+
config._building = false;
|
|
1690
|
+
config._lastBuildEndMs = Date.now();
|
|
1691
|
+
if (config._pendingBuildTrigger) {
|
|
1692
|
+
const trigger = config._pendingBuildTrigger;
|
|
1693
|
+
config._pendingBuildTrigger = null;
|
|
1694
|
+
trigger();
|
|
1695
|
+
}
|
|
1696
|
+
});
|
|
1505
1697
|
}
|
|
1506
|
-
function mutateFSTree(fsTree, event,
|
|
1698
|
+
function mutateFSTree(fsTree, event, path6) {
|
|
1507
1699
|
if (event === "add") {
|
|
1508
|
-
fsTree[
|
|
1700
|
+
fsTree[path6] = null;
|
|
1509
1701
|
} else if (event === "unlink") {
|
|
1510
|
-
delete fsTree[
|
|
1702
|
+
delete fsTree[path6];
|
|
1511
1703
|
} else if (event === "unlinkDir") {
|
|
1704
|
+
const dirPrefix = path6.endsWith("/") ? path6 : path6 + "/";
|
|
1512
1705
|
for (const treePath of Object.keys(fsTree)) {
|
|
1513
|
-
if (treePath.startsWith(
|
|
1706
|
+
if (treePath.startsWith(dirPrefix)) delete fsTree[treePath];
|
|
1514
1707
|
}
|
|
1515
1708
|
}
|
|
1516
1709
|
}
|
|
1517
|
-
function
|
|
1518
|
-
if (event === "change")
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
return green("ADDED:");
|
|
1522
|
-
} else if (event === "unlink" || event === "unlinkDir") {
|
|
1523
|
-
return red("REMOVED:");
|
|
1524
|
-
}
|
|
1710
|
+
function colorEvent(event) {
|
|
1711
|
+
if (event === "change") return yellow("CHANGED:");
|
|
1712
|
+
if (event === "add" || event === "addDir") return green("ADDED:");
|
|
1713
|
+
return red("REMOVED:");
|
|
1525
1714
|
}
|
|
1715
|
+
var CHANGE_DEDUPE_MS;
|
|
1526
1716
|
var init_file_watcher = __esm({
|
|
1527
1717
|
"lib/setup/file-watcher.ts"() {
|
|
1528
1718
|
init_color();
|
|
1719
|
+
CHANGE_DEDUPE_MS = 30;
|
|
1529
1720
|
}
|
|
1530
1721
|
});
|
|
1531
1722
|
|
|
@@ -1594,7 +1785,7 @@ function setupKeyboardEvents(config, cachedContent, connections) {
|
|
|
1594
1785
|
});
|
|
1595
1786
|
}
|
|
1596
1787
|
function abortBrowserQUnit(_config, connections) {
|
|
1597
|
-
connections.server.publish("abort"
|
|
1788
|
+
connections.server.publish("abort");
|
|
1598
1789
|
}
|
|
1599
1790
|
var init_keyboard_events = __esm({
|
|
1600
1791
|
"lib/setup/keyboard-events.ts"() {
|
|
@@ -1671,11 +1862,21 @@ async function run(config) {
|
|
|
1671
1862
|
if (["change", "unlink", "unlinkDir"].includes(event)) {
|
|
1672
1863
|
if (event === "change" && !(file in config.fsTree)) return;
|
|
1673
1864
|
cachedContent.allTestCode = null;
|
|
1865
|
+
if (config.debug) {
|
|
1866
|
+
console.log(
|
|
1867
|
+
`# Rerun triggered: ${event} \u2192 ${file.replace(`${config.projectRoot}/`, "")}`
|
|
1868
|
+
);
|
|
1869
|
+
}
|
|
1674
1870
|
return await runTestsInBrowser(config, cachedContent, connections);
|
|
1675
1871
|
}
|
|
1872
|
+
if (config.debug) {
|
|
1873
|
+
console.log(
|
|
1874
|
+
`# Rerun triggered: ${event} \u2192 ${file.replace(`${config.projectRoot}/`, "")}`
|
|
1875
|
+
);
|
|
1876
|
+
}
|
|
1676
1877
|
await runTestsInBrowser(config, cachedContent, connections, [file]);
|
|
1677
1878
|
},
|
|
1678
|
-
(_path, _event) => connections.server.publish("refresh"
|
|
1879
|
+
(_path, _event) => connections.server.publish("refresh")
|
|
1679
1880
|
);
|
|
1680
1881
|
await watcherReady;
|
|
1681
1882
|
}
|
|
@@ -1688,13 +1889,17 @@ async function run(config) {
|
|
|
1688
1889
|
config.lastRanTestFiles = allFiles;
|
|
1689
1890
|
const groupConfigs = groups.map((groupFiles, i) => ({
|
|
1690
1891
|
...config,
|
|
1691
|
-
fsTree: Object.fromEntries(groupFiles.map((
|
|
1892
|
+
fsTree: Object.fromEntries(groupFiles.map((filePath) => [filePath, config.fsTree[filePath]])),
|
|
1692
1893
|
// Single group keeps the root output dir for backward-compatible file paths.
|
|
1693
1894
|
output: groupCount === 1 ? config.output : `${config.output}/group-${i}`,
|
|
1694
|
-
_groupMode: true
|
|
1895
|
+
_groupMode: true,
|
|
1896
|
+
_phase: "bundling"
|
|
1695
1897
|
}));
|
|
1696
1898
|
const groupCachedContents = groups.map(() => ({ ...cachedContent }));
|
|
1697
1899
|
console.log("TAP version 13");
|
|
1900
|
+
console.log(
|
|
1901
|
+
`# Running ${allFiles.length} test file${allFiles.length === 1 ? "" : "s"} across ${groupCount} group${groupCount === 1 ? "" : "s"}`
|
|
1902
|
+
);
|
|
1698
1903
|
const [browser] = await Promise.all([
|
|
1699
1904
|
launchBrowser(config),
|
|
1700
1905
|
Promise.all(
|
|
@@ -1716,14 +1921,22 @@ async function run(config) {
|
|
|
1716
1921
|
const groupResults = await Promise.allSettled(
|
|
1717
1922
|
groupConfigs.map((groupConfig, i) => {
|
|
1718
1923
|
const groupTimeout = new Promise((_, reject) => {
|
|
1719
|
-
const
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1924
|
+
const timeoutId = setTimeout(() => {
|
|
1925
|
+
const files = Object.keys(groupConfig.fsTree).map(
|
|
1926
|
+
(filePath) => filePath.replace(`${groupConfig.projectRoot}/`, "")
|
|
1927
|
+
);
|
|
1928
|
+
reject(
|
|
1929
|
+
new Error(
|
|
1930
|
+
`Group ${i} timed out after ${GROUP_TIMEOUT_MS / 1e3}s in phase '${groupConfig._phase ?? "unknown"}'
|
|
1931
|
+
Files: ${files.join(", ")}`
|
|
1932
|
+
)
|
|
1933
|
+
);
|
|
1934
|
+
}, GROUP_TIMEOUT_MS);
|
|
1935
|
+
timeoutId.unref();
|
|
1724
1936
|
});
|
|
1725
1937
|
return Promise.race([
|
|
1726
1938
|
(async () => {
|
|
1939
|
+
groupConfig._phase = "connecting";
|
|
1727
1940
|
const connections = await setupBrowser(groupConfig, groupCachedContents[i], browser);
|
|
1728
1941
|
groupConfig.expressApp = connections.server;
|
|
1729
1942
|
if (config.before) {
|
|
@@ -1739,8 +1952,8 @@ async function run(config) {
|
|
|
1739
1952
|
Promise.race([
|
|
1740
1953
|
connections.page.close(),
|
|
1741
1954
|
new Promise((resolve) => {
|
|
1742
|
-
const
|
|
1743
|
-
|
|
1955
|
+
const pageCloseTimeoutId = setTimeout(resolve, 1e4);
|
|
1956
|
+
pageCloseTimeoutId.unref();
|
|
1744
1957
|
})
|
|
1745
1958
|
]).catch(() => {
|
|
1746
1959
|
})
|
|
@@ -1766,11 +1979,12 @@ async function run(config) {
|
|
|
1766
1979
|
}
|
|
1767
1980
|
const exitTimer = setTimeout(() => process.exit(exitCode), 5e3);
|
|
1768
1981
|
exitTimer.unref();
|
|
1769
|
-
process.stdout.write("\n", () => {
|
|
1982
|
+
process.stdout.write("\n", async () => {
|
|
1770
1983
|
clearTimeout(exitTimer);
|
|
1771
1984
|
clearInterval(keepAlive);
|
|
1772
|
-
browser.close().catch(() => {
|
|
1985
|
+
await browser.close().catch(() => {
|
|
1773
1986
|
});
|
|
1987
|
+
await shutdownEarlyBrowser();
|
|
1774
1988
|
process.exit(exitCode);
|
|
1775
1989
|
});
|
|
1776
1990
|
}
|
|
@@ -1833,7 +2047,7 @@ async function addCachedContentMainHTML(projectRoot, cachedContent) {
|
|
|
1833
2047
|
function splitIntoGroups(files, groupCount) {
|
|
1834
2048
|
const groups = Array.from({ length: groupCount }, () => []);
|
|
1835
2049
|
files.forEach((file, i) => groups[i % groupCount].push(file));
|
|
1836
|
-
return groups.filter((
|
|
2050
|
+
return groups.filter((group) => group.length > 0);
|
|
1837
2051
|
}
|
|
1838
2052
|
function logWatcherAndKeyboardShortcutInfo(config, _server) {
|
|
1839
2053
|
const prefix = "Watching files...";
|
|
@@ -1855,6 +2069,7 @@ function normalizeInternalAssetPathFromHTML(projectRoot, assetPath, htmlPath) {
|
|
|
1855
2069
|
var init_run = __esm({
|
|
1856
2070
|
"lib/commands/run.ts"() {
|
|
1857
2071
|
init_browser();
|
|
2072
|
+
init_early_chrome();
|
|
1858
2073
|
init_open_output_in_browser();
|
|
1859
2074
|
init_color();
|
|
1860
2075
|
init_tests_in_browser();
|
|
@@ -1881,7 +2096,7 @@ init_color();
|
|
|
1881
2096
|
var package_default = {
|
|
1882
2097
|
name: "qunitx-cli",
|
|
1883
2098
|
type: "module",
|
|
1884
|
-
version: "0.
|
|
2099
|
+
version: "0.16.0",
|
|
1885
2100
|
description: "Browser runner for QUnitx: run your qunitx tests in google-chrome",
|
|
1886
2101
|
author: "Izel Nakri",
|
|
1887
2102
|
license: "MIT",
|
|
@@ -1911,8 +2126,10 @@ var package_default = {
|
|
|
1911
2126
|
"changelog:preview": "git-cliff",
|
|
1912
2127
|
"changelog:update": "git-cliff --output CHANGELOG.md",
|
|
1913
2128
|
postinstall: "PLAYWRIGHT_SKIP_DOWNLOAD=true deno install --allow-scripts=npm:playwright-core || true",
|
|
1914
|
-
test:
|
|
1915
|
-
"test:
|
|
2129
|
+
test: "node test/runner.ts",
|
|
2130
|
+
"test:debug": "QUNITX_DEBUG=1 node test/runner.ts",
|
|
2131
|
+
dev: "node test/runner.ts",
|
|
2132
|
+
"test:browser": "node test/runner.ts test/flags/*-test.ts test/inputs/*-test.ts",
|
|
1916
2133
|
"test:release": "bash scripts/test-release.sh",
|
|
1917
2134
|
"test:sanity-first": "./cli.ts test/helpers/failing-tests.js test/helpers/failing-tests.ts",
|
|
1918
2135
|
"test:sanity-second": "./cli.ts test/helpers/passing-tests.js test/helpers/passing-tests.ts"
|
|
@@ -1990,7 +2207,7 @@ ${color("$ qunitx new $testFileName")} # Creates a qunitx test file
|
|
|
1990
2207
|
|
|
1991
2208
|
// lib/commands/init.ts
|
|
1992
2209
|
import fs3 from "node:fs/promises";
|
|
1993
|
-
import
|
|
2210
|
+
import path2 from "node:path";
|
|
1994
2211
|
|
|
1995
2212
|
// lib/utils/find-project-root.ts
|
|
1996
2213
|
import process2 from "node:process";
|
|
@@ -2062,8 +2279,8 @@ async function writeTestsHTML(projectRoot, config, oldPackageJSON) {
|
|
|
2062
2279
|
if (await pathExists(targetPath)) {
|
|
2063
2280
|
return console.log(`${htmlPath} already exists`);
|
|
2064
2281
|
} else {
|
|
2065
|
-
const targetDirectory =
|
|
2066
|
-
const _targetOutputPath =
|
|
2282
|
+
const targetDirectory = path2.dirname(targetPath);
|
|
2283
|
+
const _targetOutputPath = path2.relative(
|
|
2067
2284
|
targetDirectory,
|
|
2068
2285
|
`${projectRoot}/${config.output}/tests.js`
|
|
2069
2286
|
);
|
|
@@ -2099,17 +2316,17 @@ init_read_boilerplate();
|
|
|
2099
2316
|
async function generateTestFiles() {
|
|
2100
2317
|
const projectRoot = await findProjectRoot();
|
|
2101
2318
|
const moduleName = process.argv[3];
|
|
2102
|
-
const
|
|
2103
|
-
if (await pathExists(
|
|
2104
|
-
console.log(`${
|
|
2319
|
+
const path6 = process.argv[3].endsWith(".js") || process.argv[3].endsWith(".ts") ? `${projectRoot}/${process.argv[3]}` : `${projectRoot}/${process.argv[3]}.js`;
|
|
2320
|
+
if (await pathExists(path6)) {
|
|
2321
|
+
console.log(`${path6} already exists!`);
|
|
2105
2322
|
return;
|
|
2106
2323
|
}
|
|
2107
2324
|
const testJSContent = await readBoilerplate("test.js");
|
|
2108
|
-
const targetFolderPaths =
|
|
2325
|
+
const targetFolderPaths = path6.split("/");
|
|
2109
2326
|
targetFolderPaths.pop();
|
|
2110
2327
|
await fs4.mkdir(targetFolderPaths.join("/"), { recursive: true });
|
|
2111
|
-
await fs4.writeFile(
|
|
2112
|
-
console.log(green(`${
|
|
2328
|
+
await fs4.writeFile(path6, testJSContent.replace("{{moduleName}}", moduleName));
|
|
2329
|
+
console.log(green(`${path6} written`));
|
|
2113
2330
|
}
|
|
2114
2331
|
|
|
2115
2332
|
// lib/setup/config.ts
|
|
@@ -2117,13 +2334,28 @@ import fs6 from "node:fs/promises";
|
|
|
2117
2334
|
|
|
2118
2335
|
// lib/setup/fs-tree.ts
|
|
2119
2336
|
import fs5, { glob as fsGlob } from "node:fs/promises";
|
|
2120
|
-
import
|
|
2337
|
+
import path3 from "node:path";
|
|
2121
2338
|
function isGlob(str) {
|
|
2122
2339
|
return /[*?{[]/.test(str);
|
|
2123
2340
|
}
|
|
2124
2341
|
async function readDirRecursive(dir, filter) {
|
|
2125
2342
|
const entries = await fs5.readdir(dir, { recursive: true, withFileTypes: true });
|
|
2126
|
-
|
|
2343
|
+
const candidates = entries.filter(
|
|
2344
|
+
(dirent) => (dirent.isFile() || dirent.isSymbolicLink()) && filter(dirent.name)
|
|
2345
|
+
);
|
|
2346
|
+
const resolvedPaths = await Promise.all(
|
|
2347
|
+
candidates.map(async (dirent) => {
|
|
2348
|
+
const fullPath = path3.join(dirent.parentPath, dirent.name);
|
|
2349
|
+
if (dirent.isFile()) return fullPath;
|
|
2350
|
+
try {
|
|
2351
|
+
const statResult = await fs5.stat(fullPath);
|
|
2352
|
+
return statResult.isFile() ? fullPath : null;
|
|
2353
|
+
} catch {
|
|
2354
|
+
return null;
|
|
2355
|
+
}
|
|
2356
|
+
})
|
|
2357
|
+
);
|
|
2358
|
+
return resolvedPaths.filter((resolvedPath) => resolvedPath !== null);
|
|
2127
2359
|
}
|
|
2128
2360
|
async function buildFSTree(fileAbsolutePaths, config = {}) {
|
|
2129
2361
|
const targetExtensions = config.extensions || ["js", "ts"];
|
|
@@ -2195,20 +2427,20 @@ function setupTestFilePaths(_projectRoot, inputs2) {
|
|
|
2195
2427
|
});
|
|
2196
2428
|
return result.map((metaItem) => metaItem.input);
|
|
2197
2429
|
}
|
|
2198
|
-
function pathIsFile(
|
|
2199
|
-
const inputs2 =
|
|
2430
|
+
function pathIsFile(path6) {
|
|
2431
|
+
const inputs2 = path6.split("/");
|
|
2200
2432
|
return inputs2[inputs2.length - 1].includes(".");
|
|
2201
2433
|
}
|
|
2202
2434
|
function pathIsIncludedInPaths(paths, targetPath) {
|
|
2203
|
-
return paths.some((
|
|
2204
|
-
if (
|
|
2435
|
+
return paths.some((path6) => {
|
|
2436
|
+
if (path6 === targetPath) {
|
|
2205
2437
|
return false;
|
|
2206
2438
|
}
|
|
2207
|
-
return matchesGlob(targetPath.input, buildGlobFormat(
|
|
2439
|
+
return matchesGlob(targetPath.input, buildGlobFormat(path6));
|
|
2208
2440
|
});
|
|
2209
2441
|
}
|
|
2210
|
-
function buildGlobFormat(
|
|
2211
|
-
return
|
|
2442
|
+
function buildGlobFormat(path6) {
|
|
2443
|
+
return path6.isFile ? path6.input : `${path6.input}/**`;
|
|
2212
2444
|
}
|
|
2213
2445
|
|
|
2214
2446
|
// lib/utils/parse-cli-flags.ts
|
|
@@ -2240,7 +2472,7 @@ function parseCliFlags(projectRoot) {
|
|
|
2240
2472
|
return Object.assign(result, { port: Number(arg.split("=")[1]), portExplicit: true });
|
|
2241
2473
|
} else if (arg.startsWith("--extensions")) {
|
|
2242
2474
|
return Object.assign(result, {
|
|
2243
|
-
extensions: arg.split("=")[1].split(",").map((
|
|
2475
|
+
extensions: arg.split("=")[1].split(",").map((extension) => extension.trim())
|
|
2244
2476
|
});
|
|
2245
2477
|
} else if (arg.startsWith("--browser")) {
|
|
2246
2478
|
const value = arg.split("=")[1];
|
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.16.0",
|
|
5
5
|
"description": "Browser runner for QUnitx: run your qunitx tests in google-chrome",
|
|
6
6
|
"author": "Izel Nakri",
|
|
7
7
|
"license": "MIT",
|
|
@@ -31,8 +31,10 @@
|
|
|
31
31
|
"changelog:preview": "git-cliff",
|
|
32
32
|
"changelog:update": "git-cliff --output CHANGELOG.md",
|
|
33
33
|
"postinstall": "PLAYWRIGHT_SKIP_DOWNLOAD=true deno install --allow-scripts=npm:playwright-core || true",
|
|
34
|
-
"test": "node
|
|
35
|
-
"test:
|
|
34
|
+
"test": "node test/runner.ts",
|
|
35
|
+
"test:debug": "QUNITX_DEBUG=1 node test/runner.ts",
|
|
36
|
+
"dev": "node test/runner.ts",
|
|
37
|
+
"test:browser": "node test/runner.ts test/flags/*-test.ts test/inputs/*-test.ts",
|
|
36
38
|
"test:release": "bash scripts/test-release.sh",
|
|
37
39
|
"test:sanity-first": "./cli.ts test/helpers/failing-tests.js test/helpers/failing-tests.ts",
|
|
38
40
|
"test:sanity-second": "./cli.ts test/helpers/passing-tests.js test/helpers/passing-tests.ts"
|