qunitx-cli 0.15.0 → 0.17.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/dist/cli.js +395 -212
- package/package.json +5 -5
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
var __defProp = Object.defineProperty;
|
|
3
3
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
4
|
var __esm = (fn, res) => function __init() {
|
|
@@ -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,
|
|
@@ -689,7 +788,9 @@ function setupWebServer(config, cachedContent) {
|
|
|
689
788
|
if (!config._groupMode) console.log("TAP version 13");
|
|
690
789
|
if (config.debug && config._groupMode) {
|
|
691
790
|
const allFiles = Object.keys(config.fsTree);
|
|
692
|
-
const relFiles = allFiles.map(
|
|
791
|
+
const relFiles = allFiles.map(
|
|
792
|
+
(filePath) => filePath.replace(`${config.projectRoot}/`, "")
|
|
793
|
+
);
|
|
693
794
|
const shown = relFiles.slice(0, 2);
|
|
694
795
|
const rest = relFiles.length - shown.length;
|
|
695
796
|
const fileList = rest > 0 ? `${shown.join(" ")} +${rest} more` : shown.join(" ");
|
|
@@ -805,7 +906,7 @@ function setupWebServer(config, cachedContent) {
|
|
|
805
906
|
const filePath = (url.endsWith("/") ? [STATIC_FILES_PATH, url, "index.html"] : [STATIC_FILES_PATH, url]).join("");
|
|
806
907
|
const statusCode = await pathExists(filePath) ? 200 : 404;
|
|
807
908
|
res.writeHead(statusCode, {
|
|
808
|
-
"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
|
|
809
910
|
});
|
|
810
911
|
if (statusCode === 404) {
|
|
811
912
|
res.end();
|
|
@@ -820,7 +921,7 @@ function replaceAssetPaths(html, htmlPath, projectRoot) {
|
|
|
820
921
|
const assetPaths = findInternalAssetsFromHTML(html);
|
|
821
922
|
const htmlDirectory = htmlPath.split("/").slice(0, -1).join("/");
|
|
822
923
|
return assetPaths.reduce((result, assetPath) => {
|
|
823
|
-
const normalizedFullAbsolutePath =
|
|
924
|
+
const normalizedFullAbsolutePath = path4.normalize(`${htmlDirectory}/${assetPath}`);
|
|
824
925
|
return result.replace(assetPath, normalizedFullAbsolutePath.replace(projectRoot, "."));
|
|
825
926
|
}, html);
|
|
826
927
|
}
|
|
@@ -999,18 +1100,18 @@ async function launchBrowser(config) {
|
|
|
999
1100
|
const browserName = config.browser || "chromium";
|
|
1000
1101
|
if (browserName === "chromium") {
|
|
1001
1102
|
const waitStart = Date.now();
|
|
1002
|
-
const [playwrightCore2,
|
|
1103
|
+
const [playwrightCore2, earlyChrome2] = await Promise.all([
|
|
1003
1104
|
playwrightCorePromise,
|
|
1004
1105
|
earlyBrowserPromise
|
|
1005
1106
|
]);
|
|
1006
1107
|
perfLog(
|
|
1007
1108
|
`browser.js: playwright-core + earlyChrome resolved in ${Date.now() - waitStart}ms, earlyChrome:`,
|
|
1008
|
-
|
|
1109
|
+
earlyChrome2?.cdpEndpoint ?? null
|
|
1009
1110
|
);
|
|
1010
|
-
if (
|
|
1111
|
+
if (earlyChrome2) {
|
|
1011
1112
|
const connectStart = Date.now();
|
|
1012
1113
|
const browser = await playwrightCore2.chromium.connectOverCDP({
|
|
1013
|
-
endpointURL:
|
|
1114
|
+
endpointURL: earlyChrome2.cdpEndpoint
|
|
1014
1115
|
});
|
|
1015
1116
|
perfLog(`browser.js: connectOverCDP took ${Date.now() - connectStart}ms`);
|
|
1016
1117
|
return browser;
|
|
@@ -1180,25 +1281,29 @@ async function buildTestBundle(config, cachedContent) {
|
|
|
1180
1281
|
return;
|
|
1181
1282
|
}
|
|
1182
1283
|
const outfile = `${projectRoot}/${output}/tests.js`;
|
|
1183
|
-
|
|
1284
|
+
const sourcemap = config.debug ? "inline" : config.watch ? "linked" : false;
|
|
1285
|
+
const needsDisk = true;
|
|
1286
|
+
const [allTestCode] = await Promise.all([
|
|
1184
1287
|
buildWithOverlayfsRetry(
|
|
1185
1288
|
{
|
|
1186
1289
|
stdin: {
|
|
1187
|
-
contents: allTestFilePaths.map((
|
|
1290
|
+
contents: allTestFilePaths.map((filePath) => `import "${filePath}";`).join(""),
|
|
1188
1291
|
resolveDir: process.cwd()
|
|
1189
1292
|
},
|
|
1190
1293
|
bundle: true,
|
|
1191
1294
|
logLevel: "error",
|
|
1192
1295
|
outfile,
|
|
1193
1296
|
keepNames: true,
|
|
1194
|
-
|
|
1297
|
+
legalComments: "none",
|
|
1298
|
+
target: esbuildTarget(config.browser),
|
|
1299
|
+
sourcemap,
|
|
1195
1300
|
// Signal the runtime that all test modules are registered. The runtime's maybeStart()
|
|
1196
1301
|
// waits for both this event and the WebSocket 'open' event before calling QUnit.start().
|
|
1197
1302
|
// Dispatching from the bundle (rather than from a script onload attr) is reliable across
|
|
1198
1303
|
// all browsers and does not require changes to user test code.
|
|
1199
1304
|
footer: { js: 'window.dispatchEvent(new CustomEvent("qunitx:tests-ready"));' }
|
|
1200
1305
|
},
|
|
1201
|
-
|
|
1306
|
+
needsDisk
|
|
1202
1307
|
),
|
|
1203
1308
|
Promise.all(
|
|
1204
1309
|
cachedContent.htmlPathsToRunTests.map(async (htmlPath) => {
|
|
@@ -1210,7 +1315,7 @@ async function buildTestBundle(config, cachedContent) {
|
|
|
1210
1315
|
})
|
|
1211
1316
|
)
|
|
1212
1317
|
]);
|
|
1213
|
-
cachedContent.allTestCode =
|
|
1318
|
+
cachedContent.allTestCode = allTestCode;
|
|
1214
1319
|
}
|
|
1215
1320
|
async function runTestsInBrowser(config, cachedContent = {}, connections, targetTestFilesToFilter) {
|
|
1216
1321
|
const { projectRoot, output } = config;
|
|
@@ -1229,8 +1334,11 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
1229
1334
|
}
|
|
1230
1335
|
if (runHasFilter) {
|
|
1231
1336
|
const outputPath = `${projectRoot}/${output}/filtered-tests.js`;
|
|
1232
|
-
await buildFilteredTests(
|
|
1233
|
-
|
|
1337
|
+
cachedContent.filteredTestCode = await buildFilteredTests(
|
|
1338
|
+
targetTestFilesToFilter,
|
|
1339
|
+
outputPath,
|
|
1340
|
+
config
|
|
1341
|
+
);
|
|
1234
1342
|
}
|
|
1235
1343
|
const TIME_COUNTER = timeCounter();
|
|
1236
1344
|
if (runHasFilter) {
|
|
@@ -1253,6 +1361,7 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
1253
1361
|
connections.server && connections.server.close(),
|
|
1254
1362
|
connections.browser && connections.browser.close()
|
|
1255
1363
|
]);
|
|
1364
|
+
await shutdownEarlyBrowser();
|
|
1256
1365
|
return process.exit(config.COUNTER.failCount > 0 ? 1 : 0);
|
|
1257
1366
|
}
|
|
1258
1367
|
}
|
|
@@ -1269,42 +1378,60 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
1269
1378
|
return connections;
|
|
1270
1379
|
}
|
|
1271
1380
|
function buildFilteredTests(filteredTests, outputPath, config) {
|
|
1381
|
+
const sourcemap = config.debug ? "inline" : config.watch ? "linked" : false;
|
|
1382
|
+
const needsDisk = sourcemap === "linked" || Boolean(config.open);
|
|
1272
1383
|
return buildWithOverlayfsRetry(
|
|
1273
1384
|
{
|
|
1274
1385
|
stdin: {
|
|
1275
|
-
contents: filteredTests.map((
|
|
1386
|
+
contents: filteredTests.map((filePath) => `import "${filePath}";`).join(""),
|
|
1276
1387
|
resolveDir: process.cwd()
|
|
1277
1388
|
},
|
|
1278
1389
|
bundle: true,
|
|
1279
1390
|
logLevel: "error",
|
|
1280
1391
|
outfile: outputPath,
|
|
1281
|
-
|
|
1392
|
+
legalComments: "none",
|
|
1393
|
+
target: esbuildTarget(config.browser),
|
|
1394
|
+
sourcemap,
|
|
1282
1395
|
footer: { js: 'window.dispatchEvent(new CustomEvent("qunitx:tests-ready"));' }
|
|
1283
1396
|
},
|
|
1284
|
-
|
|
1397
|
+
needsDisk
|
|
1285
1398
|
);
|
|
1286
1399
|
}
|
|
1287
|
-
async function buildWithOverlayfsRetry(options,
|
|
1400
|
+
async function buildWithOverlayfsRetry(options, needsDisk) {
|
|
1288
1401
|
const RETRY_DELAY_MS = 100;
|
|
1289
1402
|
const MAX_RETRIES = 3;
|
|
1290
1403
|
const EMPTY_BUNDLE_THRESHOLD = 500;
|
|
1291
|
-
|
|
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();
|
|
1292
1411
|
for (let retry = 1; retry <= MAX_RETRIES; retry++) {
|
|
1293
|
-
|
|
1294
|
-
if (bytes2 >= EMPTY_BUNDLE_THRESHOLD) return result;
|
|
1412
|
+
if (js.length >= EMPTY_BUNDLE_THRESHOLD) break;
|
|
1295
1413
|
console.log(
|
|
1296
|
-
`# [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`
|
|
1297
1415
|
);
|
|
1298
1416
|
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
|
|
1299
|
-
result = await
|
|
1417
|
+
({ result, js } = await getContents());
|
|
1300
1418
|
}
|
|
1301
|
-
|
|
1302
|
-
if (bytes < EMPTY_BUNDLE_THRESHOLD) {
|
|
1419
|
+
if (js.length < EMPTY_BUNDLE_THRESHOLD) {
|
|
1303
1420
|
console.log(
|
|
1304
|
-
`# [buildWithOverlayfsRetry] bundle is ${
|
|
1421
|
+
`# [buildWithOverlayfsRetry] bundle is ${js.length} bytes after ${MAX_RETRIES} retries \u2014 proceeding`
|
|
1422
|
+
);
|
|
1423
|
+
}
|
|
1424
|
+
if (needsDisk) {
|
|
1425
|
+
await Promise.all(
|
|
1426
|
+
result.outputFiles.map((outputFile) => fs8.writeFile(outputFile.path, outputFile.contents))
|
|
1305
1427
|
);
|
|
1306
1428
|
}
|
|
1307
|
-
return
|
|
1429
|
+
return js;
|
|
1430
|
+
}
|
|
1431
|
+
function esbuildTarget(browser) {
|
|
1432
|
+
if (browser === "firefox") return ["firefox115"];
|
|
1433
|
+
if (browser === "webkit") return ["safari16"];
|
|
1434
|
+
return ["chrome120"];
|
|
1308
1435
|
}
|
|
1309
1436
|
async function runTestInsideHTMLFile(filePath, { page, server, browser }, config) {
|
|
1310
1437
|
let QUNIT_RESULT;
|
|
@@ -1313,6 +1440,9 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
|
|
|
1313
1440
|
let wsConnected = false;
|
|
1314
1441
|
try {
|
|
1315
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);
|
|
1316
1446
|
let resolveTestRace;
|
|
1317
1447
|
const testRaceResult = new Promise((resolve) => {
|
|
1318
1448
|
resolveTestRace = resolve;
|
|
@@ -1321,11 +1451,11 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
|
|
|
1321
1451
|
config._onWsOpen = () => {
|
|
1322
1452
|
wsConnected = true;
|
|
1323
1453
|
clearTimeout(timeoutHandle);
|
|
1324
|
-
timeoutHandle = setTimeout(resolveTestRace,
|
|
1454
|
+
timeoutHandle = setTimeout(resolveTestRace, startupMs);
|
|
1325
1455
|
};
|
|
1326
1456
|
config._onTestsJsServed = () => {
|
|
1327
1457
|
clearTimeout(timeoutHandle);
|
|
1328
|
-
timeoutHandle = setTimeout(resolveTestRace,
|
|
1458
|
+
timeoutHandle = setTimeout(resolveTestRace, testsJsMs);
|
|
1329
1459
|
};
|
|
1330
1460
|
config._resetTestTimeout = () => {
|
|
1331
1461
|
wsConnected = true;
|
|
@@ -1333,14 +1463,14 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
|
|
|
1333
1463
|
timeoutHandle = setTimeout(resolveTestRace, config.timeout);
|
|
1334
1464
|
};
|
|
1335
1465
|
const targetUrl = `http://localhost:${config.port}${filePath}`;
|
|
1336
|
-
const navOptions = { timeout:
|
|
1466
|
+
const navOptions = { timeout: navMs, waitUntil: "commit" };
|
|
1337
1467
|
if (page.url().split("?")[0] === targetUrl) {
|
|
1338
1468
|
await page.reload(navOptions);
|
|
1339
1469
|
} else {
|
|
1340
1470
|
await page.goto(targetUrl, navOptions);
|
|
1341
1471
|
}
|
|
1342
1472
|
clearTimeout(timeoutHandle);
|
|
1343
|
-
timeoutHandle = setTimeout(resolveTestRace,
|
|
1473
|
+
timeoutHandle = setTimeout(resolveTestRace, startupMs);
|
|
1344
1474
|
await testRaceResult;
|
|
1345
1475
|
QUNIT_RESULT = await page.evaluate(() => window.QUNIT_RESULT);
|
|
1346
1476
|
} catch (error) {
|
|
@@ -1382,6 +1512,7 @@ async function failOnNonWatchMode(watchMode = false, connections = {}, groupMode
|
|
|
1382
1512
|
connections.server && connections.server.close(),
|
|
1383
1513
|
connections.browser && connections.browser.close()
|
|
1384
1514
|
]);
|
|
1515
|
+
await shutdownEarlyBrowser();
|
|
1385
1516
|
process.exit(1);
|
|
1386
1517
|
}
|
|
1387
1518
|
}
|
|
@@ -1389,6 +1520,7 @@ var BundleError;
|
|
|
1389
1520
|
var init_tests_in_browser = __esm({
|
|
1390
1521
|
"lib/commands/run/tests-in-browser.ts"() {
|
|
1391
1522
|
init_color();
|
|
1523
|
+
init_early_chrome();
|
|
1392
1524
|
init_time_counter();
|
|
1393
1525
|
init_run_user_module();
|
|
1394
1526
|
init_display_final_result();
|
|
@@ -1404,70 +1536,81 @@ var init_tests_in_browser = __esm({
|
|
|
1404
1536
|
|
|
1405
1537
|
// lib/setup/file-watcher.ts
|
|
1406
1538
|
import fs9 from "node:fs";
|
|
1407
|
-
import { stat } from "node:fs/promises";
|
|
1408
|
-
import
|
|
1539
|
+
import { stat, lstat } from "node:fs/promises";
|
|
1540
|
+
import path5 from "node:path";
|
|
1409
1541
|
function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFunc) {
|
|
1410
1542
|
const extensions = config.extensions || ["js", "ts"];
|
|
1411
1543
|
const readyPromises = [];
|
|
1412
1544
|
const parentWatchers = [];
|
|
1413
|
-
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) {
|
|
1414
1566
|
let ready = false;
|
|
1415
1567
|
const lastChangeMs = {};
|
|
1416
|
-
const CHANGE_DEDUPE_MS = 30;
|
|
1417
1568
|
const childWatcher = fs9.watch(watchPath, { recursive: true }, async (eventType, filename) => {
|
|
1418
1569
|
if (!ready || !filename) return;
|
|
1419
|
-
const fullPath =
|
|
1570
|
+
const fullPath = path5.join(watchPath, filename);
|
|
1420
1571
|
if (eventType === "change") {
|
|
1421
1572
|
if (!config._building) {
|
|
1422
1573
|
const now = Date.now();
|
|
1423
|
-
|
|
1574
|
+
const last = lastChangeMs[fullPath] ?? 0;
|
|
1575
|
+
if (now - last < CHANGE_DEDUPE_MS) {
|
|
1576
|
+
if (!config._lastBuildEndMs || config._lastBuildEndMs <= last) return;
|
|
1577
|
+
}
|
|
1424
1578
|
lastChangeMs[fullPath] = now;
|
|
1425
1579
|
}
|
|
1426
1580
|
return handleWatchEvent(config, extensions, "change", fullPath, onEventFunc, onFinishFunc);
|
|
1427
1581
|
}
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
config,
|
|
1432
|
-
extensions,
|
|
1433
|
-
s.isDirectory() ? "addDir" : "add",
|
|
1434
|
-
fullPath,
|
|
1435
|
-
onEventFunc,
|
|
1436
|
-
onFinishFunc
|
|
1437
|
-
);
|
|
1438
|
-
} catch {
|
|
1439
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1582
|
+
const event = await classifyRenameEvent(fullPath, config.fsTree);
|
|
1583
|
+
if (!event) return;
|
|
1584
|
+
if (event === "add") {
|
|
1440
1585
|
try {
|
|
1441
|
-
const
|
|
1442
|
-
|
|
1443
|
-
config,
|
|
1444
|
-
extensions,
|
|
1445
|
-
s.isDirectory() ? "addDir" : "add",
|
|
1446
|
-
fullPath,
|
|
1447
|
-
onEventFunc,
|
|
1448
|
-
onFinishFunc
|
|
1449
|
-
);
|
|
1450
|
-
return;
|
|
1586
|
+
const lstatResult = await lstat(fullPath);
|
|
1587
|
+
if (lstatResult.isSymbolicLink()) trackSymlink(fullPath);
|
|
1451
1588
|
} catch {
|
|
1452
1589
|
}
|
|
1453
|
-
|
|
1454
|
-
|
|
1590
|
+
} else if (event === "unlink") {
|
|
1591
|
+
untrackSymlink(fullPath);
|
|
1455
1592
|
}
|
|
1593
|
+
handleWatchEvent(config, extensions, event, fullPath, onEventFunc, onFinishFunc);
|
|
1456
1594
|
});
|
|
1457
|
-
const parentDir =
|
|
1458
|
-
const watchedBasename =
|
|
1595
|
+
const parentDir = path5.dirname(watchPath);
|
|
1596
|
+
const watchedBasename = path5.basename(watchPath);
|
|
1597
|
+
let parentUnlinkFired = false;
|
|
1459
1598
|
const parentWatcher = fs9.watch(parentDir, async (eventType, filename) => {
|
|
1460
1599
|
if (!ready || filename !== watchedBasename || eventType !== "rename") return;
|
|
1600
|
+
if (parentUnlinkFired) return;
|
|
1601
|
+
parentUnlinkFired = true;
|
|
1461
1602
|
try {
|
|
1462
1603
|
await stat(watchPath);
|
|
1604
|
+
parentUnlinkFired = false;
|
|
1463
1605
|
} catch {
|
|
1464
1606
|
handleWatchEvent(config, extensions, "unlinkDir", watchPath, onEventFunc, onFinishFunc);
|
|
1465
1607
|
childWatcher.close();
|
|
1466
1608
|
parentWatcher.close();
|
|
1467
|
-
delete
|
|
1609
|
+
delete fileWatchers[watchPath];
|
|
1468
1610
|
}
|
|
1469
1611
|
});
|
|
1470
1612
|
parentWatchers.push(parentWatcher);
|
|
1613
|
+
fileWatchers[watchPath] = childWatcher;
|
|
1471
1614
|
readyPromises.push(
|
|
1472
1615
|
new Promise(
|
|
1473
1616
|
(resolve) => setImmediate(() => {
|
|
@@ -1476,8 +1619,18 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
1476
1619
|
})
|
|
1477
1620
|
)
|
|
1478
1621
|
);
|
|
1479
|
-
|
|
1480
|
-
|
|
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
|
+
);
|
|
1481
1634
|
return {
|
|
1482
1635
|
fileWatchers,
|
|
1483
1636
|
ready: Promise.all(readyPromises).then(() => {
|
|
@@ -1485,72 +1638,85 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
1485
1638
|
killFileWatchers() {
|
|
1486
1639
|
Object.keys(fileWatchers).forEach((key) => fileWatchers[key].close());
|
|
1487
1640
|
parentWatchers.forEach((pw) => pw.close());
|
|
1641
|
+
symlinkPollers.forEach((cancel) => cancel());
|
|
1642
|
+
symlinkPollers.clear();
|
|
1488
1643
|
return fileWatchers;
|
|
1489
1644
|
}
|
|
1490
1645
|
};
|
|
1491
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
|
+
}
|
|
1492
1661
|
function handleWatchEvent(config, extensions, event, filePath, onEventFunc, onFinishFunc) {
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
if (event === "change" && config._building && config._justAddedFiles?.has(filePath))
|
|
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();
|
|
1496
1666
|
mutateFSTree(config.fsTree, event, filePath);
|
|
1497
1667
|
console.log(
|
|
1498
1668
|
"#",
|
|
1499
1669
|
magenta().bold("==================================================================")
|
|
1500
1670
|
);
|
|
1501
|
-
console.log("#",
|
|
1671
|
+
console.log("#", colorEvent(event), filePath.split(config.projectRoot)[1]);
|
|
1502
1672
|
console.log(
|
|
1503
1673
|
"#",
|
|
1504
1674
|
magenta().bold("==================================================================")
|
|
1505
1675
|
);
|
|
1506
|
-
if (
|
|
1507
|
-
config._building = true;
|
|
1508
|
-
config._justAddedFiles = event === "add" ? /* @__PURE__ */ new Set([filePath]) : /* @__PURE__ */ new Set();
|
|
1509
|
-
const result = onEventFunc(event, filePath);
|
|
1510
|
-
if (!(result instanceof Promise)) {
|
|
1511
|
-
config._building = false;
|
|
1512
|
-
return result;
|
|
1513
|
-
}
|
|
1514
|
-
result.then(() => {
|
|
1515
|
-
onFinishFunc ? onFinishFunc(event, filePath) : null;
|
|
1516
|
-
}).catch((error) => {
|
|
1517
|
-
console.error("#", red("Build error:"), error.message || error);
|
|
1518
|
-
}).finally(() => {
|
|
1519
|
-
config._building = false;
|
|
1520
|
-
if (config._pendingBuildTrigger) {
|
|
1521
|
-
const trigger = config._pendingBuildTrigger;
|
|
1522
|
-
config._pendingBuildTrigger = null;
|
|
1523
|
-
trigger();
|
|
1524
|
-
}
|
|
1525
|
-
});
|
|
1526
|
-
} else {
|
|
1676
|
+
if (config._building) {
|
|
1527
1677
|
if (event === "add") config._justAddedFiles?.add(filePath);
|
|
1528
1678
|
config._pendingBuildTrigger = () => handleWatchEvent(config, extensions, event, filePath, onEventFunc, onFinishFunc);
|
|
1529
|
-
|
|
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
|
+
});
|
|
1530
1697
|
}
|
|
1531
|
-
function mutateFSTree(fsTree, event,
|
|
1698
|
+
function mutateFSTree(fsTree, event, path6) {
|
|
1532
1699
|
if (event === "add") {
|
|
1533
|
-
fsTree[
|
|
1700
|
+
fsTree[path6] = null;
|
|
1534
1701
|
} else if (event === "unlink") {
|
|
1535
|
-
delete fsTree[
|
|
1702
|
+
delete fsTree[path6];
|
|
1536
1703
|
} else if (event === "unlinkDir") {
|
|
1704
|
+
const dirPrefix = path6.endsWith("/") ? path6 : path6 + "/";
|
|
1537
1705
|
for (const treePath of Object.keys(fsTree)) {
|
|
1538
|
-
if (treePath.startsWith(
|
|
1706
|
+
if (treePath.startsWith(dirPrefix)) delete fsTree[treePath];
|
|
1539
1707
|
}
|
|
1540
1708
|
}
|
|
1541
1709
|
}
|
|
1542
|
-
function
|
|
1543
|
-
if (event === "change")
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
return green("ADDED:");
|
|
1547
|
-
} else if (event === "unlink" || event === "unlinkDir") {
|
|
1548
|
-
return red("REMOVED:");
|
|
1549
|
-
}
|
|
1710
|
+
function colorEvent(event) {
|
|
1711
|
+
if (event === "change") return yellow("CHANGED:");
|
|
1712
|
+
if (event === "add" || event === "addDir") return green("ADDED:");
|
|
1713
|
+
return red("REMOVED:");
|
|
1550
1714
|
}
|
|
1715
|
+
var CHANGE_DEDUPE_MS;
|
|
1551
1716
|
var init_file_watcher = __esm({
|
|
1552
1717
|
"lib/setup/file-watcher.ts"() {
|
|
1553
1718
|
init_color();
|
|
1719
|
+
CHANGE_DEDUPE_MS = 30;
|
|
1554
1720
|
}
|
|
1555
1721
|
});
|
|
1556
1722
|
|
|
@@ -1723,7 +1889,7 @@ async function run(config) {
|
|
|
1723
1889
|
config.lastRanTestFiles = allFiles;
|
|
1724
1890
|
const groupConfigs = groups.map((groupFiles, i) => ({
|
|
1725
1891
|
...config,
|
|
1726
|
-
fsTree: Object.fromEntries(groupFiles.map((
|
|
1892
|
+
fsTree: Object.fromEntries(groupFiles.map((filePath) => [filePath, config.fsTree[filePath]])),
|
|
1727
1893
|
// Single group keeps the root output dir for backward-compatible file paths.
|
|
1728
1894
|
output: groupCount === 1 ? config.output : `${config.output}/group-${i}`,
|
|
1729
1895
|
_groupMode: true,
|
|
@@ -1755,9 +1921,9 @@ async function run(config) {
|
|
|
1755
1921
|
const groupResults = await Promise.allSettled(
|
|
1756
1922
|
groupConfigs.map((groupConfig, i) => {
|
|
1757
1923
|
const groupTimeout = new Promise((_, reject) => {
|
|
1758
|
-
const
|
|
1924
|
+
const timeoutId = setTimeout(() => {
|
|
1759
1925
|
const files = Object.keys(groupConfig.fsTree).map(
|
|
1760
|
-
(
|
|
1926
|
+
(filePath) => filePath.replace(`${groupConfig.projectRoot}/`, "")
|
|
1761
1927
|
);
|
|
1762
1928
|
reject(
|
|
1763
1929
|
new Error(
|
|
@@ -1766,7 +1932,7 @@ async function run(config) {
|
|
|
1766
1932
|
)
|
|
1767
1933
|
);
|
|
1768
1934
|
}, GROUP_TIMEOUT_MS);
|
|
1769
|
-
|
|
1935
|
+
timeoutId.unref();
|
|
1770
1936
|
});
|
|
1771
1937
|
return Promise.race([
|
|
1772
1938
|
(async () => {
|
|
@@ -1786,8 +1952,8 @@ async function run(config) {
|
|
|
1786
1952
|
Promise.race([
|
|
1787
1953
|
connections.page.close(),
|
|
1788
1954
|
new Promise((resolve) => {
|
|
1789
|
-
const
|
|
1790
|
-
|
|
1955
|
+
const pageCloseTimeoutId = setTimeout(resolve, 1e4);
|
|
1956
|
+
pageCloseTimeoutId.unref();
|
|
1791
1957
|
})
|
|
1792
1958
|
]).catch(() => {
|
|
1793
1959
|
})
|
|
@@ -1813,11 +1979,12 @@ async function run(config) {
|
|
|
1813
1979
|
}
|
|
1814
1980
|
const exitTimer = setTimeout(() => process.exit(exitCode), 5e3);
|
|
1815
1981
|
exitTimer.unref();
|
|
1816
|
-
process.stdout.write("\n", () => {
|
|
1982
|
+
process.stdout.write("\n", async () => {
|
|
1817
1983
|
clearTimeout(exitTimer);
|
|
1818
1984
|
clearInterval(keepAlive);
|
|
1819
|
-
browser.close().catch(() => {
|
|
1985
|
+
await browser.close().catch(() => {
|
|
1820
1986
|
});
|
|
1987
|
+
await shutdownEarlyBrowser();
|
|
1821
1988
|
process.exit(exitCode);
|
|
1822
1989
|
});
|
|
1823
1990
|
}
|
|
@@ -1880,7 +2047,7 @@ async function addCachedContentMainHTML(projectRoot, cachedContent) {
|
|
|
1880
2047
|
function splitIntoGroups(files, groupCount) {
|
|
1881
2048
|
const groups = Array.from({ length: groupCount }, () => []);
|
|
1882
2049
|
files.forEach((file, i) => groups[i % groupCount].push(file));
|
|
1883
|
-
return groups.filter((
|
|
2050
|
+
return groups.filter((group) => group.length > 0);
|
|
1884
2051
|
}
|
|
1885
2052
|
function logWatcherAndKeyboardShortcutInfo(config, _server) {
|
|
1886
2053
|
const prefix = "Watching files...";
|
|
@@ -1902,6 +2069,7 @@ function normalizeInternalAssetPathFromHTML(projectRoot, assetPath, htmlPath) {
|
|
|
1902
2069
|
var init_run = __esm({
|
|
1903
2070
|
"lib/commands/run.ts"() {
|
|
1904
2071
|
init_browser();
|
|
2072
|
+
init_early_chrome();
|
|
1905
2073
|
init_open_output_in_browser();
|
|
1906
2074
|
init_color();
|
|
1907
2075
|
init_tests_in_browser();
|
|
@@ -1928,7 +2096,7 @@ init_color();
|
|
|
1928
2096
|
var package_default = {
|
|
1929
2097
|
name: "qunitx-cli",
|
|
1930
2098
|
type: "module",
|
|
1931
|
-
version: "0.
|
|
2099
|
+
version: "0.17.0",
|
|
1932
2100
|
description: "Browser runner for QUnitx: run your qunitx tests in google-chrome",
|
|
1933
2101
|
author: "Izel Nakri",
|
|
1934
2102
|
license: "MIT",
|
|
@@ -1958,10 +2126,10 @@ var package_default = {
|
|
|
1958
2126
|
"changelog:preview": "git-cliff",
|
|
1959
2127
|
"changelog:update": "git-cliff --output CHANGELOG.md",
|
|
1960
2128
|
postinstall: "PLAYWRIGHT_SKIP_DOWNLOAD=true deno install --allow-scripts=npm:playwright-core || true",
|
|
1961
|
-
test: "node
|
|
1962
|
-
"test:debug": "
|
|
1963
|
-
dev: "node
|
|
1964
|
-
"test:browser": "node
|
|
2129
|
+
test: "node test/runner.ts",
|
|
2130
|
+
"test:debug": "QUNITX_DEBUG=1 node test/runner.ts",
|
|
2131
|
+
dev: "node test/runner.ts --watch",
|
|
2132
|
+
"test:browser": "node test/runner.ts test/flags/*-test.ts test/inputs/*-test.ts",
|
|
1965
2133
|
"test:release": "bash scripts/test-release.sh",
|
|
1966
2134
|
"test:sanity-first": "./cli.ts test/helpers/failing-tests.js test/helpers/failing-tests.ts",
|
|
1967
2135
|
"test:sanity-second": "./cli.ts test/helpers/passing-tests.js test/helpers/passing-tests.ts"
|
|
@@ -2039,7 +2207,7 @@ ${color("$ qunitx new $testFileName")} # Creates a qunitx test file
|
|
|
2039
2207
|
|
|
2040
2208
|
// lib/commands/init.ts
|
|
2041
2209
|
import fs3 from "node:fs/promises";
|
|
2042
|
-
import
|
|
2210
|
+
import path2 from "node:path";
|
|
2043
2211
|
|
|
2044
2212
|
// lib/utils/find-project-root.ts
|
|
2045
2213
|
import process2 from "node:process";
|
|
@@ -2111,8 +2279,8 @@ async function writeTestsHTML(projectRoot, config, oldPackageJSON) {
|
|
|
2111
2279
|
if (await pathExists(targetPath)) {
|
|
2112
2280
|
return console.log(`${htmlPath} already exists`);
|
|
2113
2281
|
} else {
|
|
2114
|
-
const targetDirectory =
|
|
2115
|
-
const _targetOutputPath =
|
|
2282
|
+
const targetDirectory = path2.dirname(targetPath);
|
|
2283
|
+
const _targetOutputPath = path2.relative(
|
|
2116
2284
|
targetDirectory,
|
|
2117
2285
|
`${projectRoot}/${config.output}/tests.js`
|
|
2118
2286
|
);
|
|
@@ -2148,17 +2316,17 @@ init_read_boilerplate();
|
|
|
2148
2316
|
async function generateTestFiles() {
|
|
2149
2317
|
const projectRoot = await findProjectRoot();
|
|
2150
2318
|
const moduleName = process.argv[3];
|
|
2151
|
-
const
|
|
2152
|
-
if (await pathExists(
|
|
2153
|
-
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!`);
|
|
2154
2322
|
return;
|
|
2155
2323
|
}
|
|
2156
2324
|
const testJSContent = await readBoilerplate("test.js");
|
|
2157
|
-
const targetFolderPaths =
|
|
2325
|
+
const targetFolderPaths = path6.split("/");
|
|
2158
2326
|
targetFolderPaths.pop();
|
|
2159
2327
|
await fs4.mkdir(targetFolderPaths.join("/"), { recursive: true });
|
|
2160
|
-
await fs4.writeFile(
|
|
2161
|
-
console.log(green(`${
|
|
2328
|
+
await fs4.writeFile(path6, testJSContent.replace("{{moduleName}}", moduleName));
|
|
2329
|
+
console.log(green(`${path6} written`));
|
|
2162
2330
|
}
|
|
2163
2331
|
|
|
2164
2332
|
// lib/setup/config.ts
|
|
@@ -2166,13 +2334,28 @@ import fs6 from "node:fs/promises";
|
|
|
2166
2334
|
|
|
2167
2335
|
// lib/setup/fs-tree.ts
|
|
2168
2336
|
import fs5, { glob as fsGlob } from "node:fs/promises";
|
|
2169
|
-
import
|
|
2337
|
+
import path3 from "node:path";
|
|
2170
2338
|
function isGlob(str) {
|
|
2171
2339
|
return /[*?{[]/.test(str);
|
|
2172
2340
|
}
|
|
2173
2341
|
async function readDirRecursive(dir, filter) {
|
|
2174
2342
|
const entries = await fs5.readdir(dir, { recursive: true, withFileTypes: true });
|
|
2175
|
-
|
|
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);
|
|
2176
2359
|
}
|
|
2177
2360
|
async function buildFSTree(fileAbsolutePaths, config = {}) {
|
|
2178
2361
|
const targetExtensions = config.extensions || ["js", "ts"];
|
|
@@ -2244,20 +2427,20 @@ function setupTestFilePaths(_projectRoot, inputs2) {
|
|
|
2244
2427
|
});
|
|
2245
2428
|
return result.map((metaItem) => metaItem.input);
|
|
2246
2429
|
}
|
|
2247
|
-
function pathIsFile(
|
|
2248
|
-
const inputs2 =
|
|
2430
|
+
function pathIsFile(path6) {
|
|
2431
|
+
const inputs2 = path6.split("/");
|
|
2249
2432
|
return inputs2[inputs2.length - 1].includes(".");
|
|
2250
2433
|
}
|
|
2251
2434
|
function pathIsIncludedInPaths(paths, targetPath) {
|
|
2252
|
-
return paths.some((
|
|
2253
|
-
if (
|
|
2435
|
+
return paths.some((path6) => {
|
|
2436
|
+
if (path6 === targetPath) {
|
|
2254
2437
|
return false;
|
|
2255
2438
|
}
|
|
2256
|
-
return matchesGlob(targetPath.input, buildGlobFormat(
|
|
2439
|
+
return matchesGlob(targetPath.input, buildGlobFormat(path6));
|
|
2257
2440
|
});
|
|
2258
2441
|
}
|
|
2259
|
-
function buildGlobFormat(
|
|
2260
|
-
return
|
|
2442
|
+
function buildGlobFormat(path6) {
|
|
2443
|
+
return path6.isFile ? path6.input : `${path6.input}/**`;
|
|
2261
2444
|
}
|
|
2262
2445
|
|
|
2263
2446
|
// lib/utils/parse-cli-flags.ts
|
|
@@ -2289,7 +2472,7 @@ function parseCliFlags(projectRoot) {
|
|
|
2289
2472
|
return Object.assign(result, { port: Number(arg.split("=")[1]), portExplicit: true });
|
|
2290
2473
|
} else if (arg.startsWith("--extensions")) {
|
|
2291
2474
|
return Object.assign(result, {
|
|
2292
|
-
extensions: arg.split("=")[1].split(",").map((
|
|
2475
|
+
extensions: arg.split("=")[1].split(",").map((extension) => extension.trim())
|
|
2293
2476
|
});
|
|
2294
2477
|
} else if (arg.startsWith("--browser")) {
|
|
2295
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.17.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,10 +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:debug": "
|
|
36
|
-
"dev": "node
|
|
37
|
-
"test:browser": "node
|
|
34
|
+
"test": "node test/runner.ts",
|
|
35
|
+
"test:debug": "QUNITX_DEBUG=1 node test/runner.ts",
|
|
36
|
+
"dev": "node test/runner.ts --watch",
|
|
37
|
+
"test:browser": "node test/runner.ts test/flags/*-test.ts test/inputs/*-test.ts",
|
|
38
38
|
"test:release": "bash scripts/test-release.sh",
|
|
39
39
|
"test:sanity-first": "./cli.ts test/helpers/failing-tests.js test/helpers/failing-tests.ts",
|
|
40
40
|
"test:sanity-second": "./cli.ts test/helpers/passing-tests.js test/helpers/passing-tests.ts"
|