qunitx-cli 0.19.3 → 0.21.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 +111 -0
- package/dist/cli.js +575 -230
- package/package.json +4 -6
package/dist/cli.js
CHANGED
|
@@ -38,10 +38,11 @@ var init_find_chrome = __esm({
|
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
// lib/utils/kill-process-group.ts
|
|
41
|
+
import { spawnSync } from "node:child_process";
|
|
41
42
|
function killProcessGroup(pid) {
|
|
42
43
|
try {
|
|
43
44
|
if (process.platform === "win32") {
|
|
44
|
-
|
|
45
|
+
spawnSync("taskkill", ["/F", "/T", "/PID", String(pid)], { stdio: "ignore" });
|
|
45
46
|
} else {
|
|
46
47
|
process.kill(-pid, "SIGKILL");
|
|
47
48
|
}
|
|
@@ -92,12 +93,17 @@ async function cleanupBrowserDir(dirPath) {
|
|
|
92
93
|
}
|
|
93
94
|
const dirName = dirPath.split("/").pop();
|
|
94
95
|
await killAllReferencingProcesses(dirPath, dirName);
|
|
95
|
-
const deadline = Date.now() +
|
|
96
|
+
const deadline = Date.now() + CLEANUP_DEADLINE_MS;
|
|
96
97
|
while (Date.now() < deadline) {
|
|
97
|
-
const removed = await fs.rm(dirPath, { recursive: true, force: true }).then(
|
|
98
|
+
const removed = await fs.rm(dirPath, { recursive: true, force: true }).then(
|
|
99
|
+
() => fs.access(dirPath).then(
|
|
100
|
+
() => false,
|
|
101
|
+
() => true
|
|
102
|
+
)
|
|
103
|
+
).catch(() => false);
|
|
98
104
|
if (removed) return;
|
|
99
105
|
await killAllReferencingProcesses(dirPath, dirName);
|
|
100
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
106
|
+
await new Promise((resolve) => setTimeout(resolve, CLEANUP_RETRY_MS));
|
|
101
107
|
}
|
|
102
108
|
if (!await fs.access(dirPath).then(() => true).catch(() => false))
|
|
103
109
|
return;
|
|
@@ -117,8 +123,11 @@ async function cleanupBrowserDir(dirPath) {
|
|
|
117
123
|
})
|
|
118
124
|
);
|
|
119
125
|
}
|
|
126
|
+
var CLEANUP_DEADLINE_MS, CLEANUP_RETRY_MS;
|
|
120
127
|
var init_cleanup_browser_dir = __esm({
|
|
121
128
|
"lib/utils/cleanup-browser-dir.ts"() {
|
|
129
|
+
CLEANUP_DEADLINE_MS = 5e3;
|
|
130
|
+
CLEANUP_RETRY_MS = 50;
|
|
122
131
|
}
|
|
123
132
|
});
|
|
124
133
|
|
|
@@ -191,8 +200,11 @@ var init_chromium_args = __esm({
|
|
|
191
200
|
// ── Sandbox / rendering ──────────────────────────────────────────────────────
|
|
192
201
|
"--no-sandbox",
|
|
193
202
|
// required in most CI/container environments
|
|
194
|
-
|
|
195
|
-
//
|
|
203
|
+
// SwiftShader software WebGL: needed on Linux CI (containerised, no GPU).
|
|
204
|
+
// Omit on macOS — Chrome uses Metal natively; SwiftShader crashes the renderer
|
|
205
|
+
// process on macOS arm64, causing "Target page, context or browser has been closed".
|
|
206
|
+
// Omit on Windows — ANGLE/D3D11 is available and SwiftShader is not needed.
|
|
207
|
+
...process.platform === "linux" ? ["--enable-unsafe-swiftshader"] : [],
|
|
196
208
|
// ── Window / UI ──────────────────────────────────────────────────────────────
|
|
197
209
|
"--window-size=1440,900",
|
|
198
210
|
"--hide-scrollbars",
|
|
@@ -299,7 +311,13 @@ var init_chrome_prelaunch = __esm({
|
|
|
299
311
|
else if (arg === "--watch" || arg === "-w") flags.watchFromArgv = true;
|
|
300
312
|
return flags;
|
|
301
313
|
},
|
|
302
|
-
|
|
314
|
+
// QUNITX_BROWSER env var seeds the default so prelaunch is skipped for firefox/webkit
|
|
315
|
+
// even when --browser is not passed on the command line (e.g. browser-compat CI).
|
|
316
|
+
{
|
|
317
|
+
browserFromArgv: process.env.QUNITX_BROWSER || "chromium",
|
|
318
|
+
openFromArgv: false,
|
|
319
|
+
watchFromArgv: false
|
|
320
|
+
}
|
|
303
321
|
));
|
|
304
322
|
openWatchMode = openFromArgv && watchFromArgv;
|
|
305
323
|
earlyChrome = null;
|
|
@@ -310,7 +328,7 @@ var init_chrome_prelaunch = __esm({
|
|
|
310
328
|
});
|
|
311
329
|
}
|
|
312
330
|
perfLog("chrome-prelaunch.ts: module evaluated");
|
|
313
|
-
prelaunchPromise = isRunCommand && browserFromArgv === "chromium" ? findChrome().then((chromePath) => {
|
|
331
|
+
prelaunchPromise = isRunCommand && browserFromArgv === "chromium" && process.platform !== "darwin" ? findChrome().then((chromePath) => {
|
|
314
332
|
perfLog("chrome-prelaunch.ts: findChrome resolved", chromePath);
|
|
315
333
|
return preLaunchChrome(chromePath, CHROMIUM_ARGS, !openWatchMode);
|
|
316
334
|
}).then((info) => {
|
|
@@ -472,10 +490,11 @@ function dumpYaml({
|
|
|
472
490
|
expected,
|
|
473
491
|
message,
|
|
474
492
|
stack,
|
|
493
|
+
source,
|
|
475
494
|
at
|
|
476
495
|
}) {
|
|
477
496
|
return `name: ${dumpString(name, "")}
|
|
478
|
-
` + yamlLine("actual", actual) + yamlLine("expected", expected) + (message !== null ? yamlLine("message", message) : "") + (stack !== null ? yamlLine("stack", stack) : "") + (at !== null ? yamlLine("at", at) : "");
|
|
497
|
+
` + yamlLine("actual", actual) + yamlLine("expected", expected) + (message !== null ? yamlLine("message", message) : "") + (stack !== null ? yamlLine("stack", stack) : "") + (source !== null ? yamlLine("source", source) : "") + (at !== null ? yamlLine("at", at) : "");
|
|
479
498
|
}
|
|
480
499
|
var NEEDS_QUOTING;
|
|
481
500
|
var init_dump_yaml = __esm({
|
|
@@ -498,8 +517,205 @@ var init_indent_string = __esm({
|
|
|
498
517
|
}
|
|
499
518
|
});
|
|
500
519
|
|
|
520
|
+
// lib/utils/source-map-decoder.ts
|
|
521
|
+
function readVLQ(s, pos) {
|
|
522
|
+
let accumulated = 0;
|
|
523
|
+
let shift = 0;
|
|
524
|
+
let digit;
|
|
525
|
+
do {
|
|
526
|
+
digit = BASE64_LOOKUP[s.charCodeAt(pos++)];
|
|
527
|
+
accumulated |= (digit & 31) << shift;
|
|
528
|
+
shift += 5;
|
|
529
|
+
} while (digit & 32);
|
|
530
|
+
return [accumulated & 1 ? -(accumulated >>> 1) : accumulated >>> 1, pos];
|
|
531
|
+
}
|
|
532
|
+
function decodeMappings(mappings) {
|
|
533
|
+
let sourceIndex = 0, sourceLine = 0, sourceCol = 0;
|
|
534
|
+
return mappings.split(";").map((lineStr) => {
|
|
535
|
+
const segments = [];
|
|
536
|
+
let generatedCol = 0;
|
|
537
|
+
let pos = 0;
|
|
538
|
+
while (pos < lineStr.length) {
|
|
539
|
+
if (lineStr[pos] === ",") {
|
|
540
|
+
pos++;
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
let delta;
|
|
544
|
+
[delta, pos] = readVLQ(lineStr, pos);
|
|
545
|
+
generatedCol += delta;
|
|
546
|
+
if (pos >= lineStr.length || lineStr[pos] === ",") continue;
|
|
547
|
+
[delta, pos] = readVLQ(lineStr, pos);
|
|
548
|
+
sourceIndex += delta;
|
|
549
|
+
[delta, pos] = readVLQ(lineStr, pos);
|
|
550
|
+
sourceLine += delta;
|
|
551
|
+
[delta, pos] = readVLQ(lineStr, pos);
|
|
552
|
+
sourceCol += delta;
|
|
553
|
+
if (pos < lineStr.length && lineStr[pos] !== ",") [, pos] = readVLQ(lineStr, pos);
|
|
554
|
+
segments.push({ generatedCol, sourceIndex, sourceLine, sourceCol });
|
|
555
|
+
}
|
|
556
|
+
return segments;
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
function parseSourceMap(json, outDir) {
|
|
560
|
+
const map = JSON.parse(json);
|
|
561
|
+
return {
|
|
562
|
+
segmentsByLine: decodeMappings(map.mappings),
|
|
563
|
+
sources: map.sources ?? [],
|
|
564
|
+
sourceRoot: map.sourceRoot ?? "",
|
|
565
|
+
outDir,
|
|
566
|
+
sourcesContent: map.sourcesContent ?? []
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
function base64DecodeUtf8(b64) {
|
|
570
|
+
const binary = atob(b64);
|
|
571
|
+
return UTF8.decode(Uint8Array.from(binary, (c) => c.charCodeAt(0)));
|
|
572
|
+
}
|
|
573
|
+
function extractInlineSourceMap(bundle, outDir) {
|
|
574
|
+
if (!bundle) return null;
|
|
575
|
+
const text = typeof bundle === "string" ? bundle : UTF8.decode(bundle);
|
|
576
|
+
const match = text.match(
|
|
577
|
+
/\/\/# sourceMappingURL=data:application\/json;base64,([A-Za-z0-9+/=]+)/
|
|
578
|
+
);
|
|
579
|
+
if (!match) return null;
|
|
580
|
+
try {
|
|
581
|
+
return parseSourceMap(base64DecodeUtf8(match[1]), outDir);
|
|
582
|
+
} catch {
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
function normalizePosix(p) {
|
|
587
|
+
const abs = p.startsWith("/");
|
|
588
|
+
const parts = p.split("/");
|
|
589
|
+
const out = [];
|
|
590
|
+
for (const part of parts) {
|
|
591
|
+
if (part === "..") out.pop();
|
|
592
|
+
else if (part !== "" && part !== ".") out.push(part);
|
|
593
|
+
}
|
|
594
|
+
return (abs ? "/" : "") + out.join("/");
|
|
595
|
+
}
|
|
596
|
+
function posixResolve(base, relative) {
|
|
597
|
+
if (relative.startsWith("/")) return normalizePosix(relative);
|
|
598
|
+
return normalizePosix(base + "/" + relative);
|
|
599
|
+
}
|
|
600
|
+
function toAbsolutePath(raw, outDir, sourceRoot) {
|
|
601
|
+
if (raw.startsWith("file://")) return raw.slice(7);
|
|
602
|
+
if (raw.startsWith("/")) return raw;
|
|
603
|
+
const base = sourceRoot ? normalizePosix(outDir + "/" + sourceRoot) : outDir;
|
|
604
|
+
return posixResolve(base, raw);
|
|
605
|
+
}
|
|
606
|
+
function lookupPosition(decoder, generatedLine, generatedCol) {
|
|
607
|
+
const segments = decoder.segmentsByLine[generatedLine - 1];
|
|
608
|
+
if (!segments?.length) return null;
|
|
609
|
+
const col0 = generatedCol - 1;
|
|
610
|
+
let lo = 0, hi = segments.length - 1, best = -1;
|
|
611
|
+
while (lo <= hi) {
|
|
612
|
+
const mid = lo + hi >>> 1;
|
|
613
|
+
if (segments[mid].generatedCol <= col0) {
|
|
614
|
+
best = mid;
|
|
615
|
+
lo = mid + 1;
|
|
616
|
+
} else {
|
|
617
|
+
hi = mid - 1;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
if (best === -1) return null;
|
|
621
|
+
const { sourceIndex, sourceLine, sourceCol } = segments[best];
|
|
622
|
+
const rawSource = decoder.sources[sourceIndex];
|
|
623
|
+
if (!rawSource) return null;
|
|
624
|
+
const content = decoder.sourcesContent[sourceIndex];
|
|
625
|
+
const sourceText = content ? content.split("\n", sourceLine + 1)[sourceLine]?.trim() || null : null;
|
|
626
|
+
return {
|
|
627
|
+
absolutePath: toAbsolutePath(rawSource, decoder.outDir, decoder.sourceRoot),
|
|
628
|
+
line: sourceLine + 1,
|
|
629
|
+
// 0-based → 1-based
|
|
630
|
+
col: sourceCol + 1,
|
|
631
|
+
// 0-based → 1-based
|
|
632
|
+
sourceText
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
function parseFrameLocation(s) {
|
|
636
|
+
const colSep = s.lastIndexOf(":");
|
|
637
|
+
if (colSep < 0) return null;
|
|
638
|
+
const colStr = s.slice(colSep + 1);
|
|
639
|
+
if (!/^\d+$/.test(colStr)) return null;
|
|
640
|
+
const lineSep = s.lastIndexOf(":", colSep - 1);
|
|
641
|
+
if (lineSep < 0) return null;
|
|
642
|
+
const lineStr = s.slice(lineSep + 1, colSep);
|
|
643
|
+
if (!/^\d+$/.test(lineStr)) return null;
|
|
644
|
+
return { url: s.slice(0, lineSep), line: +lineStr, col: +colStr };
|
|
645
|
+
}
|
|
646
|
+
function isBundleUrl(url) {
|
|
647
|
+
const normalized = url.startsWith("async ") ? url.slice(6) : url;
|
|
648
|
+
return /^https?:\/\//.test(normalized) && /\/(tests|filtered-tests)\.js$/.test(normalized);
|
|
649
|
+
}
|
|
650
|
+
function isNodeModulesPath(absolutePath) {
|
|
651
|
+
return absolutePath.includes("/node_modules/") || absolutePath.includes("\\node_modules\\");
|
|
652
|
+
}
|
|
653
|
+
function makeDisplayPath(absolutePath, projectRoot) {
|
|
654
|
+
const prefix = projectRoot + "/";
|
|
655
|
+
return absolutePath.startsWith(prefix) ? absolutePath.slice(prefix.length) : absolutePath;
|
|
656
|
+
}
|
|
657
|
+
function tryResolve(urlLineCol, decoder, projectRoot) {
|
|
658
|
+
const loc = parseFrameLocation(urlLineCol);
|
|
659
|
+
if (!loc || !isBundleUrl(loc.url)) return null;
|
|
660
|
+
const orig = lookupPosition(decoder, loc.line, loc.col);
|
|
661
|
+
if (!orig) return null;
|
|
662
|
+
const display = `${makeDisplayPath(orig.absolutePath, projectRoot)}:${orig.line}:${orig.col}`;
|
|
663
|
+
return {
|
|
664
|
+
display,
|
|
665
|
+
userPath: isNodeModulesPath(orig.absolutePath) ? null : display,
|
|
666
|
+
sourceText: orig.sourceText
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
function resolveFrame(frame, decoder, projectRoot) {
|
|
670
|
+
const chromeName = frame.match(/^(\s*at\s+)(.*?)\s+\(([^)]+)\)\s*$/);
|
|
671
|
+
if (chromeName) {
|
|
672
|
+
const r = tryResolve(chromeName[3], decoder, projectRoot);
|
|
673
|
+
return r ? {
|
|
674
|
+
resolved: `${chromeName[1]}${chromeName[2]} (${r.display})`,
|
|
675
|
+
userPath: r.userPath,
|
|
676
|
+
sourceText: r.sourceText
|
|
677
|
+
} : null;
|
|
678
|
+
}
|
|
679
|
+
const chromeAnon = frame.match(/^(\s*at\s+(?:async\s+)?)(.+)/);
|
|
680
|
+
if (chromeAnon) {
|
|
681
|
+
const r = tryResolve(chromeAnon[2], decoder, projectRoot);
|
|
682
|
+
return r ? { resolved: `${chromeAnon[1]}${r.display}`, userPath: r.userPath, sourceText: r.sourceText } : null;
|
|
683
|
+
}
|
|
684
|
+
const gecko = frame.match(/^([^@]*)@(.+)$/);
|
|
685
|
+
if (gecko) {
|
|
686
|
+
const r = tryResolve(gecko[2], decoder, projectRoot);
|
|
687
|
+
return r ? { resolved: `${gecko[1]}@${r.display}`, userPath: r.userPath, sourceText: r.sourceText } : null;
|
|
688
|
+
}
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
function resolveStack(stack, decoder, projectRoot) {
|
|
692
|
+
let firstUserFrame = null;
|
|
693
|
+
let firstUserSourceText = null;
|
|
694
|
+
const resolvedLines = stack.split("\n").map((frame) => {
|
|
695
|
+
const result = resolveFrame(frame, decoder, projectRoot);
|
|
696
|
+
if (!result) return frame;
|
|
697
|
+
if (!firstUserFrame && result.userPath) {
|
|
698
|
+
firstUserFrame = result.userPath;
|
|
699
|
+
firstUserSourceText = result.sourceText;
|
|
700
|
+
}
|
|
701
|
+
return result.resolved;
|
|
702
|
+
});
|
|
703
|
+
return { resolvedStack: resolvedLines.join("\n"), firstUserFrame, firstUserSourceText };
|
|
704
|
+
}
|
|
705
|
+
var BASE64, BASE64_LOOKUP, UTF8;
|
|
706
|
+
var init_source_map_decoder = __esm({
|
|
707
|
+
"lib/utils/source-map-decoder.ts"() {
|
|
708
|
+
BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
709
|
+
BASE64_LOOKUP = new Uint8Array(128);
|
|
710
|
+
[...BASE64].forEach((ch, i) => {
|
|
711
|
+
BASE64_LOOKUP[ch.charCodeAt(0)] = i;
|
|
712
|
+
});
|
|
713
|
+
UTF8 = new TextDecoder();
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
|
|
501
717
|
// lib/tap/display-test-result.ts
|
|
502
|
-
function TAPDisplayTestResult(COUNTER, details) {
|
|
718
|
+
function TAPDisplayTestResult(COUNTER, details, decoder, projectRoot) {
|
|
503
719
|
COUNTER.testCount++;
|
|
504
720
|
if (details.status === "skipped") {
|
|
505
721
|
COUNTER.skipCount++;
|
|
@@ -519,6 +735,19 @@ function TAPDisplayTestResult(COUNTER, details) {
|
|
|
519
735
|
if (!assertion.passed && assertion.todo === false) {
|
|
520
736
|
COUNTER.errorCount = (COUNTER.errorCount ?? 0) + 1;
|
|
521
737
|
process.stdout.write(" ---\n");
|
|
738
|
+
let stackStr = assertion.stack?.trim() || null;
|
|
739
|
+
let atStr = extractStackAt(assertion.stack);
|
|
740
|
+
let sourceText = null;
|
|
741
|
+
if (decoder && projectRoot && assertion.stack) {
|
|
742
|
+
const { resolvedStack, firstUserFrame, firstUserSourceText } = resolveStack(
|
|
743
|
+
assertion.stack,
|
|
744
|
+
decoder,
|
|
745
|
+
projectRoot
|
|
746
|
+
);
|
|
747
|
+
stackStr = resolvedStack.trim() || null;
|
|
748
|
+
atStr = firstUserFrame;
|
|
749
|
+
sourceText = firstUserSourceText;
|
|
750
|
+
}
|
|
522
751
|
process.stdout.write(
|
|
523
752
|
indentString(
|
|
524
753
|
dumpYaml({
|
|
@@ -526,10 +755,9 @@ function TAPDisplayTestResult(COUNTER, details) {
|
|
|
526
755
|
actual: assertion.actual !== null && typeof assertion.actual === "object" ? JSON.parse(JSON.stringify(assertion.actual, getCircularReplacer())) : assertion.actual,
|
|
527
756
|
expected: assertion.expected !== null && typeof assertion.expected === "object" ? JSON.parse(JSON.stringify(assertion.expected, getCircularReplacer())) : assertion.expected,
|
|
528
757
|
message: assertion.message || null,
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
at: extractStackAt(assertion.stack)
|
|
758
|
+
stack: stackStr,
|
|
759
|
+
source: sourceText,
|
|
760
|
+
at: atStr
|
|
533
761
|
}),
|
|
534
762
|
4
|
|
535
763
|
)
|
|
@@ -573,6 +801,7 @@ var init_display_test_result = __esm({
|
|
|
573
801
|
"lib/tap/display-test-result.ts"() {
|
|
574
802
|
init_dump_yaml();
|
|
575
803
|
init_indent_string();
|
|
804
|
+
init_source_map_decoder();
|
|
576
805
|
}
|
|
577
806
|
});
|
|
578
807
|
|
|
@@ -703,8 +932,8 @@ var init_http = __esm({
|
|
|
703
932
|
});
|
|
704
933
|
}
|
|
705
934
|
/** Registers a GET route handler. */
|
|
706
|
-
get(
|
|
707
|
-
this.#registerRouteHandler("GET",
|
|
935
|
+
get(path10, handler) {
|
|
936
|
+
this.#registerRouteHandler("GET", path10, handler);
|
|
708
937
|
}
|
|
709
938
|
/**
|
|
710
939
|
* Starts listening on the given port (0 = OS-assigned).
|
|
@@ -735,32 +964,32 @@ var init_http = __esm({
|
|
|
735
964
|
});
|
|
736
965
|
}
|
|
737
966
|
/** Registers a POST route handler. */
|
|
738
|
-
post(
|
|
739
|
-
this.#registerRouteHandler("POST",
|
|
967
|
+
post(path10, handler) {
|
|
968
|
+
this.#registerRouteHandler("POST", path10, handler);
|
|
740
969
|
}
|
|
741
970
|
/** Registers a DELETE route handler. */
|
|
742
|
-
delete(
|
|
743
|
-
this.#registerRouteHandler("DELETE",
|
|
971
|
+
delete(path10, handler) {
|
|
972
|
+
this.#registerRouteHandler("DELETE", path10, handler);
|
|
744
973
|
}
|
|
745
974
|
/** Registers a PUT route handler. */
|
|
746
|
-
put(
|
|
747
|
-
this.#registerRouteHandler("PUT",
|
|
975
|
+
put(path10, handler) {
|
|
976
|
+
this.#registerRouteHandler("PUT", path10, handler);
|
|
748
977
|
}
|
|
749
978
|
/** Adds a middleware function to the chain. */
|
|
750
979
|
use(middleware) {
|
|
751
980
|
this.middleware.push(middleware);
|
|
752
981
|
}
|
|
753
|
-
#registerRouteHandler(method,
|
|
982
|
+
#registerRouteHandler(method, path10, handler) {
|
|
754
983
|
if (!this.routes[method]) {
|
|
755
984
|
this.routes[method] = {};
|
|
756
985
|
}
|
|
757
|
-
const paramNames = this.#extractParamNames(
|
|
758
|
-
this.routes[method][
|
|
759
|
-
path:
|
|
986
|
+
const paramNames = this.#extractParamNames(path10);
|
|
987
|
+
this.routes[method][path10] = {
|
|
988
|
+
path: path10,
|
|
760
989
|
handler,
|
|
761
990
|
paramNames,
|
|
762
|
-
isWildcard:
|
|
763
|
-
compiledRegex: paramNames.length > 0 ? new RegExp(`^${this.#buildRegexPattern(
|
|
991
|
+
isWildcard: path10 === "/*",
|
|
992
|
+
compiledRegex: paramNames.length > 0 ? new RegExp(`^${this.#buildRegexPattern(path10, paramNames)}$`) : null
|
|
764
993
|
};
|
|
765
994
|
}
|
|
766
995
|
#handleRequest(req, res) {
|
|
@@ -798,11 +1027,11 @@ var init_http = __esm({
|
|
|
798
1027
|
return null;
|
|
799
1028
|
}
|
|
800
1029
|
return routes[url] || Object.values(routes).find((route) => {
|
|
801
|
-
const { path:
|
|
802
|
-
if (!isWildcard && !
|
|
1030
|
+
const { path: path10, isWildcard } = route;
|
|
1031
|
+
if (!isWildcard && !path10.includes(":")) {
|
|
803
1032
|
return false;
|
|
804
1033
|
}
|
|
805
|
-
if (isWildcard || this.#matchPathSegments(
|
|
1034
|
+
if (isWildcard || this.#matchPathSegments(path10, url)) {
|
|
806
1035
|
if (route.compiledRegex) {
|
|
807
1036
|
const regexMatches = route.compiledRegex.exec(url);
|
|
808
1037
|
if (regexMatches) {
|
|
@@ -812,10 +1041,10 @@ var init_http = __esm({
|
|
|
812
1041
|
return true;
|
|
813
1042
|
}
|
|
814
1043
|
return false;
|
|
815
|
-
}) ||
|
|
1044
|
+
}) || null;
|
|
816
1045
|
}
|
|
817
|
-
#matchPathSegments(
|
|
818
|
-
const pathSegments =
|
|
1046
|
+
#matchPathSegments(path10, url) {
|
|
1047
|
+
const pathSegments = path10.split("/");
|
|
819
1048
|
const urlSegments = url.split("/");
|
|
820
1049
|
if (pathSegments.length !== urlSegments.length) {
|
|
821
1050
|
return false;
|
|
@@ -832,14 +1061,14 @@ var init_http = __esm({
|
|
|
832
1061
|
}
|
|
833
1062
|
return true;
|
|
834
1063
|
}
|
|
835
|
-
#buildRegexPattern(
|
|
836
|
-
let regexPattern =
|
|
1064
|
+
#buildRegexPattern(path10, _paramNames) {
|
|
1065
|
+
let regexPattern = path10.replace(/:[^/]+/g, "([^/]+)");
|
|
837
1066
|
regexPattern = regexPattern.replace(/\//g, "\\/");
|
|
838
1067
|
return regexPattern;
|
|
839
1068
|
}
|
|
840
|
-
#extractParamNames(
|
|
1069
|
+
#extractParamNames(path10) {
|
|
841
1070
|
const paramRegex = /:(\w+)/g;
|
|
842
|
-
const paramMatches =
|
|
1071
|
+
const paramMatches = path10.match(paramRegex);
|
|
843
1072
|
return paramMatches ? paramMatches.map((match) => match.slice(1)) : [];
|
|
844
1073
|
}
|
|
845
1074
|
#extractParams(route, _url) {
|
|
@@ -856,9 +1085,9 @@ var init_http = __esm({
|
|
|
856
1085
|
|
|
857
1086
|
// lib/setup/web-server.ts
|
|
858
1087
|
import fs8 from "node:fs";
|
|
859
|
-
import
|
|
1088
|
+
import path5 from "node:path";
|
|
860
1089
|
function setupWebServer(config, cachedContent) {
|
|
861
|
-
const STATIC_FILES_PATH =
|
|
1090
|
+
const STATIC_FILES_PATH = path5.resolve(config.projectRoot, config.output);
|
|
862
1091
|
const server = new HTTPServer();
|
|
863
1092
|
const mainHTMLWithReplacedAssets = replaceAssetPaths(
|
|
864
1093
|
cachedContent.mainHTML.html,
|
|
@@ -902,7 +1131,7 @@ function setupWebServer(config, cachedContent) {
|
|
|
902
1131
|
);
|
|
903
1132
|
}
|
|
904
1133
|
config._resetTestTimeout?.();
|
|
905
|
-
TAPDisplayTestResult(config.COUNTER, details);
|
|
1134
|
+
TAPDisplayTestResult(config.COUNTER, details, config._sourceMapDecoder, config.projectRoot);
|
|
906
1135
|
} else if (event === "done") {
|
|
907
1136
|
config._phase = "done";
|
|
908
1137
|
config._lastQUnitResult = qunitResult ?? null;
|
|
@@ -919,7 +1148,26 @@ function setupWebServer(config, cachedContent) {
|
|
|
919
1148
|
}
|
|
920
1149
|
});
|
|
921
1150
|
});
|
|
922
|
-
server.get("/tests.js", (_req, res) => {
|
|
1151
|
+
server.get("/tests.js", async (_req, res) => {
|
|
1152
|
+
if (cachedContent._activeRebuild) {
|
|
1153
|
+
await cachedContent._activeRebuild.catch(() => {
|
|
1154
|
+
});
|
|
1155
|
+
if (!cachedContent.allTestCode) {
|
|
1156
|
+
config._lastQUnitResult = {
|
|
1157
|
+
totalTests: 0,
|
|
1158
|
+
finishedTests: 0,
|
|
1159
|
+
failedTests: 0,
|
|
1160
|
+
currentTest: null
|
|
1161
|
+
};
|
|
1162
|
+
config._testRunDone?.();
|
|
1163
|
+
config._testRunDone = null;
|
|
1164
|
+
res.writeHead(200, {
|
|
1165
|
+
"Content-Type": "application/javascript",
|
|
1166
|
+
"Cache-Control": "no-store"
|
|
1167
|
+
});
|
|
1168
|
+
return void res.end();
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
923
1171
|
const bytes = cachedContent.allTestCode?.length ?? null;
|
|
924
1172
|
config.debug && process.stdout.write(
|
|
925
1173
|
`# [HTTPServer] GET /tests.js \u2192 ${bytes !== null ? `${bytes} bytes` : "NOT READY (allTestCode is null)"}
|
|
@@ -927,10 +1175,9 @@ function setupWebServer(config, cachedContent) {
|
|
|
927
1175
|
);
|
|
928
1176
|
if (bytes === null) {
|
|
929
1177
|
res.writeHead(503, { "Content-Type": "application/javascript", "Cache-Control": "no-store" });
|
|
930
|
-
res.end(
|
|
1178
|
+
return void res.end(
|
|
931
1179
|
'console.error("[qunitx] /tests.js requested before bundle was built \u2014 allTestCode is null");'
|
|
932
1180
|
);
|
|
933
|
-
return;
|
|
934
1181
|
}
|
|
935
1182
|
config._onTestsJsServed?.();
|
|
936
1183
|
res.writeHead(200, {
|
|
@@ -948,10 +1195,9 @@ function setupWebServer(config, cachedContent) {
|
|
|
948
1195
|
);
|
|
949
1196
|
if (bytes === null) {
|
|
950
1197
|
res.writeHead(503, { "Content-Type": "application/javascript", "Cache-Control": "no-store" });
|
|
951
|
-
res.end(
|
|
1198
|
+
return res.end(
|
|
952
1199
|
'console.error("[qunitx] /filtered-tests.js requested before bundle was built \u2014 filteredTestCode is null");'
|
|
953
1200
|
);
|
|
954
|
-
return;
|
|
955
1201
|
}
|
|
956
1202
|
config._onTestsJsServed?.();
|
|
957
1203
|
res.writeHead(200, {
|
|
@@ -961,39 +1207,59 @@ function setupWebServer(config, cachedContent) {
|
|
|
961
1207
|
});
|
|
962
1208
|
res.end(cachedContent.filteredTestCode);
|
|
963
1209
|
});
|
|
964
|
-
server.get("/", (_req, res) => {
|
|
1210
|
+
server.get("/", async (_req, res) => {
|
|
1211
|
+
await cachedContent._activeRebuild?.catch(() => {
|
|
1212
|
+
});
|
|
965
1213
|
if (cachedContent._buildError) {
|
|
966
1214
|
const htmlContent = buildErrorHTML(cachedContent._buildError);
|
|
967
1215
|
res.writeHead(200, HTML_HEADERS);
|
|
968
1216
|
res.end(htmlContent);
|
|
969
|
-
|
|
970
|
-
|
|
1217
|
+
if (cachedContent._activeRebuild) {
|
|
1218
|
+
config._lastQUnitResult = {
|
|
1219
|
+
totalTests: 0,
|
|
1220
|
+
finishedTests: 0,
|
|
1221
|
+
failedTests: 0,
|
|
1222
|
+
currentTest: null
|
|
1223
|
+
};
|
|
1224
|
+
config._testRunDone?.();
|
|
1225
|
+
config._testRunDone = null;
|
|
1226
|
+
}
|
|
1227
|
+
return saveHTML(
|
|
1228
|
+
path5.join(path5.resolve(config.projectRoot, config.output), "index.html"),
|
|
1229
|
+
htmlContent
|
|
1230
|
+
);
|
|
971
1231
|
}
|
|
972
1232
|
if (cachedContent._noTestsWarning) {
|
|
973
1233
|
res.writeHead(200, HTML_HEADERS);
|
|
974
|
-
res.end(buildNoTestsHTML(cachedContent._noTestsWarning));
|
|
975
|
-
return;
|
|
1234
|
+
return res.end(buildNoTestsHTML(cachedContent._noTestsWarning));
|
|
976
1235
|
}
|
|
977
1236
|
res.writeHead(200, HTML_HEADERS);
|
|
978
1237
|
res.end(mainIndexHTML);
|
|
979
|
-
saveHTML(
|
|
1238
|
+
saveHTML(
|
|
1239
|
+
path5.join(path5.resolve(config.projectRoot, config.output), "index.html"),
|
|
1240
|
+
mainIndexHTML
|
|
1241
|
+
);
|
|
980
1242
|
});
|
|
981
1243
|
server.get("/qunitx.html", (_req, res) => {
|
|
982
1244
|
if (cachedContent._buildError) {
|
|
983
1245
|
const htmlContent = buildErrorHTML(cachedContent._buildError);
|
|
984
1246
|
res.writeHead(200, HTML_HEADERS);
|
|
985
1247
|
res.end(htmlContent);
|
|
986
|
-
saveHTML(
|
|
987
|
-
|
|
1248
|
+
return saveHTML(
|
|
1249
|
+
path5.join(path5.resolve(config.projectRoot, config.output), "qunitx.html"),
|
|
1250
|
+
htmlContent
|
|
1251
|
+
);
|
|
988
1252
|
}
|
|
989
1253
|
if (cachedContent._noTestsWarning) {
|
|
990
1254
|
res.writeHead(200, HTML_HEADERS);
|
|
991
|
-
res.end(buildNoTestsHTML(cachedContent._noTestsWarning));
|
|
992
|
-
return;
|
|
1255
|
+
return res.end(buildNoTestsHTML(cachedContent._noTestsWarning));
|
|
993
1256
|
}
|
|
994
1257
|
res.writeHead(200, HTML_HEADERS);
|
|
995
1258
|
res.end(mainQunitxHTML);
|
|
996
|
-
saveHTML(
|
|
1259
|
+
saveHTML(
|
|
1260
|
+
path5.join(path5.resolve(config.projectRoot, config.output), "qunitx.html"),
|
|
1261
|
+
mainQunitxHTML
|
|
1262
|
+
);
|
|
997
1263
|
});
|
|
998
1264
|
server.get("/*", (req, res) => {
|
|
999
1265
|
const possibleDynamicHTML = cachedContent.dynamicContentHTMLs[`${config.projectRoot}${req.path}`];
|
|
@@ -1005,13 +1271,13 @@ function setupWebServer(config, cachedContent) {
|
|
|
1005
1271
|
);
|
|
1006
1272
|
res.writeHead(200, HTML_HEADERS);
|
|
1007
1273
|
res.end(htmlContent);
|
|
1008
|
-
saveHTML(
|
|
1274
|
+
saveHTML(path5.join(path5.resolve(config.projectRoot, config.output), req.path), htmlContent);
|
|
1009
1275
|
return;
|
|
1010
1276
|
}
|
|
1011
1277
|
const url = req.url;
|
|
1012
1278
|
const requestStartedAt = Date.now();
|
|
1013
1279
|
const filePath = (url.endsWith("/") ? [STATIC_FILES_PATH, url, "index.html"] : [STATIC_FILES_PATH, url]).join("");
|
|
1014
|
-
const contentType = req.headers.accept?.includes("text/html") ? MIME_TYPES.html : MIME_TYPES[
|
|
1280
|
+
const contentType = req.headers.accept?.includes("text/html") ? MIME_TYPES.html : MIME_TYPES[path5.extname(filePath).substring(1).toLowerCase()] || MIME_TYPES.html;
|
|
1015
1281
|
const stream = fs8.createReadStream(filePath);
|
|
1016
1282
|
stream.on("open", () => {
|
|
1017
1283
|
res.writeHead(200, { "Content-Type": contentType });
|
|
@@ -1036,44 +1302,35 @@ function replaceAssetPaths(html, htmlPath, projectRoot) {
|
|
|
1036
1302
|
const assetPaths = findInternalAssetsFromHTML(html);
|
|
1037
1303
|
const htmlDirectory = htmlPath.split("/").slice(0, -1).join("/");
|
|
1038
1304
|
return assetPaths.reduce((result, assetPath) => {
|
|
1039
|
-
const normalizedFullAbsolutePath =
|
|
1305
|
+
const normalizedFullAbsolutePath = path5.normalize(`${htmlDirectory}/${assetPath}`);
|
|
1040
1306
|
return result.replace(assetPath, normalizedFullAbsolutePath.replace(projectRoot, "."));
|
|
1041
1307
|
}, html);
|
|
1042
1308
|
}
|
|
1043
1309
|
function testRuntimeToInject(config, groupId) {
|
|
1044
1310
|
const groupIdPart = groupId !== void 0 ? `, groupId: ${groupId}` : "";
|
|
1045
1311
|
return `<script>
|
|
1046
|
-
window.testTimeout = 0;
|
|
1047
|
-
setInterval(() => {
|
|
1048
|
-
window.testTimeout = window.testTimeout + 1000;
|
|
1049
|
-
}, 1000);
|
|
1050
|
-
|
|
1051
1312
|
(function() {
|
|
1052
|
-
//
|
|
1053
|
-
//
|
|
1054
|
-
//
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
// tests.js (loaded as an async external script) dispatches this event after registering
|
|
1064
|
-
// all test modules. Decoupled from WS so Chrome can compile tests.js in a background
|
|
1065
|
-
// thread while the main thread handles the WebSocket handshake.
|
|
1066
|
-
window.addEventListener('qunitx:tests-ready', function() {
|
|
1067
|
-
testsLoaded = true;
|
|
1068
|
-
maybeStart();
|
|
1313
|
+
// setupQUnit runs exactly once, after both the WebSocket is open and tests.js has loaded.
|
|
1314
|
+
// Promise.all is naturally idempotent \u2014 resolving a Promise a second time is a no-op,
|
|
1315
|
+
// so WebKit firing WS error after open (causing a retry that re-opens) cannot double-start.
|
|
1316
|
+
let resolveWsReady = () => {};
|
|
1317
|
+
const wsReadyPromise = window.location.protocol === 'file:'
|
|
1318
|
+
? Promise.resolve()
|
|
1319
|
+
: new Promise(resolve => { resolveWsReady = resolve; });
|
|
1320
|
+
|
|
1321
|
+
// { once: true } auto-removes the listener after the first fire.
|
|
1322
|
+
const testsReadyPromise = new Promise(resolve => {
|
|
1323
|
+
window.addEventListener('qunitx:tests-ready', resolve, { once: true });
|
|
1069
1324
|
});
|
|
1070
1325
|
|
|
1071
|
-
|
|
1072
|
-
|
|
1326
|
+
Promise.all([wsReadyPromise, testsReadyPromise]).then(setupQUnit);
|
|
1327
|
+
|
|
1328
|
+
// For static files (file:// protocol) there is no WebSocket server; wsReadyPromise
|
|
1329
|
+
// is already resolved above, so setupQUnit fires as soon as tests load.
|
|
1073
1330
|
if (window.location.protocol === 'file:') return;
|
|
1074
1331
|
|
|
1075
1332
|
let wsRetryCount = 0;
|
|
1076
|
-
const WS_MAX_RETRIES = Math.ceil(${config.timeout} /
|
|
1333
|
+
const WS_MAX_RETRIES = Math.ceil(${config.timeout} / ${WS_RETRY_INTERVAL_MS}); // retry for the full test timeout window
|
|
1077
1334
|
|
|
1078
1335
|
function setupWebSocket() {
|
|
1079
1336
|
try {
|
|
@@ -1085,14 +1342,13 @@ function testRuntimeToInject(config, groupId) {
|
|
|
1085
1342
|
}
|
|
1086
1343
|
|
|
1087
1344
|
window.socket.addEventListener('open', function() {
|
|
1088
|
-
|
|
1345
|
+
resolveWsReady();
|
|
1089
1346
|
// Notify Node.js that the WS socket is open. This fires immediately (< 1 s) because
|
|
1090
1347
|
// this runtime script is tiny \u2014 tests.js background compilation hasn't finished yet.
|
|
1091
1348
|
// Node.js uses this to distinguish "WS never connected" from "WS connected but bundle slow".
|
|
1092
1349
|
if (navigator.webdriver) {
|
|
1093
1350
|
window.socket.send(JSON.stringify({ event: 'wsOpen'${groupIdPart} }));
|
|
1094
1351
|
}
|
|
1095
|
-
maybeStart();
|
|
1096
1352
|
});
|
|
1097
1353
|
window.socket.addEventListener('error', function() {
|
|
1098
1354
|
retryOrFail();
|
|
@@ -1112,10 +1368,9 @@ function testRuntimeToInject(config, groupId) {
|
|
|
1112
1368
|
wsRetryCount++;
|
|
1113
1369
|
if (wsRetryCount > WS_MAX_RETRIES) {
|
|
1114
1370
|
console.log('WebSocket connection failed after ' + WS_MAX_RETRIES + ' retries');
|
|
1115
|
-
window.testTimeout = ${config.timeout};
|
|
1116
1371
|
return;
|
|
1117
1372
|
}
|
|
1118
|
-
window.setTimeout(setupWebSocket,
|
|
1373
|
+
window.setTimeout(setupWebSocket, ${WS_RETRY_INTERVAL_MS});
|
|
1119
1374
|
}
|
|
1120
1375
|
|
|
1121
1376
|
setupWebSocket();
|
|
@@ -1149,8 +1404,6 @@ function testRuntimeToInject(config, groupId) {
|
|
|
1149
1404
|
// "no tests registered" warning (not a failure), so this gives a fast, clean result.
|
|
1150
1405
|
window.QUNIT_RESULT = { totalTests: 0, finishedTests: 0, failedTests: 0, currentTest: null };
|
|
1151
1406
|
window.socket.send(JSON.stringify({ event: 'done', details: { passed: 0, failed: 0, runtime: 0 } }));
|
|
1152
|
-
} else {
|
|
1153
|
-
window.testTimeout = ${config.timeout};
|
|
1154
1407
|
}
|
|
1155
1408
|
return;
|
|
1156
1409
|
}
|
|
@@ -1165,7 +1418,6 @@ function testRuntimeToInject(config, groupId) {
|
|
|
1165
1418
|
window.QUNIT_RESULT.currentTest = details.fullName.join(' | ');
|
|
1166
1419
|
});
|
|
1167
1420
|
window.QUnit.on('testEnd', (details) => { // NOTE: https://github.com/qunitjs/qunit/blob/master/src/html-reporter/diff.js
|
|
1168
|
-
window.testTimeout = 0;
|
|
1169
1421
|
window.QUNIT_RESULT.finishedTests++;
|
|
1170
1422
|
if (details.status === 'failed') window.QUNIT_RESULT.failedTests++;
|
|
1171
1423
|
window.QUNIT_RESULT.currentTest = null;
|
|
@@ -1182,16 +1434,10 @@ function testRuntimeToInject(config, groupId) {
|
|
|
1182
1434
|
window.QUnit.done((details) => {
|
|
1183
1435
|
if (navigator.webdriver) {
|
|
1184
1436
|
window.socket.send(JSON.stringify({ event: 'done', details: details, qunitResult: window.QUNIT_RESULT, abort: window.abortQUnit }, getCircularReplacer()));
|
|
1185
|
-
// Do NOT set testTimeout here. The WS 'done' event (testsDone promise) is the
|
|
1186
|
-
// canonical completion signal for Playwright runs. waitForFunction is reserved
|
|
1187
|
-
// for true timeouts (test hangs) where testTimeout increments naturally via setInterval.
|
|
1188
|
-
// Setting testTimeout after done caused a race: under CI load, waitForFunction could
|
|
1189
|
-
// win before Node.js processed the WS done message, dropping all testEnd events.
|
|
1190
|
-
} else {
|
|
1191
|
-
window.testTimeout = ${config.timeout};
|
|
1192
1437
|
}
|
|
1193
1438
|
});
|
|
1194
1439
|
|
|
1440
|
+
window.QUnit.config.testTimeout = ${config.timeout};
|
|
1195
1441
|
window.QUnit.start();
|
|
1196
1442
|
}
|
|
1197
1443
|
</script>`;
|
|
@@ -1292,8 +1538,8 @@ function buildNoTestsHTML(files) {
|
|
|
1292
1538
|
var retries = 0;
|
|
1293
1539
|
function connect() {
|
|
1294
1540
|
var ws = new WebSocket(\`ws://\${location.hostname}:\${location.port}\`);
|
|
1295
|
-
ws.addEventListener('message', function (e) { if (e.data === 'refresh') location.reload(true); });
|
|
1296
|
-
ws.addEventListener('close', function () { if (retries++ <
|
|
1541
|
+
ws.addEventListener('message', function (e) { if (e.data === 'refresh' && !navigator.webdriver) location.reload(true); });
|
|
1542
|
+
ws.addEventListener('close', function () { if (retries++ < ${WATCH_WS_RECONNECT_MAX_RETRIES}) setTimeout(connect, ${WATCH_WS_RECONNECT_INTERVAL_MS}); });
|
|
1297
1543
|
ws.addEventListener('error', function () { ws.close(); });
|
|
1298
1544
|
}
|
|
1299
1545
|
connect();
|
|
@@ -1394,8 +1640,8 @@ function buildErrorHTML(buildError) {
|
|
|
1394
1640
|
var retries = 0;
|
|
1395
1641
|
function connect() {
|
|
1396
1642
|
var ws = new WebSocket(\`ws://\${location.hostname}:\${location.port}\`);
|
|
1397
|
-
ws.addEventListener('message', function (e) { if (e.data === 'refresh') location.reload(true); });
|
|
1398
|
-
ws.addEventListener('close', function () { if (retries++ <
|
|
1643
|
+
ws.addEventListener('message', function (e) { if (e.data === 'refresh' && !navigator.webdriver) location.reload(true); });
|
|
1644
|
+
ws.addEventListener('close', function () { if (retries++ < ${WATCH_WS_RECONNECT_MAX_RETRIES}) setTimeout(connect, ${WATCH_WS_RECONNECT_INTERVAL_MS}); });
|
|
1399
1645
|
ws.addEventListener('error', function () { ws.close(); });
|
|
1400
1646
|
}
|
|
1401
1647
|
connect();
|
|
@@ -1424,26 +1670,26 @@ function registerGroupRoutes(server, groupConfig, groupCachedContent, groupId) {
|
|
|
1424
1670
|
server.get(`/group-${groupId}/`, (_req, res) => {
|
|
1425
1671
|
if (groupCachedContent._buildError) {
|
|
1426
1672
|
res.writeHead(200, HTML_HEADERS);
|
|
1427
|
-
res.end(buildErrorHTML(groupCachedContent._buildError));
|
|
1428
|
-
return;
|
|
1673
|
+
return res.end(buildErrorHTML(groupCachedContent._buildError));
|
|
1429
1674
|
}
|
|
1430
1675
|
if (groupCachedContent._noTestsWarning) {
|
|
1431
1676
|
res.writeHead(200, HTML_HEADERS);
|
|
1432
|
-
res.end(buildNoTestsHTML(groupCachedContent._noTestsWarning));
|
|
1433
|
-
return;
|
|
1677
|
+
return res.end(buildNoTestsHTML(groupCachedContent._noTestsWarning));
|
|
1434
1678
|
}
|
|
1435
1679
|
res.writeHead(200, HTML_HEADERS);
|
|
1436
1680
|
res.end(mainGroupHTML);
|
|
1437
|
-
saveHTML(
|
|
1681
|
+
saveHTML(
|
|
1682
|
+
path5.join(path5.resolve(groupConfig.projectRoot, groupConfig.output), "index.html"),
|
|
1683
|
+
mainGroupHTML
|
|
1684
|
+
);
|
|
1438
1685
|
});
|
|
1439
1686
|
server.get(`/group-${groupId}/tests.js`, (_req, res) => {
|
|
1440
1687
|
const bytes = groupCachedContent.allTestCode?.length ?? null;
|
|
1441
1688
|
if (bytes === null) {
|
|
1442
1689
|
res.writeHead(503, { "Content-Type": "application/javascript", "Cache-Control": "no-store" });
|
|
1443
|
-
res.end(
|
|
1690
|
+
return res.end(
|
|
1444
1691
|
'console.error("[qunitx] /tests.js requested before bundle was built \u2014 allTestCode is null");'
|
|
1445
1692
|
);
|
|
1446
|
-
return;
|
|
1447
1693
|
}
|
|
1448
1694
|
groupConfig._onTestsJsServed?.();
|
|
1449
1695
|
res.writeHead(200, {
|
|
@@ -1495,7 +1741,7 @@ function setupGroupWSHandler(server, groupConfigs) {
|
|
|
1495
1741
|
);
|
|
1496
1742
|
}
|
|
1497
1743
|
config._resetTestTimeout?.();
|
|
1498
|
-
TAPDisplayTestResult(config.COUNTER, details);
|
|
1744
|
+
TAPDisplayTestResult(config.COUNTER, details, config._sourceMapDecoder, config.projectRoot);
|
|
1499
1745
|
} else if (event === "done") {
|
|
1500
1746
|
config._phase = "done";
|
|
1501
1747
|
config._lastQUnitResult = qunitResult ?? null;
|
|
@@ -1529,10 +1775,10 @@ function registerSharedStaticHandler(server, groupConfigs) {
|
|
|
1529
1775
|
res.end("Not found");
|
|
1530
1776
|
return;
|
|
1531
1777
|
}
|
|
1532
|
-
const STATIC_FILES_PATH =
|
|
1778
|
+
const STATIC_FILES_PATH = path5.resolve(groupConfig.projectRoot, groupConfig.output);
|
|
1533
1779
|
const subPath = match[2] || "/";
|
|
1534
1780
|
const filePath = (subPath.endsWith("/") ? [STATIC_FILES_PATH, subPath, "index.html"] : [STATIC_FILES_PATH, subPath]).join("");
|
|
1535
|
-
const contentType = req.headers.accept?.includes("text/html") ? MIME_TYPES.html : MIME_TYPES[
|
|
1781
|
+
const contentType = req.headers.accept?.includes("text/html") ? MIME_TYPES.html : MIME_TYPES[path5.extname(filePath).substring(1).toLowerCase()] || MIME_TYPES.html;
|
|
1536
1782
|
const stream = fs8.createReadStream(filePath);
|
|
1537
1783
|
stream.on("open", () => {
|
|
1538
1784
|
res.writeHead(200, { "Content-Type": contentType });
|
|
@@ -1544,7 +1790,7 @@ function registerSharedStaticHandler(server, groupConfigs) {
|
|
|
1544
1790
|
});
|
|
1545
1791
|
});
|
|
1546
1792
|
}
|
|
1547
|
-
var fsPromise, HTML_HEADERS, NOT_FOUND_HTML;
|
|
1793
|
+
var fsPromise, HTML_HEADERS, WATCH_WS_RECONNECT_INTERVAL_MS, WATCH_WS_RECONNECT_MAX_RETRIES, WS_RETRY_INTERVAL_MS, NOT_FOUND_HTML;
|
|
1548
1794
|
var init_web_server = __esm({
|
|
1549
1795
|
"lib/setup/web-server.ts"() {
|
|
1550
1796
|
init_find_internal_assets_from_html();
|
|
@@ -1554,6 +1800,9 @@ var init_web_server = __esm({
|
|
|
1554
1800
|
init_http();
|
|
1555
1801
|
fsPromise = fs8.promises;
|
|
1556
1802
|
HTML_HEADERS = { "Content-Type": "text/html", "Cache-Control": "no-store" };
|
|
1803
|
+
WATCH_WS_RECONNECT_INTERVAL_MS = 1e3;
|
|
1804
|
+
WATCH_WS_RECONNECT_MAX_RETRIES = 120;
|
|
1805
|
+
WS_RETRY_INTERVAL_MS = 10;
|
|
1557
1806
|
NOT_FOUND_HTML = `<!DOCTYPE html>
|
|
1558
1807
|
<html lang="en">
|
|
1559
1808
|
<head>
|
|
@@ -1599,13 +1848,23 @@ async function launchBrowser(config) {
|
|
|
1599
1848
|
);
|
|
1600
1849
|
if (prelaunch) {
|
|
1601
1850
|
const connectStart = Date.now();
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1851
|
+
try {
|
|
1852
|
+
const browser = await playwrightCore2.chromium.connectOverCDP({
|
|
1853
|
+
endpointURL: prelaunch.cdpEndpoint,
|
|
1854
|
+
// Short timeout: if Chrome isn't CDP-ready within 5s (e.g. resource contention on
|
|
1855
|
+
// slow CI runners with many concurrent pre-launches), fall back to chromium.launch().
|
|
1856
|
+
timeout: 5e3
|
|
1857
|
+
});
|
|
1858
|
+
perfLog(`browser.js: connectOverCDP took ${Date.now() - connectStart}ms`);
|
|
1859
|
+
return browser;
|
|
1860
|
+
} catch {
|
|
1861
|
+
perfLog(
|
|
1862
|
+
`browser.js: connectOverCDP failed after ${Date.now() - connectStart}ms \u2014 falling back to chromium.launch()`
|
|
1863
|
+
);
|
|
1864
|
+
await shutdownPrelaunch();
|
|
1865
|
+
}
|
|
1607
1866
|
}
|
|
1608
|
-
const executablePath = await findChrome();
|
|
1867
|
+
const executablePath = process.platform !== "darwin" ? await findChrome() : null;
|
|
1609
1868
|
const launchOptions = {
|
|
1610
1869
|
args: CHROMIUM_ARGS,
|
|
1611
1870
|
headless: true,
|
|
@@ -1637,12 +1896,9 @@ async function setupBrowser(config, cachedContent, existingBrowser = null, share
|
|
|
1637
1896
|
perfLog(`browser.js: newPage (shared server) took ${Date.now() - setupStart}ms`);
|
|
1638
1897
|
return [sharedServer, existingBrowser, newPage2];
|
|
1639
1898
|
}
|
|
1640
|
-
const
|
|
1641
|
-
setupWebServer(config, cachedContent),
|
|
1642
|
-
Promise.resolve(existingBrowser)
|
|
1643
|
-
]);
|
|
1899
|
+
const newServer = setupWebServer(config, cachedContent);
|
|
1644
1900
|
perfLog(`browser.js: setupWebServer took ${Date.now() - setupStart}ms`);
|
|
1645
|
-
const activeBrowser =
|
|
1901
|
+
const activeBrowser = existingBrowser ?? await launchBrowser(config);
|
|
1646
1902
|
const pageStart = Date.now();
|
|
1647
1903
|
const isHeadedWatchMode = config.open === true && config.watch;
|
|
1648
1904
|
const getPage = isHeadedWatchMode ? () => activeBrowser.contexts()[0]?.pages()[0] ?? activeBrowser.newPage() : () => activeBrowser.newPage();
|
|
@@ -1704,9 +1960,11 @@ var init_browser = __esm({
|
|
|
1704
1960
|
|
|
1705
1961
|
// lib/utils/open-output-in-browser.ts
|
|
1706
1962
|
import { spawn as spawn2 } from "node:child_process";
|
|
1963
|
+
import path6 from "node:path";
|
|
1964
|
+
import { pathToFileURL } from "node:url";
|
|
1707
1965
|
async function openOutputInBrowser(config) {
|
|
1708
1966
|
try {
|
|
1709
|
-
const outputFile = config.watch ? `http://localhost:${config.port}` :
|
|
1967
|
+
const outputFile = config.watch ? `http://localhost:${config.port}` : pathToFileURL(path6.join(path6.resolve(config.projectRoot, config.output), "index.html")).href;
|
|
1710
1968
|
if (typeof config.open === "string") {
|
|
1711
1969
|
spawnDetached(config.open, [outputFile]);
|
|
1712
1970
|
return;
|
|
@@ -1752,9 +2010,10 @@ var init_time_counter = __esm({
|
|
|
1752
2010
|
});
|
|
1753
2011
|
|
|
1754
2012
|
// lib/utils/run-user-module.ts
|
|
2013
|
+
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
1755
2014
|
async function runUserModule(modulePath, params, scriptPosition) {
|
|
1756
2015
|
try {
|
|
1757
|
-
const func = await import(modulePath);
|
|
2016
|
+
const func = await import(pathToFileURL2(modulePath).href);
|
|
1758
2017
|
if (func) {
|
|
1759
2018
|
func.default ? await func.default(params) : typeof func === "function" ? await func(params) : null;
|
|
1760
2019
|
}
|
|
@@ -1797,8 +2056,14 @@ var init_display_final_result = __esm({
|
|
|
1797
2056
|
|
|
1798
2057
|
// lib/commands/run/tests-in-browser.ts
|
|
1799
2058
|
import fs9 from "node:fs/promises";
|
|
1800
|
-
import
|
|
2059
|
+
import path7 from "node:path";
|
|
1801
2060
|
import esbuild from "esbuild";
|
|
2061
|
+
function toEsbuildImportPath(filePath) {
|
|
2062
|
+
const rel = path7.relative(process.cwd(), filePath);
|
|
2063
|
+
const normalized = rel.replace(/\\/g, "/");
|
|
2064
|
+
if (path7.isAbsolute(rel)) return filePath.replace(/\\/g, "/");
|
|
2065
|
+
return normalized.startsWith(".") ? normalized : "./" + normalized;
|
|
2066
|
+
}
|
|
1802
2067
|
function deriveBuildErrorType(error) {
|
|
1803
2068
|
const msgs = error?.errors ?? [];
|
|
1804
2069
|
const text = msgs[0]?.text ?? (error instanceof Error ? error.message : String(error));
|
|
@@ -1834,13 +2099,14 @@ async function buildTestBundle(config, cachedContent) {
|
|
|
1834
2099
|
console.log("# [buildTestBundle] fsTree is empty \u2014 skipping build (no test files found)");
|
|
1835
2100
|
return;
|
|
1836
2101
|
}
|
|
1837
|
-
const
|
|
1838
|
-
|
|
1839
|
-
|
|
2102
|
+
const outDir = path7.resolve(projectRoot, output);
|
|
2103
|
+
const outfile = path7.join(outDir, "tests.js");
|
|
2104
|
+
await fs9.mkdir(outDir, { recursive: true });
|
|
2105
|
+
const sourcemap = "inline";
|
|
1840
2106
|
const needsDisk = true;
|
|
1841
2107
|
const buildOptions = {
|
|
1842
2108
|
stdin: {
|
|
1843
|
-
contents: allTestFilePaths.map((filePath) => `import "${filePath}";`).join(""),
|
|
2109
|
+
contents: allTestFilePaths.map((filePath) => `import "${toEsbuildImportPath(filePath)}";`).join(""),
|
|
1844
2110
|
resolveDir: process.cwd()
|
|
1845
2111
|
},
|
|
1846
2112
|
// Allow test files outside the project root (e.g. /tmp/my-test.ts) to import
|
|
@@ -1867,29 +2133,28 @@ async function buildTestBundle(config, cachedContent) {
|
|
|
1867
2133
|
config.watch ? buildIncrementally(buildOptions, allTestFilePaths.join("\0"), cachedContent, needsDisk) : buildWithOverlayfsRetry(buildOptions, needsDisk),
|
|
1868
2134
|
Promise.all(
|
|
1869
2135
|
cachedContent.htmlPathsToRunTests.map(async (htmlPath) => {
|
|
1870
|
-
const targetPath =
|
|
2136
|
+
const targetPath = path7.join(outDir, htmlPath);
|
|
1871
2137
|
if (htmlPath !== "/") {
|
|
1872
2138
|
await fs9.rm(targetPath, { force: true, recursive: true });
|
|
1873
|
-
await fs9.mkdir(
|
|
2139
|
+
await fs9.mkdir(path7.dirname(targetPath), { recursive: true });
|
|
1874
2140
|
}
|
|
1875
2141
|
})
|
|
1876
2142
|
)
|
|
1877
2143
|
]);
|
|
1878
2144
|
cachedContent.allTestCode = allTestCode;
|
|
2145
|
+
config._sourceMapDecoder = extractInlineSourceMap(allTestCode, outDir);
|
|
1879
2146
|
} catch (error) {
|
|
1880
2147
|
cachedContent._buildError = {
|
|
1881
2148
|
type: deriveBuildErrorType(error),
|
|
1882
2149
|
formatted: formatBuildErrors(error)
|
|
1883
2150
|
};
|
|
1884
|
-
await fs9.writeFile(
|
|
1885
|
-
`${projectRoot}/${output}/index.html`,
|
|
1886
|
-
buildErrorHTML(cachedContent._buildError)
|
|
1887
|
-
);
|
|
2151
|
+
await fs9.writeFile(path7.join(outDir, "index.html"), buildErrorHTML(cachedContent._buildError));
|
|
1888
2152
|
throw error;
|
|
1889
2153
|
}
|
|
1890
2154
|
}
|
|
1891
2155
|
async function runTestsInBrowser(config, cachedContent = {}, connections, targetTestFilesToFilter) {
|
|
1892
2156
|
const { projectRoot, output } = config;
|
|
2157
|
+
const outDir = path7.resolve(projectRoot, output);
|
|
1893
2158
|
const allTestFilePaths = Object.keys(config.fsTree);
|
|
1894
2159
|
const runHasFilter = !!targetTestFilesToFilter;
|
|
1895
2160
|
if (!config._groupMode) {
|
|
@@ -1907,18 +2172,23 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
1907
2172
|
const preBuildPromise = cachedContent._preBuildPromise;
|
|
1908
2173
|
cachedContent._preBuildPromise = null;
|
|
1909
2174
|
if (!cachedContent.allTestCode) {
|
|
1910
|
-
|
|
2175
|
+
if (preBuildPromise) {
|
|
2176
|
+
cachedContent._activeRebuild = preBuildPromise;
|
|
2177
|
+
} else {
|
|
2178
|
+
await buildTestBundle(config, cachedContent);
|
|
2179
|
+
}
|
|
1911
2180
|
}
|
|
1912
|
-
if (!cachedContent.allTestCode) {
|
|
2181
|
+
if (!cachedContent.allTestCode && !cachedContent._activeRebuild) {
|
|
1913
2182
|
return connections;
|
|
1914
2183
|
}
|
|
1915
2184
|
if (runHasFilter) {
|
|
1916
|
-
const outputPath =
|
|
2185
|
+
const outputPath = path7.join(outDir, "filtered-tests.js");
|
|
1917
2186
|
cachedContent.filteredTestCode = await buildFilteredTests(
|
|
1918
2187
|
targetTestFilesToFilter,
|
|
1919
2188
|
outputPath,
|
|
1920
2189
|
config
|
|
1921
2190
|
);
|
|
2191
|
+
config._sourceMapDecoder = extractInlineSourceMap(cachedContent.filteredTestCode, outDir);
|
|
1922
2192
|
}
|
|
1923
2193
|
const TIME_COUNTER = timeCounter();
|
|
1924
2194
|
if (runHasFilter) {
|
|
@@ -1930,6 +2200,17 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
1930
2200
|
)
|
|
1931
2201
|
);
|
|
1932
2202
|
}
|
|
2203
|
+
if (cachedContent._activeRebuild) {
|
|
2204
|
+
await cachedContent._activeRebuild.catch(() => {
|
|
2205
|
+
});
|
|
2206
|
+
cachedContent._activeRebuild = null;
|
|
2207
|
+
if (!cachedContent.allTestCode) {
|
|
2208
|
+
config.watch && cachedContent._buildError && console.log(
|
|
2209
|
+
`# esbuild Bundle Error: ${cachedContent._buildError.formatted}`.split("\n").join("\n# ")
|
|
2210
|
+
);
|
|
2211
|
+
return connections;
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
1933
2214
|
const TIME_TAKEN = TIME_COUNTER.stop();
|
|
1934
2215
|
if (!config._groupMode) {
|
|
1935
2216
|
if (config.COUNTER.testCount === 0 && !cachedContent._buildError) {
|
|
@@ -1941,7 +2222,7 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
1941
2222
|
console.log(
|
|
1942
2223
|
`# Warning: 0 tests registered \u2014 no QUnit test cases found in ${allTestFilePaths.length} ${fileWord}`
|
|
1943
2224
|
);
|
|
1944
|
-
fs9.writeFile(
|
|
2225
|
+
fs9.writeFile(path7.join(outDir, "index.html"), buildNoTestsHTML(displayFiles)).catch(
|
|
1945
2226
|
() => {
|
|
1946
2227
|
}
|
|
1947
2228
|
);
|
|
@@ -1961,6 +2242,7 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
1961
2242
|
}
|
|
1962
2243
|
}
|
|
1963
2244
|
} catch (error) {
|
|
2245
|
+
cachedContent._activeRebuild = null;
|
|
1964
2246
|
config.lastFailedTestFiles = config.lastRanTestFiles;
|
|
1965
2247
|
const exception = new BundleError(error);
|
|
1966
2248
|
if (!cachedContent._buildError && error.errors?.length) {
|
|
@@ -1969,7 +2251,7 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
1969
2251
|
formatted: formatBuildErrors(error)
|
|
1970
2252
|
};
|
|
1971
2253
|
fs9.writeFile(
|
|
1972
|
-
|
|
2254
|
+
path7.join(outDir, "qunitx.html"),
|
|
1973
2255
|
buildErrorHTML(cachedContent._buildError)
|
|
1974
2256
|
).catch(
|
|
1975
2257
|
(err) => config.debug && process.stderr.write(`# [qunitx] writeFile qunitx.html: ${err.message}
|
|
@@ -1985,12 +2267,12 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
1985
2267
|
return connections;
|
|
1986
2268
|
}
|
|
1987
2269
|
function buildFilteredTests(filteredTests, outputPath, config) {
|
|
1988
|
-
const sourcemap =
|
|
1989
|
-
const needsDisk =
|
|
2270
|
+
const sourcemap = "inline";
|
|
2271
|
+
const needsDisk = Boolean(config.open);
|
|
1990
2272
|
return buildWithOverlayfsRetry(
|
|
1991
2273
|
{
|
|
1992
2274
|
stdin: {
|
|
1993
|
-
contents: filteredTests.map((filePath) => `import "${filePath}";`).join(""),
|
|
2275
|
+
contents: filteredTests.map((filePath) => `import "${filePath.replace(/\\/g, "/")}";`).join(""),
|
|
1994
2276
|
resolveDir: process.cwd()
|
|
1995
2277
|
},
|
|
1996
2278
|
nodePaths: ANCESTOR_NODE_MODULES,
|
|
@@ -2060,9 +2342,9 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
|
|
|
2060
2342
|
let wsConnected = false;
|
|
2061
2343
|
try {
|
|
2062
2344
|
console.log("#", blue(`QUnitX running: http://localhost:${config.port}${filePath}`));
|
|
2063
|
-
const navMs = config.timeout +
|
|
2064
|
-
const startupMs = Math.max(config.timeout *
|
|
2065
|
-
const testsJsMs = Math.max(config.timeout *
|
|
2345
|
+
const navMs = config.timeout + NAV_GRACE_MS;
|
|
2346
|
+
const startupMs = Math.max(config.timeout * STARTUP_TIMEOUT_FACTOR, navMs);
|
|
2347
|
+
const testsJsMs = Math.max(config.timeout * TESTS_JS_TIMEOUT_FACTOR, navMs);
|
|
2066
2348
|
let resolveTestRace;
|
|
2067
2349
|
const testRaceResult = new Promise((resolve) => {
|
|
2068
2350
|
resolveTestRace = resolve;
|
|
@@ -2080,7 +2362,7 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
|
|
|
2080
2362
|
config._resetTestTimeout = () => {
|
|
2081
2363
|
wsConnected = true;
|
|
2082
2364
|
clearTimeout(timeoutHandle);
|
|
2083
|
-
timeoutHandle = setTimeout(resolveTestRace, config.timeout);
|
|
2365
|
+
timeoutHandle = setTimeout(resolveTestRace, config.timeout + TEST_STALL_BUFFER_MS);
|
|
2084
2366
|
};
|
|
2085
2367
|
const targetUrl = `http://localhost:${config.port}${filePath}`;
|
|
2086
2368
|
const navOptions = { timeout: navMs, waitUntil: "commit" };
|
|
@@ -2150,7 +2432,7 @@ async function failOnNonWatchMode(watchMode = false, connections = {}, groupMode
|
|
|
2150
2432
|
process.exit(1);
|
|
2151
2433
|
}
|
|
2152
2434
|
}
|
|
2153
|
-
async function flushConsoleHandlers(handlers, deadline = Date.now() +
|
|
2435
|
+
async function flushConsoleHandlers(handlers, deadline = Date.now() + CONSOLE_FLUSH_TIMEOUT_MS) {
|
|
2154
2436
|
if (!handlers || handlers.size === 0 || Date.now() >= deadline) return;
|
|
2155
2437
|
await Promise.allSettled([...handlers]);
|
|
2156
2438
|
return flushConsoleHandlers(handlers, deadline);
|
|
@@ -2160,7 +2442,7 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
|
2160
2442
|
cachedContent._buildError = null;
|
|
2161
2443
|
cachedContent._noTestsWarning = null;
|
|
2162
2444
|
});
|
|
2163
|
-
const { projectRoot, debug,
|
|
2445
|
+
const { projectRoot, debug, browser } = groupConfigs[0];
|
|
2164
2446
|
const activeGroups = groupConfigs.reduce(
|
|
2165
2447
|
(acc, groupConfig, groupIndex) => {
|
|
2166
2448
|
const files = Object.keys(groupConfig.fsTree);
|
|
@@ -2181,10 +2463,10 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
|
2181
2463
|
);
|
|
2182
2464
|
await Promise.all(
|
|
2183
2465
|
activeGroups.map(
|
|
2184
|
-
(group) => fs9.mkdir(
|
|
2466
|
+
(group) => fs9.mkdir(path7.resolve(group.config.projectRoot, group.config.output), { recursive: true })
|
|
2185
2467
|
)
|
|
2186
2468
|
);
|
|
2187
|
-
const sourcemap =
|
|
2469
|
+
const sourcemap = "inline";
|
|
2188
2470
|
const groupEntryPlugin = {
|
|
2189
2471
|
name: "group-entry-loader",
|
|
2190
2472
|
setup(build) {
|
|
@@ -2195,12 +2477,13 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
|
2195
2477
|
build.onLoad({ filter: /.*/, namespace: "group-entry" }, (args) => {
|
|
2196
2478
|
const slotIndex = parseInt(args.path.replace("group-entry-", ""));
|
|
2197
2479
|
return {
|
|
2198
|
-
contents: activeGroups[slotIndex].files.map((filePath) => `import "${filePath}";`).join(""),
|
|
2480
|
+
contents: activeGroups[slotIndex].files.map((filePath) => `import "${toEsbuildImportPath(filePath)}";`).join(""),
|
|
2199
2481
|
resolveDir: process.cwd()
|
|
2200
2482
|
};
|
|
2201
2483
|
});
|
|
2202
2484
|
}
|
|
2203
2485
|
};
|
|
2486
|
+
const esbuildOutdir = path7.join(projectRoot, "tmp");
|
|
2204
2487
|
const buildOptions = {
|
|
2205
2488
|
entryPoints: activeGroups.map((_, slotIndex) => ({
|
|
2206
2489
|
in: `group-entry-${slotIndex}`,
|
|
@@ -2212,7 +2495,7 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
|
2212
2495
|
logLevel: "silent",
|
|
2213
2496
|
// outdir only labels the paths in outputFiles[].path — nothing is written to disk
|
|
2214
2497
|
// (write:false). Use projectRoot/tmp as a stable sentinel; mkdir is not required.
|
|
2215
|
-
outdir:
|
|
2498
|
+
outdir: esbuildOutdir,
|
|
2216
2499
|
keepNames: true,
|
|
2217
2500
|
legalComments: "none",
|
|
2218
2501
|
target: esbuildTarget(browser),
|
|
@@ -2238,8 +2521,17 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
|
2238
2521
|
const slotIndex = parseInt(match[1]);
|
|
2239
2522
|
const isMap = Boolean(match[2]);
|
|
2240
2523
|
const { config, cachedContent } = activeGroups[slotIndex];
|
|
2241
|
-
const destPath =
|
|
2242
|
-
|
|
2524
|
+
const destPath = path7.join(
|
|
2525
|
+
path7.resolve(config.projectRoot, config.output),
|
|
2526
|
+
"tests.js" + (isMap ? ".map" : "")
|
|
2527
|
+
);
|
|
2528
|
+
if (!isMap) {
|
|
2529
|
+
cachedContent.allTestCode = Buffer.from(outputFile.contents);
|
|
2530
|
+
config._sourceMapDecoder = extractInlineSourceMap(
|
|
2531
|
+
cachedContent.allTestCode,
|
|
2532
|
+
esbuildOutdir
|
|
2533
|
+
);
|
|
2534
|
+
}
|
|
2243
2535
|
return fs9.writeFile(destPath, outputFile.contents);
|
|
2244
2536
|
})
|
|
2245
2537
|
);
|
|
@@ -2249,7 +2541,10 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
|
2249
2541
|
await Promise.all(
|
|
2250
2542
|
activeGroups.map((group) => {
|
|
2251
2543
|
group.cachedContent._buildError = buildError;
|
|
2252
|
-
return fs9.writeFile(
|
|
2544
|
+
return fs9.writeFile(
|
|
2545
|
+
path7.join(path7.resolve(group.config.projectRoot, group.config.output), "index.html"),
|
|
2546
|
+
errorHtml
|
|
2547
|
+
).catch(
|
|
2253
2548
|
(err) => debug && process.stderr.write(`# [qunitx] writeFile index.html: ${err.message}
|
|
2254
2549
|
`)
|
|
2255
2550
|
);
|
|
@@ -2258,7 +2553,7 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
|
2258
2553
|
throw error;
|
|
2259
2554
|
}
|
|
2260
2555
|
}
|
|
2261
|
-
var BundleError, ancestorNodeModules, ANCESTOR_NODE_MODULES, RETRY_DELAY_MS, MAX_RETRIES, EMPTY_BUNDLE_THRESHOLD, GROUP_OUTPUT_REGEX;
|
|
2556
|
+
var BundleError, ancestorNodeModules, ANCESTOR_NODE_MODULES, RETRY_DELAY_MS, MAX_RETRIES, EMPTY_BUNDLE_THRESHOLD, NAV_GRACE_MS, STARTUP_TIMEOUT_FACTOR, TESTS_JS_TIMEOUT_FACTOR, CONSOLE_FLUSH_TIMEOUT_MS, TEST_STALL_BUFFER_MS, GROUP_OUTPUT_REGEX;
|
|
2262
2557
|
var init_tests_in_browser = __esm({
|
|
2263
2558
|
"lib/commands/run/tests-in-browser.ts"() {
|
|
2264
2559
|
init_color();
|
|
@@ -2267,6 +2562,7 @@ var init_tests_in_browser = __esm({
|
|
|
2267
2562
|
init_run_user_module();
|
|
2268
2563
|
init_display_final_result();
|
|
2269
2564
|
init_web_server();
|
|
2565
|
+
init_source_map_decoder();
|
|
2270
2566
|
BundleError = class extends Error {
|
|
2271
2567
|
constructor(message) {
|
|
2272
2568
|
super(message);
|
|
@@ -2274,13 +2570,18 @@ var init_tests_in_browser = __esm({
|
|
|
2274
2570
|
this.message = `esbuild Bundle Error: ${message}`.split("\n").join("\n# ");
|
|
2275
2571
|
}
|
|
2276
2572
|
};
|
|
2277
|
-
ancestorNodeModules = (dir) => dir.split(
|
|
2278
|
-
(_, i, parts) =>
|
|
2573
|
+
ancestorNodeModules = (dir) => dir.split(path7.sep).map(
|
|
2574
|
+
(_, i, parts) => path7.join(parts.slice(0, parts.length - i).join(path7.sep) || path7.sep, "node_modules")
|
|
2279
2575
|
);
|
|
2280
2576
|
ANCESTOR_NODE_MODULES = ancestorNodeModules(process.cwd());
|
|
2281
2577
|
RETRY_DELAY_MS = 100;
|
|
2282
2578
|
MAX_RETRIES = 3;
|
|
2283
2579
|
EMPTY_BUNDLE_THRESHOLD = 500;
|
|
2580
|
+
NAV_GRACE_MS = 1e4;
|
|
2581
|
+
STARTUP_TIMEOUT_FACTOR = 3;
|
|
2582
|
+
TESTS_JS_TIMEOUT_FACTOR = 4;
|
|
2583
|
+
CONSOLE_FLUSH_TIMEOUT_MS = 2e3;
|
|
2584
|
+
TEST_STALL_BUFFER_MS = 5e3;
|
|
2284
2585
|
GROUP_OUTPUT_REGEX = /group-(\d+)\.js(\.map)?$/;
|
|
2285
2586
|
}
|
|
2286
2587
|
});
|
|
@@ -2288,7 +2589,7 @@ var init_tests_in_browser = __esm({
|
|
|
2288
2589
|
// lib/setup/file-watcher.ts
|
|
2289
2590
|
import fs10 from "node:fs";
|
|
2290
2591
|
import { stat, lstat } from "node:fs/promises";
|
|
2291
|
-
import
|
|
2592
|
+
import path8 from "node:path";
|
|
2292
2593
|
function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFunc) {
|
|
2293
2594
|
const extensions = config.extensions || ["js", "ts"];
|
|
2294
2595
|
const readyPromises = [];
|
|
@@ -2297,16 +2598,20 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
2297
2598
|
const symlinkPollers = /* @__PURE__ */ new Map();
|
|
2298
2599
|
function trackSymlink(filePath) {
|
|
2299
2600
|
if (symlinkPollers.has(filePath)) return;
|
|
2300
|
-
const handler = (curr) => {
|
|
2601
|
+
const handler = (curr, prev) => {
|
|
2301
2602
|
if (curr.nlink === 0) {
|
|
2302
2603
|
fs10.unwatchFile(filePath, handler);
|
|
2303
2604
|
symlinkPollers.delete(filePath);
|
|
2304
2605
|
if (filePath in config.fsTree) {
|
|
2305
2606
|
handleWatchEvent(config, extensions, "unlink", filePath, onEventFunc, onFinishFunc);
|
|
2306
2607
|
}
|
|
2608
|
+
} else if ((process.platform === "win32" || process.platform === "darwin") && curr.mtimeMs !== prev.mtimeMs) {
|
|
2609
|
+
if (filePath in config.fsTree) {
|
|
2610
|
+
handleWatchEvent(config, extensions, "change", filePath, onEventFunc, onFinishFunc);
|
|
2611
|
+
}
|
|
2307
2612
|
}
|
|
2308
2613
|
};
|
|
2309
|
-
fs10.watchFile(filePath, { interval:
|
|
2614
|
+
fs10.watchFile(filePath, { interval: SYMLINK_POLL_INTERVAL_MS, persistent: false }, handler);
|
|
2310
2615
|
symlinkPollers.set(filePath, () => fs10.unwatchFile(filePath, handler));
|
|
2311
2616
|
}
|
|
2312
2617
|
function untrackSymlink(filePath) {
|
|
@@ -2315,18 +2620,23 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
2315
2620
|
}
|
|
2316
2621
|
for (const watchPath of testFileLookupPaths) {
|
|
2317
2622
|
let ready = false;
|
|
2318
|
-
const
|
|
2623
|
+
const lastEventMs = {};
|
|
2624
|
+
const seenMtimeMs = {};
|
|
2319
2625
|
const childWatcher = fs10.watch(watchPath, { recursive: true }, async (eventType, filename) => {
|
|
2320
2626
|
if (!ready || !filename) return;
|
|
2321
|
-
const fullPath = filename ===
|
|
2627
|
+
const fullPath = filename === path8.basename(watchPath) ? watchPath : path8.join(watchPath, filename);
|
|
2322
2628
|
if (eventType === "change") {
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2629
|
+
const now = Date.now();
|
|
2630
|
+
const last = lastEventMs[fullPath] ?? 0;
|
|
2631
|
+
lastEventMs[fullPath] = now;
|
|
2632
|
+
try {
|
|
2633
|
+
const { mtimeMs } = await stat(fullPath);
|
|
2634
|
+
const prevMtime = seenMtimeMs[fullPath] ?? 0;
|
|
2635
|
+
seenMtimeMs[fullPath] = mtimeMs;
|
|
2636
|
+
if (now - last < CHANGE_DEDUPE_MS && mtimeMs > 0 && mtimeMs === prevMtime) return;
|
|
2637
|
+
if (config._lastBuildEndMs && mtimeMs < Math.floor(config._lastBuildEndMs / 1e3) * 1e3)
|
|
2638
|
+
return;
|
|
2639
|
+
} catch {
|
|
2330
2640
|
}
|
|
2331
2641
|
return handleWatchEvent(config, extensions, "change", fullPath, onEventFunc, onFinishFunc);
|
|
2332
2642
|
}
|
|
@@ -2343,8 +2653,8 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
2343
2653
|
}
|
|
2344
2654
|
handleWatchEvent(config, extensions, event, fullPath, onEventFunc, onFinishFunc);
|
|
2345
2655
|
});
|
|
2346
|
-
const parentDir =
|
|
2347
|
-
const watchedBasename =
|
|
2656
|
+
const parentDir = path8.dirname(watchPath);
|
|
2657
|
+
const watchedBasename = path8.basename(watchPath);
|
|
2348
2658
|
let parentUnlinkFired = false;
|
|
2349
2659
|
const parentWatcher = fs10.watch(parentDir, async (eventType, filename) => {
|
|
2350
2660
|
if (!ready || filename !== watchedBasename || eventType !== "rename") return;
|
|
@@ -2396,18 +2706,20 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
2396
2706
|
};
|
|
2397
2707
|
}
|
|
2398
2708
|
async function classifyRenameEvent(fullPath, fsTree) {
|
|
2399
|
-
for (const delay of [0,
|
|
2709
|
+
for (const delay of [0, OVERLAYFS_RENAME_RETRY_MS]) {
|
|
2400
2710
|
if (delay) await new Promise((resolve) => setTimeout(resolve, delay));
|
|
2401
2711
|
try {
|
|
2402
2712
|
const statResult = await stat(fullPath);
|
|
2403
|
-
|
|
2713
|
+
if (statResult.isDirectory()) return "addDir";
|
|
2714
|
+
return fsTree && fullPath in fsTree ? "change" : "add";
|
|
2404
2715
|
} catch {
|
|
2405
2716
|
}
|
|
2406
2717
|
}
|
|
2407
2718
|
if (!fsTree) return null;
|
|
2408
2719
|
if (fullPath in fsTree) return "unlink";
|
|
2409
|
-
|
|
2410
|
-
|
|
2720
|
+
return Object.keys(fsTree).some(
|
|
2721
|
+
(trackedPath) => trackedPath.startsWith(fullPath + "/") || trackedPath.startsWith(fullPath + "\\")
|
|
2722
|
+
) ? "unlinkDir" : null;
|
|
2411
2723
|
}
|
|
2412
2724
|
function handleWatchEvent(config, extensions, event, filePath, onEventFunc, onFinishFunc) {
|
|
2413
2725
|
if (event !== "unlinkDir" && !extensions.some((ext) => filePath.endsWith(`.${ext}`)))
|
|
@@ -2447,15 +2759,15 @@ function handleWatchEvent(config, extensions, event, filePath, onEventFunc, onFi
|
|
|
2447
2759
|
}
|
|
2448
2760
|
});
|
|
2449
2761
|
}
|
|
2450
|
-
function mutateFSTree(fsTree, event,
|
|
2762
|
+
function mutateFSTree(fsTree, event, filePath) {
|
|
2451
2763
|
if (event === "add") {
|
|
2452
|
-
fsTree[
|
|
2764
|
+
fsTree[filePath] = null;
|
|
2453
2765
|
} else if (event === "unlink") {
|
|
2454
|
-
delete fsTree[
|
|
2766
|
+
delete fsTree[filePath];
|
|
2455
2767
|
} else if (event === "unlinkDir") {
|
|
2456
|
-
const dirPrefix = path7.endsWith("/") ? path7 : path7 + "/";
|
|
2457
2768
|
for (const treePath of Object.keys(fsTree)) {
|
|
2458
|
-
if (treePath.startsWith(
|
|
2769
|
+
if (treePath.startsWith(filePath + "/") || treePath.startsWith(filePath + "\\"))
|
|
2770
|
+
delete fsTree[treePath];
|
|
2459
2771
|
}
|
|
2460
2772
|
}
|
|
2461
2773
|
}
|
|
@@ -2464,11 +2776,13 @@ function colorEvent(event) {
|
|
|
2464
2776
|
if (event === "add" || event === "addDir") return green("ADDED:");
|
|
2465
2777
|
return red("REMOVED:");
|
|
2466
2778
|
}
|
|
2467
|
-
var CHANGE_DEDUPE_MS;
|
|
2779
|
+
var CHANGE_DEDUPE_MS, SYMLINK_POLL_INTERVAL_MS, OVERLAYFS_RENAME_RETRY_MS;
|
|
2468
2780
|
var init_file_watcher = __esm({
|
|
2469
2781
|
"lib/setup/file-watcher.ts"() {
|
|
2470
2782
|
init_color();
|
|
2471
2783
|
CHANGE_DEDUPE_MS = 10;
|
|
2784
|
+
SYMLINK_POLL_INTERVAL_MS = 500;
|
|
2785
|
+
OVERLAYFS_RENAME_RETRY_MS = 50;
|
|
2472
2786
|
}
|
|
2473
2787
|
});
|
|
2474
2788
|
|
|
@@ -2549,24 +2863,27 @@ var init_keyboard_events = __esm({
|
|
|
2549
2863
|
|
|
2550
2864
|
// lib/setup/write-output-static-files.ts
|
|
2551
2865
|
import fs11 from "node:fs/promises";
|
|
2866
|
+
import path9 from "node:path";
|
|
2552
2867
|
async function writeOutputStaticFiles({ projectRoot, output }, cachedContent) {
|
|
2553
2868
|
const staticHTMLPromises = Object.keys(cachedContent.staticHTMLs).map(async (staticHTMLKey) => {
|
|
2554
|
-
const htmlRelativePath =
|
|
2555
|
-
|
|
2869
|
+
const htmlRelativePath = path9.relative(projectRoot, staticHTMLKey);
|
|
2870
|
+
const outDir = path9.resolve(projectRoot, output);
|
|
2871
|
+
await ensureFolderExists(path9.join(outDir, htmlRelativePath));
|
|
2556
2872
|
await fs11.writeFile(
|
|
2557
|
-
|
|
2873
|
+
path9.join(outDir, htmlRelativePath),
|
|
2558
2874
|
cachedContent.staticHTMLs[staticHTMLKey]
|
|
2559
2875
|
);
|
|
2560
2876
|
});
|
|
2561
2877
|
const assetPromises = Array.from(cachedContent.assets).map(async (assetAbsolutePath) => {
|
|
2562
|
-
const assetRelativePath =
|
|
2563
|
-
|
|
2564
|
-
await
|
|
2878
|
+
const assetRelativePath = path9.relative(projectRoot, assetAbsolutePath);
|
|
2879
|
+
const outDir = path9.resolve(projectRoot, output);
|
|
2880
|
+
await ensureFolderExists(path9.join(outDir, assetRelativePath));
|
|
2881
|
+
await fs11.copyFile(assetAbsolutePath, path9.join(outDir, assetRelativePath));
|
|
2565
2882
|
});
|
|
2566
2883
|
await Promise.all(staticHTMLPromises.concat(assetPromises));
|
|
2567
2884
|
}
|
|
2568
2885
|
async function ensureFolderExists(assetPath) {
|
|
2569
|
-
await fs11.mkdir(
|
|
2886
|
+
await fs11.mkdir(path9.dirname(assetPath), { recursive: true });
|
|
2570
2887
|
}
|
|
2571
2888
|
var init_write_output_static_files = __esm({
|
|
2572
2889
|
"lib/setup/write-output-static-files.ts"() {
|
|
@@ -2585,7 +2902,11 @@ import fs12 from "node:fs/promises";
|
|
|
2585
2902
|
import { normalize } from "node:path";
|
|
2586
2903
|
import { availableParallelism } from "node:os";
|
|
2587
2904
|
async function run(config) {
|
|
2588
|
-
const
|
|
2905
|
+
const browserPromise = config.watch ? null : launchBrowser(config);
|
|
2906
|
+
const [cachedContent, timings] = await Promise.all([
|
|
2907
|
+
buildCachedContent(config, config.htmlPaths),
|
|
2908
|
+
config.watch ? Promise.resolve(null) : readTimingCache(config.projectRoot)
|
|
2909
|
+
]);
|
|
2589
2910
|
if (config.watch) {
|
|
2590
2911
|
const preBuildPromise = buildTestBundle(config, cachedContent);
|
|
2591
2912
|
preBuildPromise.catch(() => {
|
|
@@ -2595,7 +2916,7 @@ async function run(config) {
|
|
|
2595
2916
|
setupBrowser(config, cachedContent),
|
|
2596
2917
|
writeOutputStaticFiles(config, cachedContent)
|
|
2597
2918
|
]);
|
|
2598
|
-
config.
|
|
2919
|
+
config.webServer = connections.server;
|
|
2599
2920
|
setupKeyboardEvents(config, cachedContent, connections);
|
|
2600
2921
|
const isHeadedWatchMode = config.open === true && config.watch;
|
|
2601
2922
|
if (config.open && !isHeadedWatchMode) {
|
|
@@ -2614,7 +2935,10 @@ async function run(config) {
|
|
|
2614
2935
|
throw error;
|
|
2615
2936
|
}
|
|
2616
2937
|
if (isHeadedWatchMode && (cachedContent._buildError || cachedContent._noTestsWarning)) {
|
|
2617
|
-
await connections.page.goto(`http://localhost:${config.port}/`, {
|
|
2938
|
+
await connections.page.goto(`http://localhost:${config.port}/`, {
|
|
2939
|
+
waitUntil: "commit",
|
|
2940
|
+
timeout: WATCH_NAV_TIMEOUT_MS
|
|
2941
|
+
}).catch(() => {
|
|
2618
2942
|
});
|
|
2619
2943
|
}
|
|
2620
2944
|
if (config.watch) {
|
|
@@ -2631,6 +2955,10 @@ async function run(config) {
|
|
|
2631
2955
|
`# Rerun triggered: ${event} \u2192 ${file.replace(`${config.projectRoot}/`, "")}`
|
|
2632
2956
|
);
|
|
2633
2957
|
}
|
|
2958
|
+
const rebuildPromise = buildTestBundle(config, cachedContent);
|
|
2959
|
+
rebuildPromise.catch(() => {
|
|
2960
|
+
});
|
|
2961
|
+
cachedContent._preBuildPromise = rebuildPromise;
|
|
2634
2962
|
return await runTestsInBrowser(config, cachedContent, connections);
|
|
2635
2963
|
}
|
|
2636
2964
|
if (config.debug) {
|
|
@@ -2643,7 +2971,10 @@ async function run(config) {
|
|
|
2643
2971
|
async (_path, _event) => {
|
|
2644
2972
|
connections.server.publish("refresh");
|
|
2645
2973
|
if (isHeadedWatchMode && (cachedContent._buildError || cachedContent._noTestsWarning)) {
|
|
2646
|
-
await connections.page.goto(`http://localhost:${config.port}/`, {
|
|
2974
|
+
await connections.page.goto(`http://localhost:${config.port}/`, {
|
|
2975
|
+
waitUntil: "commit",
|
|
2976
|
+
timeout: WATCH_NAV_TIMEOUT_MS
|
|
2977
|
+
}).catch(() => {
|
|
2647
2978
|
});
|
|
2648
2979
|
}
|
|
2649
2980
|
}
|
|
@@ -2654,8 +2985,7 @@ async function run(config) {
|
|
|
2654
2985
|
} else {
|
|
2655
2986
|
const allFiles = Object.keys(config.fsTree);
|
|
2656
2987
|
const groupCount = Math.min(allFiles.length, availableParallelism());
|
|
2657
|
-
const
|
|
2658
|
-
const { groups, weights } = await splitIntoGroups(allFiles, groupCount, timings);
|
|
2988
|
+
const { groups, weights } = await splitIntoGroups(allFiles, groupCount, timings ?? {});
|
|
2659
2989
|
config.COUNTER = {
|
|
2660
2990
|
testCount: 0,
|
|
2661
2991
|
failCount: 0,
|
|
@@ -2687,7 +3017,7 @@ async function run(config) {
|
|
|
2687
3017
|
`
|
|
2688
3018
|
);
|
|
2689
3019
|
const [browser] = await Promise.all([
|
|
2690
|
-
|
|
3020
|
+
browserPromise,
|
|
2691
3021
|
sharedServer ? bindServerToPort(sharedServer, config).then(
|
|
2692
3022
|
() => groupConfigs.forEach((gc, i) => {
|
|
2693
3023
|
gc.port = config.port;
|
|
@@ -2708,7 +3038,7 @@ async function run(config) {
|
|
|
2708
3038
|
const wallTimes = /* @__PURE__ */ new Map();
|
|
2709
3039
|
const GROUP_TIMEOUT_MS = 3 * 60 * 1e3;
|
|
2710
3040
|
const keepAlive = setInterval(() => {
|
|
2711
|
-
},
|
|
3041
|
+
}, KEEP_ALIVE_INTERVAL_MS);
|
|
2712
3042
|
const groupResults = await Promise.allSettled(
|
|
2713
3043
|
groupConfigs.map((groupConfig, i) => {
|
|
2714
3044
|
const groupTimeout = new Promise((_, reject) => {
|
|
@@ -2734,7 +3064,7 @@ async function run(config) {
|
|
|
2734
3064
|
browser,
|
|
2735
3065
|
sharedServer
|
|
2736
3066
|
);
|
|
2737
|
-
groupConfig.
|
|
3067
|
+
groupConfig.webServer = connections.server;
|
|
2738
3068
|
if (config.before) {
|
|
2739
3069
|
await runUserModule(`${process.cwd()}/${config.before}`, groupConfig, "before");
|
|
2740
3070
|
}
|
|
@@ -2749,7 +3079,7 @@ async function run(config) {
|
|
|
2749
3079
|
Promise.race([
|
|
2750
3080
|
connections.page.close(),
|
|
2751
3081
|
new Promise((resolve) => {
|
|
2752
|
-
const pageCloseTimeoutId = setTimeout(resolve,
|
|
3082
|
+
const pageCloseTimeoutId = setTimeout(resolve, PAGE_CLOSE_GRACE_MS);
|
|
2753
3083
|
pageCloseTimeoutId.unref();
|
|
2754
3084
|
})
|
|
2755
3085
|
]).catch(() => {
|
|
@@ -2783,15 +3113,14 @@ async function run(config) {
|
|
|
2783
3113
|
(err) => config.debug && process.stderr.write(`# [qunitx] persistTimings: ${err.message}
|
|
2784
3114
|
`)
|
|
2785
3115
|
);
|
|
2786
|
-
printFileTimings(fileTimes, config.projectRoot);
|
|
3116
|
+
if (config.debug) printFileTimings(fileTimes, config.projectRoot);
|
|
2787
3117
|
if (config.after) {
|
|
2788
3118
|
await runUserModule(`${process.cwd()}/${config.after}`, config.COUNTER, "after");
|
|
2789
3119
|
}
|
|
2790
|
-
const exitTimer = setTimeout(() => process.exit(exitCode),
|
|
3120
|
+
const exitTimer = setTimeout(() => process.exit(exitCode), STDOUT_FLUSH_GRACE_MS);
|
|
2791
3121
|
exitTimer.unref();
|
|
2792
3122
|
process.stdout.write("\n", async () => {
|
|
2793
3123
|
clearTimeout(exitTimer);
|
|
2794
|
-
clearInterval(keepAlive);
|
|
2795
3124
|
await Promise.all([
|
|
2796
3125
|
sharedServer?.close().catch(
|
|
2797
3126
|
(err) => config.debug && process.stderr.write(`# [qunitx] server.close: ${err.message}
|
|
@@ -2803,6 +3132,7 @@ async function run(config) {
|
|
|
2803
3132
|
)
|
|
2804
3133
|
]);
|
|
2805
3134
|
await shutdownPrelaunch();
|
|
3135
|
+
clearInterval(keepAlive);
|
|
2806
3136
|
process.exit(exitCode);
|
|
2807
3137
|
});
|
|
2808
3138
|
}
|
|
@@ -2931,6 +3261,7 @@ function normalizeInternalAssetPathFromHTML(projectRoot, assetPath, htmlPath) {
|
|
|
2931
3261
|
const currentDirectory = htmlPath ? htmlPath.split("/").slice(0, -1).join("/") : projectRoot;
|
|
2932
3262
|
return assetPath.startsWith("./") ? normalize(`${currentDirectory}/${assetPath.slice(2)}`) : normalize(`${currentDirectory}/${assetPath}`);
|
|
2933
3263
|
}
|
|
3264
|
+
var WATCH_NAV_TIMEOUT_MS, PAGE_CLOSE_GRACE_MS, STDOUT_FLUSH_GRACE_MS, KEEP_ALIVE_INTERVAL_MS;
|
|
2934
3265
|
var init_run = __esm({
|
|
2935
3266
|
"lib/commands/run.ts"() {
|
|
2936
3267
|
init_browser();
|
|
@@ -2950,6 +3281,10 @@ var init_run = __esm({
|
|
|
2950
3281
|
init_display_final_result();
|
|
2951
3282
|
init_read_template();
|
|
2952
3283
|
init_html();
|
|
3284
|
+
WATCH_NAV_TIMEOUT_MS = 5e3;
|
|
3285
|
+
PAGE_CLOSE_GRACE_MS = 1e4;
|
|
3286
|
+
STDOUT_FLUSH_GRACE_MS = 5e3;
|
|
3287
|
+
KEEP_ALIVE_INTERVAL_MS = 1e4;
|
|
2953
3288
|
}
|
|
2954
3289
|
});
|
|
2955
3290
|
|
|
@@ -2964,7 +3299,7 @@ init_color();
|
|
|
2964
3299
|
var package_default = {
|
|
2965
3300
|
name: "qunitx-cli",
|
|
2966
3301
|
type: "module",
|
|
2967
|
-
version: "0.
|
|
3302
|
+
version: "0.21.0",
|
|
2968
3303
|
description: "Browser runner for QUnitx: run your qunitx tests in google-chrome",
|
|
2969
3304
|
author: "Izel Nakri",
|
|
2970
3305
|
license: "MIT",
|
|
@@ -3019,12 +3354,10 @@ var package_default = {
|
|
|
3019
3354
|
ws: "^8.20.0"
|
|
3020
3355
|
},
|
|
3021
3356
|
devDependencies: {
|
|
3022
|
-
cors: "^2.8.6",
|
|
3023
|
-
express: "^5.2.1",
|
|
3024
3357
|
"js-yaml": "^4.1.1",
|
|
3025
|
-
prettier: "^3.8.
|
|
3026
|
-
qunitx: "^1.2.
|
|
3027
|
-
typescript: "^6.0.
|
|
3358
|
+
prettier: "^3.8.3",
|
|
3359
|
+
qunitx: "^1.2.8",
|
|
3360
|
+
typescript: "^6.0.3"
|
|
3028
3361
|
},
|
|
3029
3362
|
volta: {
|
|
3030
3363
|
node: "24.14.0"
|
|
@@ -3082,9 +3415,9 @@ import process2 from "node:process";
|
|
|
3082
3415
|
|
|
3083
3416
|
// lib/utils/path-exists.ts
|
|
3084
3417
|
import fs2 from "node:fs/promises";
|
|
3085
|
-
async function pathExists(
|
|
3418
|
+
async function pathExists(path10) {
|
|
3086
3419
|
try {
|
|
3087
|
-
await fs2.access(
|
|
3420
|
+
await fs2.access(path10);
|
|
3088
3421
|
return true;
|
|
3089
3422
|
} catch {
|
|
3090
3423
|
return false;
|
|
@@ -3156,7 +3489,7 @@ async function writeTestsHTML(projectRoot, config, oldPackageJSON) {
|
|
|
3156
3489
|
const targetDirectory = path2.dirname(targetPath);
|
|
3157
3490
|
const _targetOutputPath = path2.relative(
|
|
3158
3491
|
targetDirectory,
|
|
3159
|
-
|
|
3492
|
+
path2.join(path2.resolve(projectRoot, config.output), "tests.js")
|
|
3160
3493
|
);
|
|
3161
3494
|
const testHTMLTemplate = testHTMLTemplateBuffer.replace(
|
|
3162
3495
|
"{{applicationName}}",
|
|
@@ -3202,17 +3535,17 @@ function pathToModuleName(filePath) {
|
|
|
3202
3535
|
async function generateTestFiles() {
|
|
3203
3536
|
const projectRoot = await findProjectRoot();
|
|
3204
3537
|
const moduleName = pathToModuleName(process.argv[3]);
|
|
3205
|
-
const
|
|
3206
|
-
if (await pathExists(
|
|
3207
|
-
console.log(`${
|
|
3538
|
+
const path10 = process.argv[3].endsWith(".js") || process.argv[3].endsWith(".ts") ? `${projectRoot}/${process.argv[3]}` : `${projectRoot}/${process.argv[3]}.js`;
|
|
3539
|
+
if (await pathExists(path10)) {
|
|
3540
|
+
console.log(`${path10} already exists!`);
|
|
3208
3541
|
return;
|
|
3209
3542
|
}
|
|
3210
3543
|
const testJSContent = await readTemplate("test.js");
|
|
3211
|
-
const targetFolderPaths =
|
|
3544
|
+
const targetFolderPaths = path10.split("/");
|
|
3212
3545
|
targetFolderPaths.pop();
|
|
3213
3546
|
await fs5.mkdir(targetFolderPaths.join("/"), { recursive: true });
|
|
3214
|
-
await fs5.writeFile(
|
|
3215
|
-
console.log(green(`${
|
|
3547
|
+
await fs5.writeFile(path10, testJSContent.replace("{{moduleName}}", moduleName));
|
|
3548
|
+
console.log(green(`${path10} written`));
|
|
3216
3549
|
}
|
|
3217
3550
|
|
|
3218
3551
|
// lib/setup/config.ts
|
|
@@ -3313,23 +3646,25 @@ function setupTestFilePaths(_projectRoot, inputs2) {
|
|
|
3313
3646
|
});
|
|
3314
3647
|
return result.map((metaItem) => metaItem.input);
|
|
3315
3648
|
}
|
|
3316
|
-
function pathIsFile(
|
|
3317
|
-
const inputs2 =
|
|
3649
|
+
function pathIsFile(path10) {
|
|
3650
|
+
const inputs2 = path10.split("/");
|
|
3318
3651
|
return inputs2[inputs2.length - 1].includes(".");
|
|
3319
3652
|
}
|
|
3320
3653
|
function pathIsIncludedInPaths(paths, targetPath) {
|
|
3321
|
-
return paths.some((
|
|
3322
|
-
if (
|
|
3654
|
+
return paths.some((path10) => {
|
|
3655
|
+
if (path10 === targetPath) {
|
|
3323
3656
|
return false;
|
|
3324
3657
|
}
|
|
3325
|
-
return matchesGlob(targetPath.input, buildGlobFormat(
|
|
3658
|
+
return matchesGlob(targetPath.input, buildGlobFormat(path10));
|
|
3326
3659
|
});
|
|
3327
3660
|
}
|
|
3328
|
-
function buildGlobFormat(
|
|
3329
|
-
return
|
|
3661
|
+
function buildGlobFormat(path10) {
|
|
3662
|
+
return path10.isFile ? path10.input : `${path10.input}/**`;
|
|
3330
3663
|
}
|
|
3331
3664
|
|
|
3332
3665
|
// lib/utils/parse-cli-flags.ts
|
|
3666
|
+
import path4 from "node:path";
|
|
3667
|
+
var FALLBACK_TIMEOUT_MS = 1e4;
|
|
3333
3668
|
function parseCliFlags(projectRoot) {
|
|
3334
3669
|
const providedFlags = process.argv.slice(2).reduce(
|
|
3335
3670
|
(result, arg) => {
|
|
@@ -3344,7 +3679,7 @@ function parseCliFlags(projectRoot) {
|
|
|
3344
3679
|
} else if (arg.startsWith("--failfast") || arg.startsWith("--failFast")) {
|
|
3345
3680
|
return Object.assign(result, { failFast: parseBoolean(arg.split("=")[1]) });
|
|
3346
3681
|
} else if (arg.startsWith("--timeout")) {
|
|
3347
|
-
return Object.assign(result, { timeout: Number(arg.split("=")[1]) ||
|
|
3682
|
+
return Object.assign(result, { timeout: Number(arg.split("=")[1]) || FALLBACK_TIMEOUT_MS });
|
|
3348
3683
|
} else if (arg.startsWith("--output")) {
|
|
3349
3684
|
return Object.assign(result, { output: arg.split("=")[1] });
|
|
3350
3685
|
} else if (arg.endsWith(".html")) {
|
|
@@ -3381,12 +3716,22 @@ function parseCliFlags(projectRoot) {
|
|
|
3381
3716
|
return result;
|
|
3382
3717
|
}
|
|
3383
3718
|
result.inputs.add(
|
|
3384
|
-
arg.startsWith(projectRoot) || arg.startsWith("/") ? arg :
|
|
3719
|
+
arg.startsWith(projectRoot) || arg.startsWith("/") ? arg : path4.join(process.cwd(), arg)
|
|
3385
3720
|
);
|
|
3386
3721
|
return result;
|
|
3387
3722
|
},
|
|
3388
3723
|
{ inputs: /* @__PURE__ */ new Set([]) }
|
|
3389
3724
|
);
|
|
3725
|
+
if (!providedFlags.browser && process.env.QUNITX_BROWSER) {
|
|
3726
|
+
const envBrowser = process.env.QUNITX_BROWSER;
|
|
3727
|
+
if (!["chromium", "firefox", "webkit"].includes(envBrowser)) {
|
|
3728
|
+
console.error(
|
|
3729
|
+
`Invalid QUNITX_BROWSER value: "${envBrowser}". Must be one of: chromium, firefox, webkit`
|
|
3730
|
+
);
|
|
3731
|
+
process.exit(1);
|
|
3732
|
+
}
|
|
3733
|
+
providedFlags.browser = envBrowser;
|
|
3734
|
+
}
|
|
3390
3735
|
return { ...providedFlags, inputs: Array.from(providedFlags.inputs) };
|
|
3391
3736
|
}
|
|
3392
3737
|
function parseBoolean(result, defaultValue = true) {
|