qunitx-cli 0.19.2 → 0.20.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 +774 -259
- package/package.json +1 -3
package/dist/cli.js
CHANGED
|
@@ -55,71 +55,78 @@ var init_kill_process_group = __esm({
|
|
|
55
55
|
|
|
56
56
|
// lib/utils/cleanup-browser-dir.ts
|
|
57
57
|
import fs from "node:fs/promises";
|
|
58
|
-
async function
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
async function processReferencesDir(entry, dirPath, dirName) {
|
|
59
|
+
try {
|
|
60
|
+
const [cwd, cmdline] = await Promise.all([
|
|
61
|
+
fs.readlink(`/proc/${entry}/cwd`).catch(() => ""),
|
|
62
|
+
fs.readFile(`/proc/${entry}/cmdline`, "utf8").catch(() => "")
|
|
63
|
+
]);
|
|
64
|
+
if (cwd.startsWith(dirPath) || cmdline.includes(dirName)) return true;
|
|
65
|
+
const fds = await fs.readdir(`/proc/${entry}/fd`).catch(() => []);
|
|
66
|
+
const fdTargets = await Promise.all(
|
|
67
|
+
fds.map((fd) => fs.readlink(`/proc/${entry}/fd/${fd}`).catch(() => ""))
|
|
68
|
+
);
|
|
69
|
+
return fdTargets.some((target) => target.startsWith(dirPath));
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
63
72
|
}
|
|
64
|
-
|
|
65
|
-
|
|
73
|
+
}
|
|
74
|
+
async function killAllReferencingProcesses(dirPath, dirName) {
|
|
66
75
|
const procEntries = await fs.readdir("/proc").catch(() => []);
|
|
67
76
|
await Promise.all(
|
|
68
77
|
procEntries.map(async (entry) => {
|
|
69
78
|
if (!/^\d+$/.test(entry)) return;
|
|
70
|
-
|
|
79
|
+
if (!await processReferencesDir(entry, dirPath, dirName)) return;
|
|
71
80
|
try {
|
|
72
|
-
|
|
73
|
-
fs.readlink(`/proc/${entry}/cwd`).catch(() => ""),
|
|
74
|
-
fs.readFile(`/proc/${entry}/cmdline`, "utf8").catch(() => "")
|
|
75
|
-
]);
|
|
76
|
-
if (!cwd.startsWith(dirPath) && !cmdline.includes(dirName)) return;
|
|
77
|
-
try {
|
|
78
|
-
process.kill(pid, "SIGKILL");
|
|
79
|
-
killedPids.add(pid);
|
|
80
|
-
} catch {
|
|
81
|
-
}
|
|
81
|
+
process.kill(parseInt(entry), "SIGKILL");
|
|
82
82
|
} catch {
|
|
83
83
|
}
|
|
84
84
|
})
|
|
85
85
|
);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
killedPids.delete(pid);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
86
|
+
}
|
|
87
|
+
async function cleanupBrowserDir(dirPath) {
|
|
88
|
+
if (process.platform !== "linux") {
|
|
89
|
+
await fs.rm(dirPath, { recursive: true, force: true }).catch(() => {
|
|
90
|
+
});
|
|
91
|
+
return;
|
|
95
92
|
}
|
|
96
|
-
const
|
|
93
|
+
const dirName = dirPath.split("/").pop();
|
|
94
|
+
await killAllReferencingProcesses(dirPath, dirName);
|
|
95
|
+
const deadline = Date.now() + CLEANUP_DEADLINE_MS;
|
|
97
96
|
while (Date.now() < deadline) {
|
|
98
|
-
const removed = await fs.rm(dirPath, { recursive: true, force: true }).then(
|
|
99
|
-
|
|
100
|
-
|
|
97
|
+
const removed = await fs.rm(dirPath, { recursive: true, force: true }).then(
|
|
98
|
+
() => fs.access(dirPath).then(
|
|
99
|
+
() => false,
|
|
100
|
+
() => true
|
|
101
|
+
)
|
|
102
|
+
).catch(() => false);
|
|
103
|
+
if (removed) return;
|
|
104
|
+
await killAllReferencingProcesses(dirPath, dirName);
|
|
105
|
+
await new Promise((resolve) => setTimeout(resolve, CLEANUP_RETRY_MS));
|
|
101
106
|
}
|
|
102
|
-
if (await fs.access(dirPath).then(() => true).catch(() => false))
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
107
|
+
if (!await fs.access(dirPath).then(() => true).catch(() => false))
|
|
108
|
+
return;
|
|
109
|
+
const diagEntries = await fs.readdir("/proc").catch(() => []);
|
|
110
|
+
await Promise.all(
|
|
111
|
+
diagEntries.map(async (entry) => {
|
|
112
|
+
if (!/^\d+$/.test(entry)) return;
|
|
113
|
+
try {
|
|
114
|
+
if (!await processReferencesDir(entry, dirPath, dirName)) return;
|
|
115
|
+
const cmdline = await fs.readFile(`/proc/${entry}/cmdline`, "utf8").catch(() => "");
|
|
116
|
+
process.stderr.write(
|
|
117
|
+
`# [qunitx] cleanup failed: pid ${entry} still references ${dirPath} (cmdline: ${cmdline.replace(/\0/g, " ").slice(0, 120)})
|
|
113
118
|
`
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
119
|
+
);
|
|
120
|
+
} catch {
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
);
|
|
120
124
|
}
|
|
125
|
+
var CLEANUP_DEADLINE_MS, CLEANUP_RETRY_MS;
|
|
121
126
|
var init_cleanup_browser_dir = __esm({
|
|
122
127
|
"lib/utils/cleanup-browser-dir.ts"() {
|
|
128
|
+
CLEANUP_DEADLINE_MS = 5e3;
|
|
129
|
+
CLEANUP_RETRY_MS = 50;
|
|
123
130
|
}
|
|
124
131
|
});
|
|
125
132
|
|
|
@@ -168,36 +175,11 @@ async function preLaunchChrome(chromePath, args, headless = true) {
|
|
|
168
175
|
});
|
|
169
176
|
async function shutdown() {
|
|
170
177
|
proc.ref();
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
proc.once("close", resolve);
|
|
177
|
-
});
|
|
178
|
-
if (proc.exitCode === null) killProcessGroup(proc.pid);
|
|
179
|
-
await closed;
|
|
180
|
-
await rm(userDataDir, { recursive: true, force: true }).catch(async () => {
|
|
181
|
-
const pgid = proc.pid;
|
|
182
|
-
const warnTimer = setTimeout(
|
|
183
|
-
() => process.stderr.write(
|
|
184
|
-
`# [qunitx] warning: Chrome process group ${pgid} still alive 500ms after SIGKILL, waiting...
|
|
185
|
-
`
|
|
186
|
-
),
|
|
187
|
-
500
|
|
188
|
-
);
|
|
189
|
-
warnTimer.unref();
|
|
190
|
-
while (true) {
|
|
191
|
-
try {
|
|
192
|
-
process.kill(-pgid, 0);
|
|
193
|
-
} catch {
|
|
194
|
-
break;
|
|
195
|
-
}
|
|
196
|
-
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
197
|
-
}
|
|
198
|
-
clearTimeout(warnTimer);
|
|
199
|
-
await cleanupBrowserDir(userDataDir);
|
|
200
|
-
});
|
|
178
|
+
if (proc.exitCode === null) {
|
|
179
|
+
killProcessGroup(proc.pid);
|
|
180
|
+
await new Promise((resolve) => proc.once("close", resolve));
|
|
181
|
+
}
|
|
182
|
+
await cleanupBrowserDir(userDataDir);
|
|
201
183
|
}
|
|
202
184
|
}
|
|
203
185
|
var CDP_URL_REGEX;
|
|
@@ -498,10 +480,11 @@ function dumpYaml({
|
|
|
498
480
|
expected,
|
|
499
481
|
message,
|
|
500
482
|
stack,
|
|
483
|
+
source,
|
|
501
484
|
at
|
|
502
485
|
}) {
|
|
503
486
|
return `name: ${dumpString(name, "")}
|
|
504
|
-
` + yamlLine("actual", actual) + yamlLine("expected", expected) + (message !== null ? yamlLine("message", message) : "") + (stack !== null ? yamlLine("stack", stack) : "") + (at !== null ? yamlLine("at", at) : "");
|
|
487
|
+
` + 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) : "");
|
|
505
488
|
}
|
|
506
489
|
var NEEDS_QUOTING;
|
|
507
490
|
var init_dump_yaml = __esm({
|
|
@@ -524,8 +507,205 @@ var init_indent_string = __esm({
|
|
|
524
507
|
}
|
|
525
508
|
});
|
|
526
509
|
|
|
510
|
+
// lib/utils/source-map-decoder.ts
|
|
511
|
+
function readVLQ(s, pos) {
|
|
512
|
+
let accumulated = 0;
|
|
513
|
+
let shift = 0;
|
|
514
|
+
let digit;
|
|
515
|
+
do {
|
|
516
|
+
digit = BASE64_LOOKUP[s.charCodeAt(pos++)];
|
|
517
|
+
accumulated |= (digit & 31) << shift;
|
|
518
|
+
shift += 5;
|
|
519
|
+
} while (digit & 32);
|
|
520
|
+
return [accumulated & 1 ? -(accumulated >>> 1) : accumulated >>> 1, pos];
|
|
521
|
+
}
|
|
522
|
+
function decodeMappings(mappings) {
|
|
523
|
+
let sourceIndex = 0, sourceLine = 0, sourceCol = 0;
|
|
524
|
+
return mappings.split(";").map((lineStr) => {
|
|
525
|
+
const segments = [];
|
|
526
|
+
let generatedCol = 0;
|
|
527
|
+
let pos = 0;
|
|
528
|
+
while (pos < lineStr.length) {
|
|
529
|
+
if (lineStr[pos] === ",") {
|
|
530
|
+
pos++;
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
let delta;
|
|
534
|
+
[delta, pos] = readVLQ(lineStr, pos);
|
|
535
|
+
generatedCol += delta;
|
|
536
|
+
if (pos >= lineStr.length || lineStr[pos] === ",") continue;
|
|
537
|
+
[delta, pos] = readVLQ(lineStr, pos);
|
|
538
|
+
sourceIndex += delta;
|
|
539
|
+
[delta, pos] = readVLQ(lineStr, pos);
|
|
540
|
+
sourceLine += delta;
|
|
541
|
+
[delta, pos] = readVLQ(lineStr, pos);
|
|
542
|
+
sourceCol += delta;
|
|
543
|
+
if (pos < lineStr.length && lineStr[pos] !== ",") [, pos] = readVLQ(lineStr, pos);
|
|
544
|
+
segments.push({ generatedCol, sourceIndex, sourceLine, sourceCol });
|
|
545
|
+
}
|
|
546
|
+
return segments;
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
function parseSourceMap(json, outDir) {
|
|
550
|
+
const map = JSON.parse(json);
|
|
551
|
+
return {
|
|
552
|
+
segmentsByLine: decodeMappings(map.mappings),
|
|
553
|
+
sources: map.sources ?? [],
|
|
554
|
+
sourceRoot: map.sourceRoot ?? "",
|
|
555
|
+
outDir,
|
|
556
|
+
sourcesContent: map.sourcesContent ?? []
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
function base64DecodeUtf8(b64) {
|
|
560
|
+
const binary = atob(b64);
|
|
561
|
+
return UTF8.decode(Uint8Array.from(binary, (c) => c.charCodeAt(0)));
|
|
562
|
+
}
|
|
563
|
+
function extractInlineSourceMap(bundle, outDir) {
|
|
564
|
+
if (!bundle) return null;
|
|
565
|
+
const text = typeof bundle === "string" ? bundle : UTF8.decode(bundle);
|
|
566
|
+
const match = text.match(
|
|
567
|
+
/\/\/# sourceMappingURL=data:application\/json;base64,([A-Za-z0-9+/=]+)/
|
|
568
|
+
);
|
|
569
|
+
if (!match) return null;
|
|
570
|
+
try {
|
|
571
|
+
return parseSourceMap(base64DecodeUtf8(match[1]), outDir);
|
|
572
|
+
} catch {
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
function normalizePosix(p) {
|
|
577
|
+
const abs = p.startsWith("/");
|
|
578
|
+
const parts = p.split("/");
|
|
579
|
+
const out = [];
|
|
580
|
+
for (const part of parts) {
|
|
581
|
+
if (part === "..") out.pop();
|
|
582
|
+
else if (part !== "" && part !== ".") out.push(part);
|
|
583
|
+
}
|
|
584
|
+
return (abs ? "/" : "") + out.join("/");
|
|
585
|
+
}
|
|
586
|
+
function posixResolve(base, relative) {
|
|
587
|
+
if (relative.startsWith("/")) return normalizePosix(relative);
|
|
588
|
+
return normalizePosix(base + "/" + relative);
|
|
589
|
+
}
|
|
590
|
+
function toAbsolutePath(raw, outDir, sourceRoot) {
|
|
591
|
+
if (raw.startsWith("file://")) return raw.slice(7);
|
|
592
|
+
if (raw.startsWith("/")) return raw;
|
|
593
|
+
const base = sourceRoot ? normalizePosix(outDir + "/" + sourceRoot) : outDir;
|
|
594
|
+
return posixResolve(base, raw);
|
|
595
|
+
}
|
|
596
|
+
function lookupPosition(decoder, generatedLine, generatedCol) {
|
|
597
|
+
const segments = decoder.segmentsByLine[generatedLine - 1];
|
|
598
|
+
if (!segments?.length) return null;
|
|
599
|
+
const col0 = generatedCol - 1;
|
|
600
|
+
let lo = 0, hi = segments.length - 1, best = -1;
|
|
601
|
+
while (lo <= hi) {
|
|
602
|
+
const mid = lo + hi >>> 1;
|
|
603
|
+
if (segments[mid].generatedCol <= col0) {
|
|
604
|
+
best = mid;
|
|
605
|
+
lo = mid + 1;
|
|
606
|
+
} else {
|
|
607
|
+
hi = mid - 1;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
if (best === -1) return null;
|
|
611
|
+
const { sourceIndex, sourceLine, sourceCol } = segments[best];
|
|
612
|
+
const rawSource = decoder.sources[sourceIndex];
|
|
613
|
+
if (!rawSource) return null;
|
|
614
|
+
const content = decoder.sourcesContent[sourceIndex];
|
|
615
|
+
const sourceText = content ? content.split("\n", sourceLine + 1)[sourceLine]?.trim() || null : null;
|
|
616
|
+
return {
|
|
617
|
+
absolutePath: toAbsolutePath(rawSource, decoder.outDir, decoder.sourceRoot),
|
|
618
|
+
line: sourceLine + 1,
|
|
619
|
+
// 0-based → 1-based
|
|
620
|
+
col: sourceCol + 1,
|
|
621
|
+
// 0-based → 1-based
|
|
622
|
+
sourceText
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
function parseFrameLocation(s) {
|
|
626
|
+
const colSep = s.lastIndexOf(":");
|
|
627
|
+
if (colSep < 0) return null;
|
|
628
|
+
const colStr = s.slice(colSep + 1);
|
|
629
|
+
if (!/^\d+$/.test(colStr)) return null;
|
|
630
|
+
const lineSep = s.lastIndexOf(":", colSep - 1);
|
|
631
|
+
if (lineSep < 0) return null;
|
|
632
|
+
const lineStr = s.slice(lineSep + 1, colSep);
|
|
633
|
+
if (!/^\d+$/.test(lineStr)) return null;
|
|
634
|
+
return { url: s.slice(0, lineSep), line: +lineStr, col: +colStr };
|
|
635
|
+
}
|
|
636
|
+
function isBundleUrl(url) {
|
|
637
|
+
const normalized = url.startsWith("async ") ? url.slice(6) : url;
|
|
638
|
+
return /^https?:\/\//.test(normalized) && /\/(tests|filtered-tests)\.js$/.test(normalized);
|
|
639
|
+
}
|
|
640
|
+
function isNodeModulesPath(absolutePath) {
|
|
641
|
+
return absolutePath.includes("/node_modules/") || absolutePath.includes("\\node_modules\\");
|
|
642
|
+
}
|
|
643
|
+
function makeDisplayPath(absolutePath, projectRoot) {
|
|
644
|
+
const prefix = projectRoot + "/";
|
|
645
|
+
return absolutePath.startsWith(prefix) ? absolutePath.slice(prefix.length) : absolutePath;
|
|
646
|
+
}
|
|
647
|
+
function tryResolve(urlLineCol, decoder, projectRoot) {
|
|
648
|
+
const loc = parseFrameLocation(urlLineCol);
|
|
649
|
+
if (!loc || !isBundleUrl(loc.url)) return null;
|
|
650
|
+
const orig = lookupPosition(decoder, loc.line, loc.col);
|
|
651
|
+
if (!orig) return null;
|
|
652
|
+
const display = `${makeDisplayPath(orig.absolutePath, projectRoot)}:${orig.line}:${orig.col}`;
|
|
653
|
+
return {
|
|
654
|
+
display,
|
|
655
|
+
userPath: isNodeModulesPath(orig.absolutePath) ? null : display,
|
|
656
|
+
sourceText: orig.sourceText
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
function resolveFrame(frame, decoder, projectRoot) {
|
|
660
|
+
const chromeName = frame.match(/^(\s*at\s+)(.*?)\s+\(([^)]+)\)\s*$/);
|
|
661
|
+
if (chromeName) {
|
|
662
|
+
const r = tryResolve(chromeName[3], decoder, projectRoot);
|
|
663
|
+
return r ? {
|
|
664
|
+
resolved: `${chromeName[1]}${chromeName[2]} (${r.display})`,
|
|
665
|
+
userPath: r.userPath,
|
|
666
|
+
sourceText: r.sourceText
|
|
667
|
+
} : null;
|
|
668
|
+
}
|
|
669
|
+
const chromeAnon = frame.match(/^(\s*at\s+(?:async\s+)?)(.+)/);
|
|
670
|
+
if (chromeAnon) {
|
|
671
|
+
const r = tryResolve(chromeAnon[2], decoder, projectRoot);
|
|
672
|
+
return r ? { resolved: `${chromeAnon[1]}${r.display}`, userPath: r.userPath, sourceText: r.sourceText } : null;
|
|
673
|
+
}
|
|
674
|
+
const gecko = frame.match(/^([^@]*)@(.+)$/);
|
|
675
|
+
if (gecko) {
|
|
676
|
+
const r = tryResolve(gecko[2], decoder, projectRoot);
|
|
677
|
+
return r ? { resolved: `${gecko[1]}@${r.display}`, userPath: r.userPath, sourceText: r.sourceText } : null;
|
|
678
|
+
}
|
|
679
|
+
return null;
|
|
680
|
+
}
|
|
681
|
+
function resolveStack(stack, decoder, projectRoot) {
|
|
682
|
+
let firstUserFrame = null;
|
|
683
|
+
let firstUserSourceText = null;
|
|
684
|
+
const resolvedLines = stack.split("\n").map((frame) => {
|
|
685
|
+
const result = resolveFrame(frame, decoder, projectRoot);
|
|
686
|
+
if (!result) return frame;
|
|
687
|
+
if (!firstUserFrame && result.userPath) {
|
|
688
|
+
firstUserFrame = result.userPath;
|
|
689
|
+
firstUserSourceText = result.sourceText;
|
|
690
|
+
}
|
|
691
|
+
return result.resolved;
|
|
692
|
+
});
|
|
693
|
+
return { resolvedStack: resolvedLines.join("\n"), firstUserFrame, firstUserSourceText };
|
|
694
|
+
}
|
|
695
|
+
var BASE64, BASE64_LOOKUP, UTF8;
|
|
696
|
+
var init_source_map_decoder = __esm({
|
|
697
|
+
"lib/utils/source-map-decoder.ts"() {
|
|
698
|
+
BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
699
|
+
BASE64_LOOKUP = new Uint8Array(128);
|
|
700
|
+
[...BASE64].forEach((ch, i) => {
|
|
701
|
+
BASE64_LOOKUP[ch.charCodeAt(0)] = i;
|
|
702
|
+
});
|
|
703
|
+
UTF8 = new TextDecoder();
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
|
|
527
707
|
// lib/tap/display-test-result.ts
|
|
528
|
-
function TAPDisplayTestResult(COUNTER, details) {
|
|
708
|
+
function TAPDisplayTestResult(COUNTER, details, decoder, projectRoot) {
|
|
529
709
|
COUNTER.testCount++;
|
|
530
710
|
if (details.status === "skipped") {
|
|
531
711
|
COUNTER.skipCount++;
|
|
@@ -545,6 +725,19 @@ function TAPDisplayTestResult(COUNTER, details) {
|
|
|
545
725
|
if (!assertion.passed && assertion.todo === false) {
|
|
546
726
|
COUNTER.errorCount = (COUNTER.errorCount ?? 0) + 1;
|
|
547
727
|
process.stdout.write(" ---\n");
|
|
728
|
+
let stackStr = assertion.stack?.trim() || null;
|
|
729
|
+
let atStr = extractStackAt(assertion.stack);
|
|
730
|
+
let sourceText = null;
|
|
731
|
+
if (decoder && projectRoot && assertion.stack) {
|
|
732
|
+
const { resolvedStack, firstUserFrame, firstUserSourceText } = resolveStack(
|
|
733
|
+
assertion.stack,
|
|
734
|
+
decoder,
|
|
735
|
+
projectRoot
|
|
736
|
+
);
|
|
737
|
+
stackStr = resolvedStack.trim() || null;
|
|
738
|
+
atStr = firstUserFrame;
|
|
739
|
+
sourceText = firstUserSourceText;
|
|
740
|
+
}
|
|
548
741
|
process.stdout.write(
|
|
549
742
|
indentString(
|
|
550
743
|
dumpYaml({
|
|
@@ -552,10 +745,9 @@ function TAPDisplayTestResult(COUNTER, details) {
|
|
|
552
745
|
actual: assertion.actual !== null && typeof assertion.actual === "object" ? JSON.parse(JSON.stringify(assertion.actual, getCircularReplacer())) : assertion.actual,
|
|
553
746
|
expected: assertion.expected !== null && typeof assertion.expected === "object" ? JSON.parse(JSON.stringify(assertion.expected, getCircularReplacer())) : assertion.expected,
|
|
554
747
|
message: assertion.message || null,
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
at: extractStackAt(assertion.stack)
|
|
748
|
+
stack: stackStr,
|
|
749
|
+
source: sourceText,
|
|
750
|
+
at: atStr
|
|
559
751
|
}),
|
|
560
752
|
4
|
|
561
753
|
)
|
|
@@ -599,6 +791,7 @@ var init_display_test_result = __esm({
|
|
|
599
791
|
"lib/tap/display-test-result.ts"() {
|
|
600
792
|
init_dump_yaml();
|
|
601
793
|
init_indent_string();
|
|
794
|
+
init_source_map_decoder();
|
|
602
795
|
}
|
|
603
796
|
});
|
|
604
797
|
|
|
@@ -780,11 +973,13 @@ var init_http = __esm({
|
|
|
780
973
|
if (!this.routes[method]) {
|
|
781
974
|
this.routes[method] = {};
|
|
782
975
|
}
|
|
976
|
+
const paramNames = this.#extractParamNames(path7);
|
|
783
977
|
this.routes[method][path7] = {
|
|
784
978
|
path: path7,
|
|
785
979
|
handler,
|
|
786
|
-
paramNames
|
|
787
|
-
isWildcard: path7 === "/*"
|
|
980
|
+
paramNames,
|
|
981
|
+
isWildcard: path7 === "/*",
|
|
982
|
+
compiledRegex: paramNames.length > 0 ? new RegExp(`^${this.#buildRegexPattern(path7, paramNames)}$`) : null
|
|
788
983
|
};
|
|
789
984
|
}
|
|
790
985
|
#handleRequest(req, res) {
|
|
@@ -827,10 +1022,8 @@ var init_http = __esm({
|
|
|
827
1022
|
return false;
|
|
828
1023
|
}
|
|
829
1024
|
if (isWildcard || this.#matchPathSegments(path7, url)) {
|
|
830
|
-
if (route.
|
|
831
|
-
const
|
|
832
|
-
const regex = new RegExp(`^${regexPattern}$`);
|
|
833
|
-
const regexMatches = regex.exec(url);
|
|
1025
|
+
if (route.compiledRegex) {
|
|
1026
|
+
const regexMatches = route.compiledRegex.exec(url);
|
|
834
1027
|
if (regexMatches) {
|
|
835
1028
|
route.paramValues = regexMatches.slice(1);
|
|
836
1029
|
}
|
|
@@ -838,7 +1031,7 @@ var init_http = __esm({
|
|
|
838
1031
|
return true;
|
|
839
1032
|
}
|
|
840
1033
|
return false;
|
|
841
|
-
}) ||
|
|
1034
|
+
}) || null;
|
|
842
1035
|
}
|
|
843
1036
|
#matchPathSegments(path7, url) {
|
|
844
1037
|
const pathSegments = path7.split("/");
|
|
@@ -892,6 +1085,20 @@ function setupWebServer(config, cachedContent) {
|
|
|
892
1085
|
config.projectRoot
|
|
893
1086
|
);
|
|
894
1087
|
const runtimeScript = testRuntimeToInject(config);
|
|
1088
|
+
const mainIndexHTML = escapeAndInjectTestsToHTML(
|
|
1089
|
+
mainHTMLWithReplacedAssets,
|
|
1090
|
+
runtimeScript,
|
|
1091
|
+
"./tests.js"
|
|
1092
|
+
);
|
|
1093
|
+
const mainQunitxHTML = escapeAndInjectTestsToHTML(
|
|
1094
|
+
mainHTMLWithReplacedAssets,
|
|
1095
|
+
runtimeScript,
|
|
1096
|
+
"./filtered-tests.js"
|
|
1097
|
+
);
|
|
1098
|
+
const saveHTML = (filePath, html) => fsPromise.writeFile(filePath, html).catch(
|
|
1099
|
+
(err) => config.debug && process.stderr.write(`# [qunitx] writeFile ${filePath}: ${err.message}
|
|
1100
|
+
`)
|
|
1101
|
+
);
|
|
895
1102
|
server.wss.on("connection", function connection(socket) {
|
|
896
1103
|
socket.on("message", function message(data) {
|
|
897
1104
|
const { event, details, qunitResult, abort } = JSON.parse(data);
|
|
@@ -901,17 +1108,7 @@ function setupWebServer(config, cachedContent) {
|
|
|
901
1108
|
} else if (event === "connection") {
|
|
902
1109
|
config._phase = "running";
|
|
903
1110
|
if (!config._groupMode) process.stdout.write("TAP version 13\n");
|
|
904
|
-
if (config.debug && config._groupMode)
|
|
905
|
-
const allFiles = Object.keys(config.fsTree);
|
|
906
|
-
const relFiles = allFiles.map(
|
|
907
|
-
(filePath) => filePath.replace(`${config.projectRoot}/`, "")
|
|
908
|
-
);
|
|
909
|
-
const shown = relFiles.slice(0, 2);
|
|
910
|
-
const rest = relFiles.length - shown.length;
|
|
911
|
-
const fileList = rest > 0 ? `${shown.join(" ")} +${rest} more` : shown.join(" ");
|
|
912
|
-
process.stdout.write(`# ${blue(`\u2500\u2500 ${fileList} \u2500\u2500`)}
|
|
913
|
-
`);
|
|
914
|
-
}
|
|
1111
|
+
if (config.debug && config._groupMode) debugGroupHeader(config);
|
|
915
1112
|
config._resetTestTimeout?.();
|
|
916
1113
|
} else if (event === "testEnd" && !abort) {
|
|
917
1114
|
if (details.status === "failed") {
|
|
@@ -924,7 +1121,7 @@ function setupWebServer(config, cachedContent) {
|
|
|
924
1121
|
);
|
|
925
1122
|
}
|
|
926
1123
|
config._resetTestTimeout?.();
|
|
927
|
-
TAPDisplayTestResult(config.COUNTER, details);
|
|
1124
|
+
TAPDisplayTestResult(config.COUNTER, details, config._sourceMapDecoder, config.projectRoot);
|
|
928
1125
|
} else if (event === "done") {
|
|
929
1126
|
config._phase = "done";
|
|
930
1127
|
config._lastQUnitResult = qunitResult ?? null;
|
|
@@ -941,7 +1138,26 @@ function setupWebServer(config, cachedContent) {
|
|
|
941
1138
|
}
|
|
942
1139
|
});
|
|
943
1140
|
});
|
|
944
|
-
server.get("/tests.js", (_req, res) => {
|
|
1141
|
+
server.get("/tests.js", async (_req, res) => {
|
|
1142
|
+
if (cachedContent._activeRebuild) {
|
|
1143
|
+
await cachedContent._activeRebuild.catch(() => {
|
|
1144
|
+
});
|
|
1145
|
+
if (!cachedContent.allTestCode) {
|
|
1146
|
+
config._lastQUnitResult = {
|
|
1147
|
+
totalTests: 0,
|
|
1148
|
+
finishedTests: 0,
|
|
1149
|
+
failedTests: 0,
|
|
1150
|
+
currentTest: null
|
|
1151
|
+
};
|
|
1152
|
+
config._testRunDone?.();
|
|
1153
|
+
config._testRunDone = null;
|
|
1154
|
+
res.writeHead(200, {
|
|
1155
|
+
"Content-Type": "application/javascript",
|
|
1156
|
+
"Cache-Control": "no-store"
|
|
1157
|
+
});
|
|
1158
|
+
return void res.end();
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
945
1161
|
const bytes = cachedContent.allTestCode?.length ?? null;
|
|
946
1162
|
config.debug && process.stdout.write(
|
|
947
1163
|
`# [HTTPServer] GET /tests.js \u2192 ${bytes !== null ? `${bytes} bytes` : "NOT READY (allTestCode is null)"}
|
|
@@ -949,10 +1165,9 @@ function setupWebServer(config, cachedContent) {
|
|
|
949
1165
|
);
|
|
950
1166
|
if (bytes === null) {
|
|
951
1167
|
res.writeHead(503, { "Content-Type": "application/javascript", "Cache-Control": "no-store" });
|
|
952
|
-
res.end(
|
|
1168
|
+
return void res.end(
|
|
953
1169
|
'console.error("[qunitx] /tests.js requested before bundle was built \u2014 allTestCode is null");'
|
|
954
1170
|
);
|
|
955
|
-
return;
|
|
956
1171
|
}
|
|
957
1172
|
config._onTestsJsServed?.();
|
|
958
1173
|
res.writeHead(200, {
|
|
@@ -970,10 +1185,9 @@ function setupWebServer(config, cachedContent) {
|
|
|
970
1185
|
);
|
|
971
1186
|
if (bytes === null) {
|
|
972
1187
|
res.writeHead(503, { "Content-Type": "application/javascript", "Cache-Control": "no-store" });
|
|
973
|
-
res.end(
|
|
1188
|
+
return res.end(
|
|
974
1189
|
'console.error("[qunitx] /filtered-tests.js requested before bundle was built \u2014 filteredTestCode is null");'
|
|
975
1190
|
);
|
|
976
|
-
return;
|
|
977
1191
|
}
|
|
978
1192
|
config._onTestsJsServed?.();
|
|
979
1193
|
res.writeHead(200, {
|
|
@@ -984,68 +1198,48 @@ function setupWebServer(config, cachedContent) {
|
|
|
984
1198
|
res.end(cachedContent.filteredTestCode);
|
|
985
1199
|
});
|
|
986
1200
|
server.get("/", async (_req, res) => {
|
|
1201
|
+
await cachedContent._activeRebuild?.catch(() => {
|
|
1202
|
+
});
|
|
987
1203
|
if (cachedContent._buildError) {
|
|
988
|
-
const
|
|
989
|
-
res.writeHead(200,
|
|
990
|
-
res.
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1204
|
+
const htmlContent = buildErrorHTML(cachedContent._buildError);
|
|
1205
|
+
res.writeHead(200, HTML_HEADERS);
|
|
1206
|
+
res.end(htmlContent);
|
|
1207
|
+
if (cachedContent._activeRebuild) {
|
|
1208
|
+
config._lastQUnitResult = {
|
|
1209
|
+
totalTests: 0,
|
|
1210
|
+
finishedTests: 0,
|
|
1211
|
+
failedTests: 0,
|
|
1212
|
+
currentTest: null
|
|
1213
|
+
};
|
|
1214
|
+
config._testRunDone?.();
|
|
1215
|
+
config._testRunDone = null;
|
|
1216
|
+
}
|
|
1217
|
+
return saveHTML(`${config.projectRoot}/${config.output}/index.html`, htmlContent);
|
|
996
1218
|
}
|
|
997
1219
|
if (cachedContent._noTestsWarning) {
|
|
998
|
-
|
|
999
|
-
res.
|
|
1000
|
-
res.write(htmlContent2);
|
|
1001
|
-
res.end();
|
|
1002
|
-
return;
|
|
1220
|
+
res.writeHead(200, HTML_HEADERS);
|
|
1221
|
+
return res.end(buildNoTestsHTML(cachedContent._noTestsWarning));
|
|
1003
1222
|
}
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
"./tests.js"
|
|
1008
|
-
);
|
|
1009
|
-
res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
|
|
1010
|
-
res.write(htmlContent);
|
|
1011
|
-
res.end();
|
|
1012
|
-
return await fsPromise.writeFile(
|
|
1013
|
-
`${config.projectRoot}/${config.output}/index.html`,
|
|
1014
|
-
htmlContent
|
|
1015
|
-
);
|
|
1223
|
+
res.writeHead(200, HTML_HEADERS);
|
|
1224
|
+
res.end(mainIndexHTML);
|
|
1225
|
+
saveHTML(`${config.projectRoot}/${config.output}/index.html`, mainIndexHTML);
|
|
1016
1226
|
});
|
|
1017
|
-
server.get("/qunitx.html",
|
|
1227
|
+
server.get("/qunitx.html", (_req, res) => {
|
|
1018
1228
|
if (cachedContent._buildError) {
|
|
1019
|
-
const
|
|
1020
|
-
res.writeHead(200,
|
|
1021
|
-
res.
|
|
1022
|
-
|
|
1023
|
-
return await fsPromise.writeFile(
|
|
1024
|
-
`${config.projectRoot}/${config.output}/qunitx.html`,
|
|
1025
|
-
htmlContent2
|
|
1026
|
-
);
|
|
1229
|
+
const htmlContent = buildErrorHTML(cachedContent._buildError);
|
|
1230
|
+
res.writeHead(200, HTML_HEADERS);
|
|
1231
|
+
res.end(htmlContent);
|
|
1232
|
+
return saveHTML(`${config.projectRoot}/${config.output}/qunitx.html`, htmlContent);
|
|
1027
1233
|
}
|
|
1028
1234
|
if (cachedContent._noTestsWarning) {
|
|
1029
|
-
|
|
1030
|
-
res.
|
|
1031
|
-
res.write(htmlContent2);
|
|
1032
|
-
res.end();
|
|
1033
|
-
return;
|
|
1235
|
+
res.writeHead(200, HTML_HEADERS);
|
|
1236
|
+
return res.end(buildNoTestsHTML(cachedContent._noTestsWarning));
|
|
1034
1237
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
"./filtered-tests.js"
|
|
1039
|
-
);
|
|
1040
|
-
res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
|
|
1041
|
-
res.write(htmlContent);
|
|
1042
|
-
res.end();
|
|
1043
|
-
return await fsPromise.writeFile(
|
|
1044
|
-
`${config.projectRoot}/${config.output}/qunitx.html`,
|
|
1045
|
-
htmlContent
|
|
1046
|
-
);
|
|
1238
|
+
res.writeHead(200, HTML_HEADERS);
|
|
1239
|
+
res.end(mainQunitxHTML);
|
|
1240
|
+
saveHTML(`${config.projectRoot}/${config.output}/qunitx.html`, mainQunitxHTML);
|
|
1047
1241
|
});
|
|
1048
|
-
server.get("/*",
|
|
1242
|
+
server.get("/*", (req, res) => {
|
|
1049
1243
|
const possibleDynamicHTML = cachedContent.dynamicContentHTMLs[`${config.projectRoot}${req.path}`];
|
|
1050
1244
|
if (possibleDynamicHTML) {
|
|
1051
1245
|
const htmlContent = escapeAndInjectTestsToHTML(
|
|
@@ -1053,13 +1247,10 @@ function setupWebServer(config, cachedContent) {
|
|
|
1053
1247
|
runtimeScript,
|
|
1054
1248
|
"/tests.js"
|
|
1055
1249
|
);
|
|
1056
|
-
res.writeHead(200,
|
|
1057
|
-
res.
|
|
1058
|
-
|
|
1059
|
-
return
|
|
1060
|
-
`${config.projectRoot}/${config.output}${req.path}`,
|
|
1061
|
-
htmlContent
|
|
1062
|
-
);
|
|
1250
|
+
res.writeHead(200, HTML_HEADERS);
|
|
1251
|
+
res.end(htmlContent);
|
|
1252
|
+
saveHTML(`${config.projectRoot}/${config.output}${req.path}`, htmlContent);
|
|
1253
|
+
return;
|
|
1063
1254
|
}
|
|
1064
1255
|
const url = req.url;
|
|
1065
1256
|
const requestStartedAt = Date.now();
|
|
@@ -1093,13 +1284,9 @@ function replaceAssetPaths(html, htmlPath, projectRoot) {
|
|
|
1093
1284
|
return result.replace(assetPath, normalizedFullAbsolutePath.replace(projectRoot, "."));
|
|
1094
1285
|
}, html);
|
|
1095
1286
|
}
|
|
1096
|
-
function testRuntimeToInject(config) {
|
|
1287
|
+
function testRuntimeToInject(config, groupId) {
|
|
1288
|
+
const groupIdPart = groupId !== void 0 ? `, groupId: ${groupId}` : "";
|
|
1097
1289
|
return `<script>
|
|
1098
|
-
window.testTimeout = 0;
|
|
1099
|
-
setInterval(() => {
|
|
1100
|
-
window.testTimeout = window.testTimeout + 1000;
|
|
1101
|
-
}, 1000);
|
|
1102
|
-
|
|
1103
1290
|
(function() {
|
|
1104
1291
|
// wsOpenStatus: true once the WebSocket 'open' event fires (or immediately for static files).
|
|
1105
1292
|
// testsLoaded: true once tests.js has executed and dispatched 'qunitx:tests-ready'.
|
|
@@ -1125,7 +1312,7 @@ function testRuntimeToInject(config) {
|
|
|
1125
1312
|
if (window.location.protocol === 'file:') return;
|
|
1126
1313
|
|
|
1127
1314
|
let wsRetryCount = 0;
|
|
1128
|
-
const WS_MAX_RETRIES = Math.ceil(${config.timeout} /
|
|
1315
|
+
const WS_MAX_RETRIES = Math.ceil(${config.timeout} / ${WS_RETRY_INTERVAL_MS}); // retry for the full test timeout window
|
|
1129
1316
|
|
|
1130
1317
|
function setupWebSocket() {
|
|
1131
1318
|
try {
|
|
@@ -1142,7 +1329,7 @@ function testRuntimeToInject(config) {
|
|
|
1142
1329
|
// this runtime script is tiny \u2014 tests.js background compilation hasn't finished yet.
|
|
1143
1330
|
// Node.js uses this to distinguish "WS never connected" from "WS connected but bundle slow".
|
|
1144
1331
|
if (navigator.webdriver) {
|
|
1145
|
-
window.socket.send(JSON.stringify({ event: 'wsOpen' }));
|
|
1332
|
+
window.socket.send(JSON.stringify({ event: 'wsOpen'${groupIdPart} }));
|
|
1146
1333
|
}
|
|
1147
1334
|
maybeStart();
|
|
1148
1335
|
});
|
|
@@ -1164,10 +1351,9 @@ function testRuntimeToInject(config) {
|
|
|
1164
1351
|
wsRetryCount++;
|
|
1165
1352
|
if (wsRetryCount > WS_MAX_RETRIES) {
|
|
1166
1353
|
console.log('WebSocket connection failed after ' + WS_MAX_RETRIES + ' retries');
|
|
1167
|
-
window.testTimeout = ${config.timeout};
|
|
1168
1354
|
return;
|
|
1169
1355
|
}
|
|
1170
|
-
window.setTimeout(setupWebSocket,
|
|
1356
|
+
window.setTimeout(setupWebSocket, ${WS_RETRY_INTERVAL_MS});
|
|
1171
1357
|
}
|
|
1172
1358
|
|
|
1173
1359
|
setupWebSocket();
|
|
@@ -1201,8 +1387,6 @@ function testRuntimeToInject(config) {
|
|
|
1201
1387
|
// "no tests registered" warning (not a failure), so this gives a fast, clean result.
|
|
1202
1388
|
window.QUNIT_RESULT = { totalTests: 0, finishedTests: 0, failedTests: 0, currentTest: null };
|
|
1203
1389
|
window.socket.send(JSON.stringify({ event: 'done', details: { passed: 0, failed: 0, runtime: 0 } }));
|
|
1204
|
-
} else {
|
|
1205
|
-
window.testTimeout = ${config.timeout};
|
|
1206
1390
|
}
|
|
1207
1391
|
return;
|
|
1208
1392
|
}
|
|
@@ -1217,7 +1401,6 @@ function testRuntimeToInject(config) {
|
|
|
1217
1401
|
window.QUNIT_RESULT.currentTest = details.fullName.join(' | ');
|
|
1218
1402
|
});
|
|
1219
1403
|
window.QUnit.on('testEnd', (details) => { // NOTE: https://github.com/qunitjs/qunit/blob/master/src/html-reporter/diff.js
|
|
1220
|
-
window.testTimeout = 0;
|
|
1221
1404
|
window.QUNIT_RESULT.finishedTests++;
|
|
1222
1405
|
if (details.status === 'failed') window.QUNIT_RESULT.failedTests++;
|
|
1223
1406
|
window.QUNIT_RESULT.currentTest = null;
|
|
@@ -1234,16 +1417,10 @@ function testRuntimeToInject(config) {
|
|
|
1234
1417
|
window.QUnit.done((details) => {
|
|
1235
1418
|
if (navigator.webdriver) {
|
|
1236
1419
|
window.socket.send(JSON.stringify({ event: 'done', details: details, qunitResult: window.QUNIT_RESULT, abort: window.abortQUnit }, getCircularReplacer()));
|
|
1237
|
-
// Do NOT set testTimeout here. The WS 'done' event (testsDone promise) is the
|
|
1238
|
-
// canonical completion signal for Playwright runs. waitForFunction is reserved
|
|
1239
|
-
// for true timeouts (test hangs) where testTimeout increments naturally via setInterval.
|
|
1240
|
-
// Setting testTimeout after done caused a race: under CI load, waitForFunction could
|
|
1241
|
-
// win before Node.js processed the WS done message, dropping all testEnd events.
|
|
1242
|
-
} else {
|
|
1243
|
-
window.testTimeout = ${config.timeout};
|
|
1244
1420
|
}
|
|
1245
1421
|
});
|
|
1246
1422
|
|
|
1423
|
+
window.QUnit.config.testTimeout = ${config.timeout};
|
|
1247
1424
|
window.QUnit.start();
|
|
1248
1425
|
}
|
|
1249
1426
|
</script>`;
|
|
@@ -1344,8 +1521,8 @@ function buildNoTestsHTML(files) {
|
|
|
1344
1521
|
var retries = 0;
|
|
1345
1522
|
function connect() {
|
|
1346
1523
|
var ws = new WebSocket(\`ws://\${location.hostname}:\${location.port}\`);
|
|
1347
|
-
ws.addEventListener('message', function (e) { if (e.data === 'refresh') location.reload(true); });
|
|
1348
|
-
ws.addEventListener('close', function () { if (retries++ <
|
|
1524
|
+
ws.addEventListener('message', function (e) { if (e.data === 'refresh' && !navigator.webdriver) location.reload(true); });
|
|
1525
|
+
ws.addEventListener('close', function () { if (retries++ < ${WATCH_WS_RECONNECT_MAX_RETRIES}) setTimeout(connect, ${WATCH_WS_RECONNECT_INTERVAL_MS}); });
|
|
1349
1526
|
ws.addEventListener('error', function () { ws.close(); });
|
|
1350
1527
|
}
|
|
1351
1528
|
connect();
|
|
@@ -1446,8 +1623,8 @@ function buildErrorHTML(buildError) {
|
|
|
1446
1623
|
var retries = 0;
|
|
1447
1624
|
function connect() {
|
|
1448
1625
|
var ws = new WebSocket(\`ws://\${location.hostname}:\${location.port}\`);
|
|
1449
|
-
ws.addEventListener('message', function (e) { if (e.data === 'refresh') location.reload(true); });
|
|
1450
|
-
ws.addEventListener('close', function () { if (retries++ <
|
|
1626
|
+
ws.addEventListener('message', function (e) { if (e.data === 'refresh' && !navigator.webdriver) location.reload(true); });
|
|
1627
|
+
ws.addEventListener('close', function () { if (retries++ < ${WATCH_WS_RECONNECT_MAX_RETRIES}) setTimeout(connect, ${WATCH_WS_RECONNECT_INTERVAL_MS}); });
|
|
1451
1628
|
ws.addEventListener('error', function () { ws.close(); });
|
|
1452
1629
|
}
|
|
1453
1630
|
connect();
|
|
@@ -1457,7 +1634,143 @@ function buildErrorHTML(buildError) {
|
|
|
1457
1634
|
</body>
|
|
1458
1635
|
</html>`;
|
|
1459
1636
|
}
|
|
1460
|
-
|
|
1637
|
+
function registerGroupRoutes(server, groupConfig, groupCachedContent, groupId) {
|
|
1638
|
+
const mainHTMLWithReplacedAssets = replaceAssetPaths(
|
|
1639
|
+
groupCachedContent.mainHTML.html,
|
|
1640
|
+
groupCachedContent.mainHTML.filePath,
|
|
1641
|
+
groupConfig.projectRoot
|
|
1642
|
+
);
|
|
1643
|
+
const runtimeScript = testRuntimeToInject(groupConfig, groupId);
|
|
1644
|
+
const mainGroupHTML = escapeAndInjectTestsToHTML(
|
|
1645
|
+
mainHTMLWithReplacedAssets,
|
|
1646
|
+
runtimeScript,
|
|
1647
|
+
"./tests.js"
|
|
1648
|
+
);
|
|
1649
|
+
const saveHTML = (filePath, html) => fsPromise.writeFile(filePath, html).catch(
|
|
1650
|
+
(err) => groupConfig.debug && process.stderr.write(`# [qunitx] writeFile ${filePath}: ${err.message}
|
|
1651
|
+
`)
|
|
1652
|
+
);
|
|
1653
|
+
server.get(`/group-${groupId}/`, (_req, res) => {
|
|
1654
|
+
if (groupCachedContent._buildError) {
|
|
1655
|
+
res.writeHead(200, HTML_HEADERS);
|
|
1656
|
+
return res.end(buildErrorHTML(groupCachedContent._buildError));
|
|
1657
|
+
}
|
|
1658
|
+
if (groupCachedContent._noTestsWarning) {
|
|
1659
|
+
res.writeHead(200, HTML_HEADERS);
|
|
1660
|
+
return res.end(buildNoTestsHTML(groupCachedContent._noTestsWarning));
|
|
1661
|
+
}
|
|
1662
|
+
res.writeHead(200, HTML_HEADERS);
|
|
1663
|
+
res.end(mainGroupHTML);
|
|
1664
|
+
saveHTML(`${groupConfig.projectRoot}/${groupConfig.output}/index.html`, mainGroupHTML);
|
|
1665
|
+
});
|
|
1666
|
+
server.get(`/group-${groupId}/tests.js`, (_req, res) => {
|
|
1667
|
+
const bytes = groupCachedContent.allTestCode?.length ?? null;
|
|
1668
|
+
if (bytes === null) {
|
|
1669
|
+
res.writeHead(503, { "Content-Type": "application/javascript", "Cache-Control": "no-store" });
|
|
1670
|
+
return res.end(
|
|
1671
|
+
'console.error("[qunitx] /tests.js requested before bundle was built \u2014 allTestCode is null");'
|
|
1672
|
+
);
|
|
1673
|
+
}
|
|
1674
|
+
groupConfig._onTestsJsServed?.();
|
|
1675
|
+
res.writeHead(200, {
|
|
1676
|
+
"Content-Type": "application/javascript",
|
|
1677
|
+
"Cache-Control": "no-store",
|
|
1678
|
+
"Content-Length": bytes
|
|
1679
|
+
});
|
|
1680
|
+
res.end(groupCachedContent.allTestCode);
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
function debugGroupHeader(config) {
|
|
1684
|
+
const files = Object.keys(config.fsTree);
|
|
1685
|
+
const rel = files.map((f) => f.replace(`${config.projectRoot}/`, ""));
|
|
1686
|
+
const shown = rel.slice(0, 2);
|
|
1687
|
+
const rest = rel.length - shown.length;
|
|
1688
|
+
process.stdout.write(
|
|
1689
|
+
`# ${blue(`\u2500\u2500 ${shown.join(" ")}${rest > 0 ? ` +${rest} more` : ""} \u2500\u2500`)}
|
|
1690
|
+
`
|
|
1691
|
+
);
|
|
1692
|
+
}
|
|
1693
|
+
function setupGroupWSHandler(server, groupConfigs) {
|
|
1694
|
+
const socketToGroupId = /* @__PURE__ */ new WeakMap();
|
|
1695
|
+
server.wss.on("connection", function connection(socket) {
|
|
1696
|
+
socket.on("message", function message(data) {
|
|
1697
|
+
const { event, groupId, details, qunitResult, abort } = JSON.parse(data);
|
|
1698
|
+
let resolvedGroupId = socketToGroupId.get(socket);
|
|
1699
|
+
if (event === "wsOpen" && typeof groupId === "number") {
|
|
1700
|
+
resolvedGroupId = groupId;
|
|
1701
|
+
socketToGroupId.set(socket, groupId);
|
|
1702
|
+
}
|
|
1703
|
+
if (resolvedGroupId === void 0) return;
|
|
1704
|
+
const config = groupConfigs[resolvedGroupId];
|
|
1705
|
+
if (!config) return;
|
|
1706
|
+
if (event === "wsOpen") {
|
|
1707
|
+
config._phase = "loading";
|
|
1708
|
+
config._onWsOpen?.();
|
|
1709
|
+
} else if (event === "connection") {
|
|
1710
|
+
config._phase = "running";
|
|
1711
|
+
if (config.debug) debugGroupHeader(config);
|
|
1712
|
+
config._resetTestTimeout?.();
|
|
1713
|
+
} else if (event === "testEnd" && !abort) {
|
|
1714
|
+
if (details.status === "failed") {
|
|
1715
|
+
config.lastFailedTestFiles = config.lastRanTestFiles;
|
|
1716
|
+
}
|
|
1717
|
+
if (config.debug && details.runtime > config.timeout * 0.8) {
|
|
1718
|
+
process.stdout.write(
|
|
1719
|
+
`# SLOW (${details.runtime.toFixed(0)}ms / ${config.timeout}ms timeout): ${details.fullName.join(" | ")}
|
|
1720
|
+
`
|
|
1721
|
+
);
|
|
1722
|
+
}
|
|
1723
|
+
config._resetTestTimeout?.();
|
|
1724
|
+
TAPDisplayTestResult(config.COUNTER, details, config._sourceMapDecoder, config.projectRoot);
|
|
1725
|
+
} else if (event === "done") {
|
|
1726
|
+
config._phase = "done";
|
|
1727
|
+
config._lastQUnitResult = qunitResult ?? null;
|
|
1728
|
+
if (config.debug) {
|
|
1729
|
+
process.stdout.write(
|
|
1730
|
+
`# group done: ${details.passed} passed, ${details.failed} failed (${details.runtime}ms)
|
|
1731
|
+
`
|
|
1732
|
+
);
|
|
1733
|
+
}
|
|
1734
|
+
if (typeof config._testRunDone === "function") {
|
|
1735
|
+
config._testRunDone();
|
|
1736
|
+
config._testRunDone = null;
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
});
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
function registerSharedStaticHandler(server, groupConfigs) {
|
|
1743
|
+
const groupUrlRegex = /^\/group-(\d+)(\/.*)?$/;
|
|
1744
|
+
server.get("/*", (req, res) => {
|
|
1745
|
+
const match = groupUrlRegex.exec(req.path);
|
|
1746
|
+
if (!match) {
|
|
1747
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
1748
|
+
res.end("Not found");
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
const groupId = parseInt(match[1], 10);
|
|
1752
|
+
const groupConfig = groupConfigs[groupId];
|
|
1753
|
+
if (!groupConfig) {
|
|
1754
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
1755
|
+
res.end("Not found");
|
|
1756
|
+
return;
|
|
1757
|
+
}
|
|
1758
|
+
const STATIC_FILES_PATH = path4.join(groupConfig.projectRoot, groupConfig.output);
|
|
1759
|
+
const subPath = match[2] || "/";
|
|
1760
|
+
const filePath = (subPath.endsWith("/") ? [STATIC_FILES_PATH, subPath, "index.html"] : [STATIC_FILES_PATH, subPath]).join("");
|
|
1761
|
+
const contentType = req.headers.accept?.includes("text/html") ? MIME_TYPES.html : MIME_TYPES[path4.extname(filePath).substring(1).toLowerCase()] || MIME_TYPES.html;
|
|
1762
|
+
const stream = fs8.createReadStream(filePath);
|
|
1763
|
+
stream.on("open", () => {
|
|
1764
|
+
res.writeHead(200, { "Content-Type": contentType });
|
|
1765
|
+
stream.pipe(res);
|
|
1766
|
+
});
|
|
1767
|
+
stream.on("error", () => {
|
|
1768
|
+
res.writeHead(404, { "Content-Type": contentType });
|
|
1769
|
+
res.end(contentType === MIME_TYPES.html ? NOT_FOUND_HTML : void 0);
|
|
1770
|
+
});
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
var fsPromise, HTML_HEADERS, WATCH_WS_RECONNECT_INTERVAL_MS, WATCH_WS_RECONNECT_MAX_RETRIES, WS_RETRY_INTERVAL_MS, NOT_FOUND_HTML;
|
|
1461
1774
|
var init_web_server = __esm({
|
|
1462
1775
|
"lib/setup/web-server.ts"() {
|
|
1463
1776
|
init_find_internal_assets_from_html();
|
|
@@ -1466,6 +1779,10 @@ var init_web_server = __esm({
|
|
|
1466
1779
|
init_color();
|
|
1467
1780
|
init_http();
|
|
1468
1781
|
fsPromise = fs8.promises;
|
|
1782
|
+
HTML_HEADERS = { "Content-Type": "text/html", "Cache-Control": "no-store" };
|
|
1783
|
+
WATCH_WS_RECONNECT_INTERVAL_MS = 1e3;
|
|
1784
|
+
WATCH_WS_RECONNECT_MAX_RETRIES = 120;
|
|
1785
|
+
WS_RETRY_INTERVAL_MS = 10;
|
|
1469
1786
|
NOT_FOUND_HTML = `<!DOCTYPE html>
|
|
1470
1787
|
<html lang="en">
|
|
1471
1788
|
<head>
|
|
@@ -1541,19 +1858,24 @@ async function launchBrowser(config) {
|
|
|
1541
1858
|
handleSIGHUP: false
|
|
1542
1859
|
});
|
|
1543
1860
|
}
|
|
1544
|
-
async function setupBrowser(config, cachedContent, existingBrowser = null) {
|
|
1861
|
+
async function setupBrowser(config, cachedContent, existingBrowser = null, sharedServer = null) {
|
|
1545
1862
|
const setupStart = Date.now();
|
|
1546
|
-
const [server,
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1863
|
+
const [server, browser, page] = await (async () => {
|
|
1864
|
+
if (sharedServer) {
|
|
1865
|
+
const newPage2 = await existingBrowser.newPage();
|
|
1866
|
+
perfLog(`browser.js: newPage (shared server) took ${Date.now() - setupStart}ms`);
|
|
1867
|
+
return [sharedServer, existingBrowser, newPage2];
|
|
1868
|
+
}
|
|
1869
|
+
const newServer = setupWebServer(config, cachedContent);
|
|
1870
|
+
perfLog(`browser.js: setupWebServer took ${Date.now() - setupStart}ms`);
|
|
1871
|
+
const activeBrowser = existingBrowser ?? await launchBrowser(config);
|
|
1872
|
+
const pageStart = Date.now();
|
|
1873
|
+
const isHeadedWatchMode = config.open === true && config.watch;
|
|
1874
|
+
const getPage = isHeadedWatchMode ? () => activeBrowser.contexts()[0]?.pages()[0] ?? activeBrowser.newPage() : () => activeBrowser.newPage();
|
|
1875
|
+
const [newPage] = await Promise.all([getPage(), bindServerToPort(newServer, config)]);
|
|
1876
|
+
perfLog(`browser.js: newPage + bindServerToPort took ${Date.now() - pageStart}ms`);
|
|
1877
|
+
return [newServer, activeBrowser, newPage];
|
|
1878
|
+
})();
|
|
1557
1879
|
if (config.browser === "firefox") {
|
|
1558
1880
|
await page.addInitScript(() => {
|
|
1559
1881
|
const preSerialize = (arg) => {
|
|
@@ -1740,7 +2062,7 @@ async function buildTestBundle(config, cachedContent) {
|
|
|
1740
2062
|
}
|
|
1741
2063
|
const outfile = `${projectRoot}/${output}/tests.js`;
|
|
1742
2064
|
await fs9.mkdir(`${projectRoot}/${output}`, { recursive: true });
|
|
1743
|
-
const sourcemap =
|
|
2065
|
+
const sourcemap = "inline";
|
|
1744
2066
|
const needsDisk = true;
|
|
1745
2067
|
const buildOptions = {
|
|
1746
2068
|
stdin: {
|
|
@@ -1780,6 +2102,7 @@ async function buildTestBundle(config, cachedContent) {
|
|
|
1780
2102
|
)
|
|
1781
2103
|
]);
|
|
1782
2104
|
cachedContent.allTestCode = allTestCode;
|
|
2105
|
+
config._sourceMapDecoder = extractInlineSourceMap(allTestCode, `${projectRoot}/${output}`);
|
|
1783
2106
|
} catch (error) {
|
|
1784
2107
|
cachedContent._buildError = {
|
|
1785
2108
|
type: deriveBuildErrorType(error),
|
|
@@ -1811,9 +2134,13 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
1811
2134
|
const preBuildPromise = cachedContent._preBuildPromise;
|
|
1812
2135
|
cachedContent._preBuildPromise = null;
|
|
1813
2136
|
if (!cachedContent.allTestCode) {
|
|
1814
|
-
|
|
2137
|
+
if (preBuildPromise) {
|
|
2138
|
+
cachedContent._activeRebuild = preBuildPromise;
|
|
2139
|
+
} else {
|
|
2140
|
+
await buildTestBundle(config, cachedContent);
|
|
2141
|
+
}
|
|
1815
2142
|
}
|
|
1816
|
-
if (!cachedContent.allTestCode) {
|
|
2143
|
+
if (!cachedContent.allTestCode && !cachedContent._activeRebuild) {
|
|
1817
2144
|
return connections;
|
|
1818
2145
|
}
|
|
1819
2146
|
if (runHasFilter) {
|
|
@@ -1823,6 +2150,10 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
1823
2150
|
outputPath,
|
|
1824
2151
|
config
|
|
1825
2152
|
);
|
|
2153
|
+
config._sourceMapDecoder = extractInlineSourceMap(
|
|
2154
|
+
cachedContent.filteredTestCode,
|
|
2155
|
+
`${projectRoot}/${output}`
|
|
2156
|
+
);
|
|
1826
2157
|
}
|
|
1827
2158
|
const TIME_COUNTER = timeCounter();
|
|
1828
2159
|
if (runHasFilter) {
|
|
@@ -1834,6 +2165,17 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
1834
2165
|
)
|
|
1835
2166
|
);
|
|
1836
2167
|
}
|
|
2168
|
+
if (cachedContent._activeRebuild) {
|
|
2169
|
+
await cachedContent._activeRebuild.catch(() => {
|
|
2170
|
+
});
|
|
2171
|
+
cachedContent._activeRebuild = null;
|
|
2172
|
+
if (!cachedContent.allTestCode) {
|
|
2173
|
+
config.watch && cachedContent._buildError && console.log(
|
|
2174
|
+
`# esbuild Bundle Error: ${cachedContent._buildError.formatted}`.split("\n").join("\n# ")
|
|
2175
|
+
);
|
|
2176
|
+
return connections;
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
1837
2179
|
const TIME_TAKEN = TIME_COUNTER.stop();
|
|
1838
2180
|
if (!config._groupMode) {
|
|
1839
2181
|
if (config.COUNTER.testCount === 0 && !cachedContent._buildError) {
|
|
@@ -1865,6 +2207,7 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
1865
2207
|
}
|
|
1866
2208
|
}
|
|
1867
2209
|
} catch (error) {
|
|
2210
|
+
cachedContent._activeRebuild = null;
|
|
1868
2211
|
config.lastFailedTestFiles = config.lastRanTestFiles;
|
|
1869
2212
|
const exception = new BundleError(error);
|
|
1870
2213
|
if (!cachedContent._buildError && error.errors?.length) {
|
|
@@ -1875,8 +2218,10 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
1875
2218
|
fs9.writeFile(
|
|
1876
2219
|
`${projectRoot}/${output}/qunitx.html`,
|
|
1877
2220
|
buildErrorHTML(cachedContent._buildError)
|
|
1878
|
-
).catch(
|
|
1879
|
-
|
|
2221
|
+
).catch(
|
|
2222
|
+
(err) => config.debug && process.stderr.write(`# [qunitx] writeFile qunitx.html: ${err.message}
|
|
2223
|
+
`)
|
|
2224
|
+
);
|
|
1880
2225
|
}
|
|
1881
2226
|
if (config.watch) {
|
|
1882
2227
|
console.log(`# ${exception}`);
|
|
@@ -1887,8 +2232,8 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
1887
2232
|
return connections;
|
|
1888
2233
|
}
|
|
1889
2234
|
function buildFilteredTests(filteredTests, outputPath, config) {
|
|
1890
|
-
const sourcemap =
|
|
1891
|
-
const needsDisk =
|
|
2235
|
+
const sourcemap = "inline";
|
|
2236
|
+
const needsDisk = Boolean(config.open);
|
|
1892
2237
|
return buildWithOverlayfsRetry(
|
|
1893
2238
|
{
|
|
1894
2239
|
stdin: {
|
|
@@ -1908,9 +2253,6 @@ function buildFilteredTests(filteredTests, outputPath, config) {
|
|
|
1908
2253
|
);
|
|
1909
2254
|
}
|
|
1910
2255
|
async function runWithOverlayfsRetry(getContents, needsDisk) {
|
|
1911
|
-
const RETRY_DELAY_MS = 100;
|
|
1912
|
-
const MAX_RETRIES = 3;
|
|
1913
|
-
const EMPTY_BUNDLE_THRESHOLD = 500;
|
|
1914
2256
|
let { result, js } = await getContents();
|
|
1915
2257
|
const initialSize = js.length;
|
|
1916
2258
|
for (let retry = 1; retry <= MAX_RETRIES; retry++) {
|
|
@@ -1965,9 +2307,9 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
|
|
|
1965
2307
|
let wsConnected = false;
|
|
1966
2308
|
try {
|
|
1967
2309
|
console.log("#", blue(`QUnitX running: http://localhost:${config.port}${filePath}`));
|
|
1968
|
-
const navMs = config.timeout +
|
|
1969
|
-
const startupMs = Math.max(config.timeout *
|
|
1970
|
-
const testsJsMs = Math.max(config.timeout *
|
|
2310
|
+
const navMs = config.timeout + NAV_GRACE_MS;
|
|
2311
|
+
const startupMs = Math.max(config.timeout * STARTUP_TIMEOUT_FACTOR, navMs);
|
|
2312
|
+
const testsJsMs = Math.max(config.timeout * TESTS_JS_TIMEOUT_FACTOR, navMs);
|
|
1971
2313
|
let resolveTestRace;
|
|
1972
2314
|
const testRaceResult = new Promise((resolve) => {
|
|
1973
2315
|
resolveTestRace = resolve;
|
|
@@ -1985,7 +2327,7 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
|
|
|
1985
2327
|
config._resetTestTimeout = () => {
|
|
1986
2328
|
wsConnected = true;
|
|
1987
2329
|
clearTimeout(timeoutHandle);
|
|
1988
|
-
timeoutHandle = setTimeout(resolveTestRace, config.timeout);
|
|
2330
|
+
timeoutHandle = setTimeout(resolveTestRace, config.timeout + TEST_STALL_BUFFER_MS);
|
|
1989
2331
|
};
|
|
1990
2332
|
const targetUrl = `http://localhost:${config.port}${filePath}`;
|
|
1991
2333
|
const navOptions = { timeout: navMs, waitUntil: "commit" };
|
|
@@ -2055,12 +2397,122 @@ async function failOnNonWatchMode(watchMode = false, connections = {}, groupMode
|
|
|
2055
2397
|
process.exit(1);
|
|
2056
2398
|
}
|
|
2057
2399
|
}
|
|
2058
|
-
async function flushConsoleHandlers(handlers, deadline = Date.now() +
|
|
2400
|
+
async function flushConsoleHandlers(handlers, deadline = Date.now() + CONSOLE_FLUSH_TIMEOUT_MS) {
|
|
2059
2401
|
if (!handlers || handlers.size === 0 || Date.now() >= deadline) return;
|
|
2060
2402
|
await Promise.allSettled([...handlers]);
|
|
2061
2403
|
return flushConsoleHandlers(handlers, deadline);
|
|
2062
2404
|
}
|
|
2063
|
-
|
|
2405
|
+
async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
2406
|
+
groupCachedContents.forEach((cachedContent) => {
|
|
2407
|
+
cachedContent._buildError = null;
|
|
2408
|
+
cachedContent._noTestsWarning = null;
|
|
2409
|
+
});
|
|
2410
|
+
const { projectRoot, debug, browser } = groupConfigs[0];
|
|
2411
|
+
const activeGroups = groupConfigs.reduce(
|
|
2412
|
+
(acc, groupConfig, groupIndex) => {
|
|
2413
|
+
const files = Object.keys(groupConfig.fsTree);
|
|
2414
|
+
if (files.length > 0)
|
|
2415
|
+
acc.push({
|
|
2416
|
+
groupIndex,
|
|
2417
|
+
config: groupConfig,
|
|
2418
|
+
cachedContent: groupCachedContents[groupIndex],
|
|
2419
|
+
files
|
|
2420
|
+
});
|
|
2421
|
+
return acc;
|
|
2422
|
+
},
|
|
2423
|
+
[]
|
|
2424
|
+
);
|
|
2425
|
+
if (activeGroups.length === 0)
|
|
2426
|
+
return console.log(
|
|
2427
|
+
"# [buildAllGroupBundles] all groups empty \u2014 skipping build (no test files found)"
|
|
2428
|
+
);
|
|
2429
|
+
await Promise.all(
|
|
2430
|
+
activeGroups.map(
|
|
2431
|
+
(group) => fs9.mkdir(`${group.config.projectRoot}/${group.config.output}`, { recursive: true })
|
|
2432
|
+
)
|
|
2433
|
+
);
|
|
2434
|
+
const sourcemap = "inline";
|
|
2435
|
+
const groupEntryPlugin = {
|
|
2436
|
+
name: "group-entry-loader",
|
|
2437
|
+
setup(build) {
|
|
2438
|
+
build.onResolve({ filter: /^group-entry-\d+$/ }, (args) => ({
|
|
2439
|
+
path: args.path,
|
|
2440
|
+
namespace: "group-entry"
|
|
2441
|
+
}));
|
|
2442
|
+
build.onLoad({ filter: /.*/, namespace: "group-entry" }, (args) => {
|
|
2443
|
+
const slotIndex = parseInt(args.path.replace("group-entry-", ""));
|
|
2444
|
+
return {
|
|
2445
|
+
contents: activeGroups[slotIndex].files.map((filePath) => `import "${filePath}";`).join(""),
|
|
2446
|
+
resolveDir: process.cwd()
|
|
2447
|
+
};
|
|
2448
|
+
});
|
|
2449
|
+
}
|
|
2450
|
+
};
|
|
2451
|
+
const esbuildOutdir = path5.join(projectRoot, "tmp");
|
|
2452
|
+
const buildOptions = {
|
|
2453
|
+
entryPoints: activeGroups.map((_, slotIndex) => ({
|
|
2454
|
+
in: `group-entry-${slotIndex}`,
|
|
2455
|
+
out: `group-${slotIndex}`
|
|
2456
|
+
})),
|
|
2457
|
+
plugins: [groupEntryPlugin],
|
|
2458
|
+
nodePaths: ANCESTOR_NODE_MODULES,
|
|
2459
|
+
bundle: true,
|
|
2460
|
+
logLevel: "silent",
|
|
2461
|
+
// outdir only labels the paths in outputFiles[].path — nothing is written to disk
|
|
2462
|
+
// (write:false). Use projectRoot/tmp as a stable sentinel; mkdir is not required.
|
|
2463
|
+
outdir: esbuildOutdir,
|
|
2464
|
+
keepNames: true,
|
|
2465
|
+
legalComments: "none",
|
|
2466
|
+
target: esbuildTarget(browser),
|
|
2467
|
+
sourcemap,
|
|
2468
|
+
write: false,
|
|
2469
|
+
footer: { js: 'window.dispatchEvent(new CustomEvent("qunitx:tests-ready"));' }
|
|
2470
|
+
};
|
|
2471
|
+
const hasSmallOutput = (result) => (result.outputFiles ?? []).some(
|
|
2472
|
+
(outputFile) => GROUP_OUTPUT_REGEX.test(outputFile.path) && !outputFile.path.endsWith(".map") && outputFile.contents.length < EMPTY_BUNDLE_THRESHOLD
|
|
2473
|
+
);
|
|
2474
|
+
const buildWithRetry = async (retriesLeft) => {
|
|
2475
|
+
const result = await esbuild.build(buildOptions);
|
|
2476
|
+
if (!hasSmallOutput(result) || retriesLeft === 0) return result;
|
|
2477
|
+
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
|
|
2478
|
+
return buildWithRetry(retriesLeft - 1);
|
|
2479
|
+
};
|
|
2480
|
+
try {
|
|
2481
|
+
const result = await buildWithRetry(MAX_RETRIES);
|
|
2482
|
+
await Promise.all(
|
|
2483
|
+
(result.outputFiles ?? []).map((outputFile) => {
|
|
2484
|
+
const match = GROUP_OUTPUT_REGEX.exec(outputFile.path);
|
|
2485
|
+
if (!match) return Promise.resolve();
|
|
2486
|
+
const slotIndex = parseInt(match[1]);
|
|
2487
|
+
const isMap = Boolean(match[2]);
|
|
2488
|
+
const { config, cachedContent } = activeGroups[slotIndex];
|
|
2489
|
+
const destPath = `${config.projectRoot}/${config.output}/tests.js${isMap ? ".map" : ""}`;
|
|
2490
|
+
if (!isMap) {
|
|
2491
|
+
cachedContent.allTestCode = Buffer.from(outputFile.contents);
|
|
2492
|
+
config._sourceMapDecoder = extractInlineSourceMap(
|
|
2493
|
+
cachedContent.allTestCode,
|
|
2494
|
+
esbuildOutdir
|
|
2495
|
+
);
|
|
2496
|
+
}
|
|
2497
|
+
return fs9.writeFile(destPath, outputFile.contents);
|
|
2498
|
+
})
|
|
2499
|
+
);
|
|
2500
|
+
} catch (error) {
|
|
2501
|
+
const buildError = { type: deriveBuildErrorType(error), formatted: formatBuildErrors(error) };
|
|
2502
|
+
const errorHtml = buildErrorHTML(buildError);
|
|
2503
|
+
await Promise.all(
|
|
2504
|
+
activeGroups.map((group) => {
|
|
2505
|
+
group.cachedContent._buildError = buildError;
|
|
2506
|
+
return fs9.writeFile(`${group.config.projectRoot}/${group.config.output}/index.html`, errorHtml).catch(
|
|
2507
|
+
(err) => debug && process.stderr.write(`# [qunitx] writeFile index.html: ${err.message}
|
|
2508
|
+
`)
|
|
2509
|
+
);
|
|
2510
|
+
})
|
|
2511
|
+
);
|
|
2512
|
+
throw error;
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
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;
|
|
2064
2516
|
var init_tests_in_browser = __esm({
|
|
2065
2517
|
"lib/commands/run/tests-in-browser.ts"() {
|
|
2066
2518
|
init_color();
|
|
@@ -2069,6 +2521,7 @@ var init_tests_in_browser = __esm({
|
|
|
2069
2521
|
init_run_user_module();
|
|
2070
2522
|
init_display_final_result();
|
|
2071
2523
|
init_web_server();
|
|
2524
|
+
init_source_map_decoder();
|
|
2072
2525
|
BundleError = class extends Error {
|
|
2073
2526
|
constructor(message) {
|
|
2074
2527
|
super(message);
|
|
@@ -2080,6 +2533,15 @@ var init_tests_in_browser = __esm({
|
|
|
2080
2533
|
(_, i, parts) => path5.join(parts.slice(0, parts.length - i).join(path5.sep) || path5.sep, "node_modules")
|
|
2081
2534
|
);
|
|
2082
2535
|
ANCESTOR_NODE_MODULES = ancestorNodeModules(process.cwd());
|
|
2536
|
+
RETRY_DELAY_MS = 100;
|
|
2537
|
+
MAX_RETRIES = 3;
|
|
2538
|
+
EMPTY_BUNDLE_THRESHOLD = 500;
|
|
2539
|
+
NAV_GRACE_MS = 1e4;
|
|
2540
|
+
STARTUP_TIMEOUT_FACTOR = 3;
|
|
2541
|
+
TESTS_JS_TIMEOUT_FACTOR = 4;
|
|
2542
|
+
CONSOLE_FLUSH_TIMEOUT_MS = 2e3;
|
|
2543
|
+
TEST_STALL_BUFFER_MS = 5e3;
|
|
2544
|
+
GROUP_OUTPUT_REGEX = /group-(\d+)\.js(\.map)?$/;
|
|
2083
2545
|
}
|
|
2084
2546
|
});
|
|
2085
2547
|
|
|
@@ -2104,7 +2566,7 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
2104
2566
|
}
|
|
2105
2567
|
}
|
|
2106
2568
|
};
|
|
2107
|
-
fs10.watchFile(filePath, { interval:
|
|
2569
|
+
fs10.watchFile(filePath, { interval: SYMLINK_POLL_INTERVAL_MS, persistent: false }, handler);
|
|
2108
2570
|
symlinkPollers.set(filePath, () => fs10.unwatchFile(filePath, handler));
|
|
2109
2571
|
}
|
|
2110
2572
|
function untrackSymlink(filePath) {
|
|
@@ -2118,13 +2580,17 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
2118
2580
|
if (!ready || !filename) return;
|
|
2119
2581
|
const fullPath = filename === path6.basename(watchPath) ? watchPath : path6.join(watchPath, filename);
|
|
2120
2582
|
if (eventType === "change") {
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2583
|
+
const now = Date.now();
|
|
2584
|
+
const last = lastChangeMs[fullPath] ?? 0;
|
|
2585
|
+
lastChangeMs[fullPath] = now;
|
|
2586
|
+
if (now - last < CHANGE_DEDUPE_MS && (config._building || !config._lastBuildEndMs || config._lastBuildEndMs <= last))
|
|
2587
|
+
return;
|
|
2588
|
+
if (config._lastBuildEndMs) {
|
|
2589
|
+
try {
|
|
2590
|
+
const { mtimeMs } = await stat(fullPath);
|
|
2591
|
+
if (mtimeMs < Math.floor(config._lastBuildEndMs / 1e3) * 1e3) return;
|
|
2592
|
+
} catch {
|
|
2126
2593
|
}
|
|
2127
|
-
lastChangeMs[fullPath] = now;
|
|
2128
2594
|
}
|
|
2129
2595
|
return handleWatchEvent(config, extensions, "change", fullPath, onEventFunc, onFinishFunc);
|
|
2130
2596
|
}
|
|
@@ -2194,7 +2660,7 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
2194
2660
|
};
|
|
2195
2661
|
}
|
|
2196
2662
|
async function classifyRenameEvent(fullPath, fsTree) {
|
|
2197
|
-
for (const delay of [0,
|
|
2663
|
+
for (const delay of [0, OVERLAYFS_RENAME_RETRY_MS]) {
|
|
2198
2664
|
if (delay) await new Promise((resolve) => setTimeout(resolve, delay));
|
|
2199
2665
|
try {
|
|
2200
2666
|
const statResult = await stat(fullPath);
|
|
@@ -2262,11 +2728,13 @@ function colorEvent(event) {
|
|
|
2262
2728
|
if (event === "add" || event === "addDir") return green("ADDED:");
|
|
2263
2729
|
return red("REMOVED:");
|
|
2264
2730
|
}
|
|
2265
|
-
var CHANGE_DEDUPE_MS;
|
|
2731
|
+
var CHANGE_DEDUPE_MS, SYMLINK_POLL_INTERVAL_MS, OVERLAYFS_RENAME_RETRY_MS;
|
|
2266
2732
|
var init_file_watcher = __esm({
|
|
2267
2733
|
"lib/setup/file-watcher.ts"() {
|
|
2268
2734
|
init_color();
|
|
2269
2735
|
CHANGE_DEDUPE_MS = 10;
|
|
2736
|
+
SYMLINK_POLL_INTERVAL_MS = 500;
|
|
2737
|
+
OVERLAYFS_RENAME_RETRY_MS = 50;
|
|
2270
2738
|
}
|
|
2271
2739
|
});
|
|
2272
2740
|
|
|
@@ -2383,7 +2851,11 @@ import fs12 from "node:fs/promises";
|
|
|
2383
2851
|
import { normalize } from "node:path";
|
|
2384
2852
|
import { availableParallelism } from "node:os";
|
|
2385
2853
|
async function run(config) {
|
|
2386
|
-
const
|
|
2854
|
+
const browserPromise = config.watch ? null : launchBrowser(config);
|
|
2855
|
+
const [cachedContent, timings] = await Promise.all([
|
|
2856
|
+
buildCachedContent(config, config.htmlPaths),
|
|
2857
|
+
config.watch ? Promise.resolve(null) : readTimingCache(config.projectRoot)
|
|
2858
|
+
]);
|
|
2387
2859
|
if (config.watch) {
|
|
2388
2860
|
const preBuildPromise = buildTestBundle(config, cachedContent);
|
|
2389
2861
|
preBuildPromise.catch(() => {
|
|
@@ -2393,7 +2865,7 @@ async function run(config) {
|
|
|
2393
2865
|
setupBrowser(config, cachedContent),
|
|
2394
2866
|
writeOutputStaticFiles(config, cachedContent)
|
|
2395
2867
|
]);
|
|
2396
|
-
config.
|
|
2868
|
+
config.webServer = connections.server;
|
|
2397
2869
|
setupKeyboardEvents(config, cachedContent, connections);
|
|
2398
2870
|
const isHeadedWatchMode = config.open === true && config.watch;
|
|
2399
2871
|
if (config.open && !isHeadedWatchMode) {
|
|
@@ -2412,7 +2884,10 @@ async function run(config) {
|
|
|
2412
2884
|
throw error;
|
|
2413
2885
|
}
|
|
2414
2886
|
if (isHeadedWatchMode && (cachedContent._buildError || cachedContent._noTestsWarning)) {
|
|
2415
|
-
await connections.page.goto(`http://localhost:${config.port}/`, {
|
|
2887
|
+
await connections.page.goto(`http://localhost:${config.port}/`, {
|
|
2888
|
+
waitUntil: "commit",
|
|
2889
|
+
timeout: WATCH_NAV_TIMEOUT_MS
|
|
2890
|
+
}).catch(() => {
|
|
2416
2891
|
});
|
|
2417
2892
|
}
|
|
2418
2893
|
if (config.watch) {
|
|
@@ -2429,6 +2904,10 @@ async function run(config) {
|
|
|
2429
2904
|
`# Rerun triggered: ${event} \u2192 ${file.replace(`${config.projectRoot}/`, "")}`
|
|
2430
2905
|
);
|
|
2431
2906
|
}
|
|
2907
|
+
const rebuildPromise = buildTestBundle(config, cachedContent);
|
|
2908
|
+
rebuildPromise.catch(() => {
|
|
2909
|
+
});
|
|
2910
|
+
cachedContent._preBuildPromise = rebuildPromise;
|
|
2432
2911
|
return await runTestsInBrowser(config, cachedContent, connections);
|
|
2433
2912
|
}
|
|
2434
2913
|
if (config.debug) {
|
|
@@ -2441,7 +2920,10 @@ async function run(config) {
|
|
|
2441
2920
|
async (_path, _event) => {
|
|
2442
2921
|
connections.server.publish("refresh");
|
|
2443
2922
|
if (isHeadedWatchMode && (cachedContent._buildError || cachedContent._noTestsWarning)) {
|
|
2444
|
-
await connections.page.goto(`http://localhost:${config.port}/`, {
|
|
2923
|
+
await connections.page.goto(`http://localhost:${config.port}/`, {
|
|
2924
|
+
waitUntil: "commit",
|
|
2925
|
+
timeout: WATCH_NAV_TIMEOUT_MS
|
|
2926
|
+
}).catch(() => {
|
|
2445
2927
|
});
|
|
2446
2928
|
}
|
|
2447
2929
|
}
|
|
@@ -2452,8 +2934,7 @@ async function run(config) {
|
|
|
2452
2934
|
} else {
|
|
2453
2935
|
const allFiles = Object.keys(config.fsTree);
|
|
2454
2936
|
const groupCount = Math.min(allFiles.length, availableParallelism());
|
|
2455
|
-
const
|
|
2456
|
-
const { groups, weights } = await splitIntoGroups(allFiles, groupCount, timings);
|
|
2937
|
+
const { groups, weights } = await splitIntoGroups(allFiles, groupCount, timings ?? {});
|
|
2457
2938
|
config.COUNTER = {
|
|
2458
2939
|
testCount: 0,
|
|
2459
2940
|
failCount: 0,
|
|
@@ -2472,21 +2953,32 @@ async function run(config) {
|
|
|
2472
2953
|
_phase: "bundling"
|
|
2473
2954
|
}));
|
|
2474
2955
|
const groupCachedContents = groups.map(() => ({ ...cachedContent }));
|
|
2956
|
+
const sharedServer = groupCount > 1 && cachedContent.htmlPathsToRunTests[0] === "/" && cachedContent.htmlPathsToRunTests.length === 1 ? (() => {
|
|
2957
|
+
const s = new HTTPServer();
|
|
2958
|
+
setupGroupWSHandler(s, groupConfigs);
|
|
2959
|
+
groupConfigs.forEach((gc, i) => registerGroupRoutes(s, gc, groupCachedContents[i], i));
|
|
2960
|
+
registerSharedStaticHandler(s, groupConfigs);
|
|
2961
|
+
return s;
|
|
2962
|
+
})() : null;
|
|
2475
2963
|
process.stdout.write("TAP version 13\n");
|
|
2476
2964
|
process.stdout.write(
|
|
2477
2965
|
`# Running ${allFiles.length} test file${allFiles.length === 1 ? "" : "s"} across ${groupCount} group${groupCount === 1 ? "" : "s"}
|
|
2478
2966
|
`
|
|
2479
2967
|
);
|
|
2480
2968
|
const [browser] = await Promise.all([
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
groupConfigs.
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2969
|
+
browserPromise,
|
|
2970
|
+
sharedServer ? bindServerToPort(sharedServer, config).then(
|
|
2971
|
+
() => groupConfigs.forEach((gc, i) => {
|
|
2972
|
+
gc.port = config.port;
|
|
2973
|
+
groupCachedContents[i].htmlPathsToRunTests = [`/group-${i}/`];
|
|
2974
|
+
})
|
|
2975
|
+
) : Promise.resolve(),
|
|
2976
|
+
Promise.all([
|
|
2977
|
+
groupCount > 1 ? buildAllGroupBundles(groupConfigs, groupCachedContents) : buildTestBundle(groupConfigs[0], groupCachedContents[0]),
|
|
2978
|
+
Promise.all(
|
|
2979
|
+
groupConfigs.map((gc, i) => writeOutputStaticFiles(gc, groupCachedContents[i]))
|
|
2488
2980
|
)
|
|
2489
|
-
)
|
|
2981
|
+
])
|
|
2490
2982
|
]);
|
|
2491
2983
|
if (config.open) {
|
|
2492
2984
|
void openOutputInBrowser(config);
|
|
@@ -2495,7 +2987,7 @@ async function run(config) {
|
|
|
2495
2987
|
const wallTimes = /* @__PURE__ */ new Map();
|
|
2496
2988
|
const GROUP_TIMEOUT_MS = 3 * 60 * 1e3;
|
|
2497
2989
|
const keepAlive = setInterval(() => {
|
|
2498
|
-
},
|
|
2990
|
+
}, KEEP_ALIVE_INTERVAL_MS);
|
|
2499
2991
|
const groupResults = await Promise.allSettled(
|
|
2500
2992
|
groupConfigs.map((groupConfig, i) => {
|
|
2501
2993
|
const groupTimeout = new Promise((_, reject) => {
|
|
@@ -2515,8 +3007,13 @@ async function run(config) {
|
|
|
2515
3007
|
const startMs = Date.now();
|
|
2516
3008
|
const work = (async () => {
|
|
2517
3009
|
groupConfig._phase = "connecting";
|
|
2518
|
-
const connections = await setupBrowser(
|
|
2519
|
-
|
|
3010
|
+
const connections = await setupBrowser(
|
|
3011
|
+
groupConfig,
|
|
3012
|
+
groupCachedContents[i],
|
|
3013
|
+
browser,
|
|
3014
|
+
sharedServer
|
|
3015
|
+
);
|
|
3016
|
+
groupConfig.webServer = connections.server;
|
|
2520
3017
|
if (config.before) {
|
|
2521
3018
|
await runUserModule(`${process.cwd()}/${config.before}`, groupConfig, "before");
|
|
2522
3019
|
}
|
|
@@ -2525,13 +3022,13 @@ async function run(config) {
|
|
|
2525
3022
|
} finally {
|
|
2526
3023
|
await flushConsoleHandlers(groupConfig._pendingConsoleHandlers);
|
|
2527
3024
|
await Promise.all([
|
|
2528
|
-
|
|
3025
|
+
!sharedServer && connections.server?.close(),
|
|
2529
3026
|
connections.page && // Unref'd: the keepAlive interval above holds the event loop open, so this
|
|
2530
3027
|
// timer still fires if page.close() hangs, without preventing process exit later.
|
|
2531
3028
|
Promise.race([
|
|
2532
3029
|
connections.page.close(),
|
|
2533
3030
|
new Promise((resolve) => {
|
|
2534
|
-
const pageCloseTimeoutId = setTimeout(resolve,
|
|
3031
|
+
const pageCloseTimeoutId = setTimeout(resolve, PAGE_CLOSE_GRACE_MS);
|
|
2535
3032
|
pageCloseTimeoutId.unref();
|
|
2536
3033
|
})
|
|
2537
3034
|
]).catch(() => {
|
|
@@ -2561,20 +3058,30 @@ async function run(config) {
|
|
|
2561
3058
|
}
|
|
2562
3059
|
TAPDisplayFinalResult(config.COUNTER, TIME_COUNTER.stop());
|
|
2563
3060
|
const fileTimes = computeFileTimes(groups, weights, wallTimes);
|
|
2564
|
-
persistTimings(fileTimes, config.projectRoot).catch(
|
|
2565
|
-
|
|
3061
|
+
persistTimings(fileTimes, config.projectRoot).catch(
|
|
3062
|
+
(err) => config.debug && process.stderr.write(`# [qunitx] persistTimings: ${err.message}
|
|
3063
|
+
`)
|
|
3064
|
+
);
|
|
2566
3065
|
printFileTimings(fileTimes, config.projectRoot);
|
|
2567
3066
|
if (config.after) {
|
|
2568
3067
|
await runUserModule(`${process.cwd()}/${config.after}`, config.COUNTER, "after");
|
|
2569
3068
|
}
|
|
2570
|
-
const exitTimer = setTimeout(() => process.exit(exitCode),
|
|
3069
|
+
const exitTimer = setTimeout(() => process.exit(exitCode), STDOUT_FLUSH_GRACE_MS);
|
|
2571
3070
|
exitTimer.unref();
|
|
2572
3071
|
process.stdout.write("\n", async () => {
|
|
2573
3072
|
clearTimeout(exitTimer);
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
3073
|
+
await Promise.all([
|
|
3074
|
+
sharedServer?.close().catch(
|
|
3075
|
+
(err) => config.debug && process.stderr.write(`# [qunitx] server.close: ${err.message}
|
|
3076
|
+
`)
|
|
3077
|
+
),
|
|
3078
|
+
browser.close().catch(
|
|
3079
|
+
(err) => config.debug && process.stderr.write(`# [qunitx] browser.close: ${err.message}
|
|
3080
|
+
`)
|
|
3081
|
+
)
|
|
3082
|
+
]);
|
|
2577
3083
|
await shutdownPrelaunch();
|
|
3084
|
+
clearInterval(keepAlive);
|
|
2578
3085
|
process.exit(exitCode);
|
|
2579
3086
|
});
|
|
2580
3087
|
}
|
|
@@ -2670,7 +3177,7 @@ ${lines.join("\n")}
|
|
|
2670
3177
|
async function splitIntoGroups(files, groupCount, timings) {
|
|
2671
3178
|
const sizes = await Promise.all(
|
|
2672
3179
|
files.map(
|
|
2673
|
-
(f) => fs12.stat(f).then((s) => s.size).catch(() => 0)
|
|
3180
|
+
(f) => timings[f] > 0 ? Promise.resolve(0) : fs12.stat(f).then((s) => s.size).catch(() => 0)
|
|
2674
3181
|
)
|
|
2675
3182
|
);
|
|
2676
3183
|
const knownRates = files.map((f, i) => ({ ms: timings[f], size: sizes[i] })).filter(({ ms, size }) => ms > 0 && size > 0);
|
|
@@ -2703,10 +3210,14 @@ function normalizeInternalAssetPathFromHTML(projectRoot, assetPath, htmlPath) {
|
|
|
2703
3210
|
const currentDirectory = htmlPath ? htmlPath.split("/").slice(0, -1).join("/") : projectRoot;
|
|
2704
3211
|
return assetPath.startsWith("./") ? normalize(`${currentDirectory}/${assetPath.slice(2)}`) : normalize(`${currentDirectory}/${assetPath}`);
|
|
2705
3212
|
}
|
|
3213
|
+
var WATCH_NAV_TIMEOUT_MS, PAGE_CLOSE_GRACE_MS, STDOUT_FLUSH_GRACE_MS, KEEP_ALIVE_INTERVAL_MS;
|
|
2706
3214
|
var init_run = __esm({
|
|
2707
3215
|
"lib/commands/run.ts"() {
|
|
2708
3216
|
init_browser();
|
|
2709
3217
|
init_chrome_prelaunch();
|
|
3218
|
+
init_http();
|
|
3219
|
+
init_bind_server_to_port();
|
|
3220
|
+
init_web_server();
|
|
2710
3221
|
init_open_output_in_browser();
|
|
2711
3222
|
init_color();
|
|
2712
3223
|
init_tests_in_browser();
|
|
@@ -2719,6 +3230,10 @@ var init_run = __esm({
|
|
|
2719
3230
|
init_display_final_result();
|
|
2720
3231
|
init_read_template();
|
|
2721
3232
|
init_html();
|
|
3233
|
+
WATCH_NAV_TIMEOUT_MS = 5e3;
|
|
3234
|
+
PAGE_CLOSE_GRACE_MS = 1e4;
|
|
3235
|
+
STDOUT_FLUSH_GRACE_MS = 5e3;
|
|
3236
|
+
KEEP_ALIVE_INTERVAL_MS = 1e4;
|
|
2722
3237
|
}
|
|
2723
3238
|
});
|
|
2724
3239
|
|
|
@@ -2733,7 +3248,7 @@ init_color();
|
|
|
2733
3248
|
var package_default = {
|
|
2734
3249
|
name: "qunitx-cli",
|
|
2735
3250
|
type: "module",
|
|
2736
|
-
version: "0.
|
|
3251
|
+
version: "0.20.0",
|
|
2737
3252
|
description: "Browser runner for QUnitx: run your qunitx tests in google-chrome",
|
|
2738
3253
|
author: "Izel Nakri",
|
|
2739
3254
|
license: "MIT",
|
|
@@ -2788,8 +3303,6 @@ var package_default = {
|
|
|
2788
3303
|
ws: "^8.20.0"
|
|
2789
3304
|
},
|
|
2790
3305
|
devDependencies: {
|
|
2791
|
-
cors: "^2.8.6",
|
|
2792
|
-
express: "^5.2.1",
|
|
2793
3306
|
"js-yaml": "^4.1.1",
|
|
2794
3307
|
prettier: "^3.8.2",
|
|
2795
3308
|
qunitx: "^1.2.7",
|
|
@@ -3099,6 +3612,7 @@ function buildGlobFormat(path7) {
|
|
|
3099
3612
|
}
|
|
3100
3613
|
|
|
3101
3614
|
// lib/utils/parse-cli-flags.ts
|
|
3615
|
+
var FALLBACK_TIMEOUT_MS = 1e4;
|
|
3102
3616
|
function parseCliFlags(projectRoot) {
|
|
3103
3617
|
const providedFlags = process.argv.slice(2).reduce(
|
|
3104
3618
|
(result, arg) => {
|
|
@@ -3113,7 +3627,7 @@ function parseCliFlags(projectRoot) {
|
|
|
3113
3627
|
} else if (arg.startsWith("--failfast") || arg.startsWith("--failFast")) {
|
|
3114
3628
|
return Object.assign(result, { failFast: parseBoolean(arg.split("=")[1]) });
|
|
3115
3629
|
} else if (arg.startsWith("--timeout")) {
|
|
3116
|
-
return Object.assign(result, { timeout: Number(arg.split("=")[1]) ||
|
|
3630
|
+
return Object.assign(result, { timeout: Number(arg.split("=")[1]) || FALLBACK_TIMEOUT_MS });
|
|
3117
3631
|
} else if (arg.startsWith("--output")) {
|
|
3118
3632
|
return Object.assign(result, { output: arg.split("=")[1] });
|
|
3119
3633
|
} else if (arg.endsWith(".html")) {
|
|
@@ -3238,6 +3752,7 @@ process4.title = "qunitx";
|
|
|
3238
3752
|
} catch (error) {
|
|
3239
3753
|
console.error(error);
|
|
3240
3754
|
process4.exitCode = 1;
|
|
3755
|
+
await shutdownPrelaunch();
|
|
3241
3756
|
process4.stdout.write("\n", () => process4.exit(1));
|
|
3242
3757
|
}
|
|
3243
3758
|
})();
|