qunitx-cli 0.19.3 → 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 +383 -100
- package/package.json +1 -3
package/dist/cli.js
CHANGED
|
@@ -92,12 +92,17 @@ async function cleanupBrowserDir(dirPath) {
|
|
|
92
92
|
}
|
|
93
93
|
const dirName = dirPath.split("/").pop();
|
|
94
94
|
await killAllReferencingProcesses(dirPath, dirName);
|
|
95
|
-
const deadline = Date.now() +
|
|
95
|
+
const deadline = Date.now() + CLEANUP_DEADLINE_MS;
|
|
96
96
|
while (Date.now() < deadline) {
|
|
97
|
-
const removed = await fs.rm(dirPath, { recursive: true, force: true }).then(
|
|
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);
|
|
98
103
|
if (removed) return;
|
|
99
104
|
await killAllReferencingProcesses(dirPath, dirName);
|
|
100
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
105
|
+
await new Promise((resolve) => setTimeout(resolve, CLEANUP_RETRY_MS));
|
|
101
106
|
}
|
|
102
107
|
if (!await fs.access(dirPath).then(() => true).catch(() => false))
|
|
103
108
|
return;
|
|
@@ -117,8 +122,11 @@ async function cleanupBrowserDir(dirPath) {
|
|
|
117
122
|
})
|
|
118
123
|
);
|
|
119
124
|
}
|
|
125
|
+
var CLEANUP_DEADLINE_MS, CLEANUP_RETRY_MS;
|
|
120
126
|
var init_cleanup_browser_dir = __esm({
|
|
121
127
|
"lib/utils/cleanup-browser-dir.ts"() {
|
|
128
|
+
CLEANUP_DEADLINE_MS = 5e3;
|
|
129
|
+
CLEANUP_RETRY_MS = 50;
|
|
122
130
|
}
|
|
123
131
|
});
|
|
124
132
|
|
|
@@ -472,10 +480,11 @@ function dumpYaml({
|
|
|
472
480
|
expected,
|
|
473
481
|
message,
|
|
474
482
|
stack,
|
|
483
|
+
source,
|
|
475
484
|
at
|
|
476
485
|
}) {
|
|
477
486
|
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) : "");
|
|
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) : "");
|
|
479
488
|
}
|
|
480
489
|
var NEEDS_QUOTING;
|
|
481
490
|
var init_dump_yaml = __esm({
|
|
@@ -498,8 +507,205 @@ var init_indent_string = __esm({
|
|
|
498
507
|
}
|
|
499
508
|
});
|
|
500
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
|
+
|
|
501
707
|
// lib/tap/display-test-result.ts
|
|
502
|
-
function TAPDisplayTestResult(COUNTER, details) {
|
|
708
|
+
function TAPDisplayTestResult(COUNTER, details, decoder, projectRoot) {
|
|
503
709
|
COUNTER.testCount++;
|
|
504
710
|
if (details.status === "skipped") {
|
|
505
711
|
COUNTER.skipCount++;
|
|
@@ -519,6 +725,19 @@ function TAPDisplayTestResult(COUNTER, details) {
|
|
|
519
725
|
if (!assertion.passed && assertion.todo === false) {
|
|
520
726
|
COUNTER.errorCount = (COUNTER.errorCount ?? 0) + 1;
|
|
521
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
|
+
}
|
|
522
741
|
process.stdout.write(
|
|
523
742
|
indentString(
|
|
524
743
|
dumpYaml({
|
|
@@ -526,10 +745,9 @@ function TAPDisplayTestResult(COUNTER, details) {
|
|
|
526
745
|
actual: assertion.actual !== null && typeof assertion.actual === "object" ? JSON.parse(JSON.stringify(assertion.actual, getCircularReplacer())) : assertion.actual,
|
|
527
746
|
expected: assertion.expected !== null && typeof assertion.expected === "object" ? JSON.parse(JSON.stringify(assertion.expected, getCircularReplacer())) : assertion.expected,
|
|
528
747
|
message: assertion.message || null,
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
at: extractStackAt(assertion.stack)
|
|
748
|
+
stack: stackStr,
|
|
749
|
+
source: sourceText,
|
|
750
|
+
at: atStr
|
|
533
751
|
}),
|
|
534
752
|
4
|
|
535
753
|
)
|
|
@@ -573,6 +791,7 @@ var init_display_test_result = __esm({
|
|
|
573
791
|
"lib/tap/display-test-result.ts"() {
|
|
574
792
|
init_dump_yaml();
|
|
575
793
|
init_indent_string();
|
|
794
|
+
init_source_map_decoder();
|
|
576
795
|
}
|
|
577
796
|
});
|
|
578
797
|
|
|
@@ -812,7 +1031,7 @@ var init_http = __esm({
|
|
|
812
1031
|
return true;
|
|
813
1032
|
}
|
|
814
1033
|
return false;
|
|
815
|
-
}) ||
|
|
1034
|
+
}) || null;
|
|
816
1035
|
}
|
|
817
1036
|
#matchPathSegments(path7, url) {
|
|
818
1037
|
const pathSegments = path7.split("/");
|
|
@@ -902,7 +1121,7 @@ function setupWebServer(config, cachedContent) {
|
|
|
902
1121
|
);
|
|
903
1122
|
}
|
|
904
1123
|
config._resetTestTimeout?.();
|
|
905
|
-
TAPDisplayTestResult(config.COUNTER, details);
|
|
1124
|
+
TAPDisplayTestResult(config.COUNTER, details, config._sourceMapDecoder, config.projectRoot);
|
|
906
1125
|
} else if (event === "done") {
|
|
907
1126
|
config._phase = "done";
|
|
908
1127
|
config._lastQUnitResult = qunitResult ?? null;
|
|
@@ -919,7 +1138,26 @@ function setupWebServer(config, cachedContent) {
|
|
|
919
1138
|
}
|
|
920
1139
|
});
|
|
921
1140
|
});
|
|
922
|
-
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
|
+
}
|
|
923
1161
|
const bytes = cachedContent.allTestCode?.length ?? null;
|
|
924
1162
|
config.debug && process.stdout.write(
|
|
925
1163
|
`# [HTTPServer] GET /tests.js \u2192 ${bytes !== null ? `${bytes} bytes` : "NOT READY (allTestCode is null)"}
|
|
@@ -927,10 +1165,9 @@ function setupWebServer(config, cachedContent) {
|
|
|
927
1165
|
);
|
|
928
1166
|
if (bytes === null) {
|
|
929
1167
|
res.writeHead(503, { "Content-Type": "application/javascript", "Cache-Control": "no-store" });
|
|
930
|
-
res.end(
|
|
1168
|
+
return void res.end(
|
|
931
1169
|
'console.error("[qunitx] /tests.js requested before bundle was built \u2014 allTestCode is null");'
|
|
932
1170
|
);
|
|
933
|
-
return;
|
|
934
1171
|
}
|
|
935
1172
|
config._onTestsJsServed?.();
|
|
936
1173
|
res.writeHead(200, {
|
|
@@ -948,10 +1185,9 @@ function setupWebServer(config, cachedContent) {
|
|
|
948
1185
|
);
|
|
949
1186
|
if (bytes === null) {
|
|
950
1187
|
res.writeHead(503, { "Content-Type": "application/javascript", "Cache-Control": "no-store" });
|
|
951
|
-
res.end(
|
|
1188
|
+
return res.end(
|
|
952
1189
|
'console.error("[qunitx] /filtered-tests.js requested before bundle was built \u2014 filteredTestCode is null");'
|
|
953
1190
|
);
|
|
954
|
-
return;
|
|
955
1191
|
}
|
|
956
1192
|
config._onTestsJsServed?.();
|
|
957
1193
|
res.writeHead(200, {
|
|
@@ -961,18 +1197,28 @@ function setupWebServer(config, cachedContent) {
|
|
|
961
1197
|
});
|
|
962
1198
|
res.end(cachedContent.filteredTestCode);
|
|
963
1199
|
});
|
|
964
|
-
server.get("/", (_req, res) => {
|
|
1200
|
+
server.get("/", async (_req, res) => {
|
|
1201
|
+
await cachedContent._activeRebuild?.catch(() => {
|
|
1202
|
+
});
|
|
965
1203
|
if (cachedContent._buildError) {
|
|
966
1204
|
const htmlContent = buildErrorHTML(cachedContent._buildError);
|
|
967
1205
|
res.writeHead(200, HTML_HEADERS);
|
|
968
1206
|
res.end(htmlContent);
|
|
969
|
-
|
|
970
|
-
|
|
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);
|
|
971
1218
|
}
|
|
972
1219
|
if (cachedContent._noTestsWarning) {
|
|
973
1220
|
res.writeHead(200, HTML_HEADERS);
|
|
974
|
-
res.end(buildNoTestsHTML(cachedContent._noTestsWarning));
|
|
975
|
-
return;
|
|
1221
|
+
return res.end(buildNoTestsHTML(cachedContent._noTestsWarning));
|
|
976
1222
|
}
|
|
977
1223
|
res.writeHead(200, HTML_HEADERS);
|
|
978
1224
|
res.end(mainIndexHTML);
|
|
@@ -983,13 +1229,11 @@ function setupWebServer(config, cachedContent) {
|
|
|
983
1229
|
const htmlContent = buildErrorHTML(cachedContent._buildError);
|
|
984
1230
|
res.writeHead(200, HTML_HEADERS);
|
|
985
1231
|
res.end(htmlContent);
|
|
986
|
-
saveHTML(`${config.projectRoot}/${config.output}/qunitx.html`, htmlContent);
|
|
987
|
-
return;
|
|
1232
|
+
return saveHTML(`${config.projectRoot}/${config.output}/qunitx.html`, htmlContent);
|
|
988
1233
|
}
|
|
989
1234
|
if (cachedContent._noTestsWarning) {
|
|
990
1235
|
res.writeHead(200, HTML_HEADERS);
|
|
991
|
-
res.end(buildNoTestsHTML(cachedContent._noTestsWarning));
|
|
992
|
-
return;
|
|
1236
|
+
return res.end(buildNoTestsHTML(cachedContent._noTestsWarning));
|
|
993
1237
|
}
|
|
994
1238
|
res.writeHead(200, HTML_HEADERS);
|
|
995
1239
|
res.end(mainQunitxHTML);
|
|
@@ -1043,11 +1287,6 @@ function replaceAssetPaths(html, htmlPath, projectRoot) {
|
|
|
1043
1287
|
function testRuntimeToInject(config, groupId) {
|
|
1044
1288
|
const groupIdPart = groupId !== void 0 ? `, groupId: ${groupId}` : "";
|
|
1045
1289
|
return `<script>
|
|
1046
|
-
window.testTimeout = 0;
|
|
1047
|
-
setInterval(() => {
|
|
1048
|
-
window.testTimeout = window.testTimeout + 1000;
|
|
1049
|
-
}, 1000);
|
|
1050
|
-
|
|
1051
1290
|
(function() {
|
|
1052
1291
|
// wsOpenStatus: true once the WebSocket 'open' event fires (or immediately for static files).
|
|
1053
1292
|
// testsLoaded: true once tests.js has executed and dispatched 'qunitx:tests-ready'.
|
|
@@ -1073,7 +1312,7 @@ function testRuntimeToInject(config, groupId) {
|
|
|
1073
1312
|
if (window.location.protocol === 'file:') return;
|
|
1074
1313
|
|
|
1075
1314
|
let wsRetryCount = 0;
|
|
1076
|
-
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
|
|
1077
1316
|
|
|
1078
1317
|
function setupWebSocket() {
|
|
1079
1318
|
try {
|
|
@@ -1112,10 +1351,9 @@ function testRuntimeToInject(config, groupId) {
|
|
|
1112
1351
|
wsRetryCount++;
|
|
1113
1352
|
if (wsRetryCount > WS_MAX_RETRIES) {
|
|
1114
1353
|
console.log('WebSocket connection failed after ' + WS_MAX_RETRIES + ' retries');
|
|
1115
|
-
window.testTimeout = ${config.timeout};
|
|
1116
1354
|
return;
|
|
1117
1355
|
}
|
|
1118
|
-
window.setTimeout(setupWebSocket,
|
|
1356
|
+
window.setTimeout(setupWebSocket, ${WS_RETRY_INTERVAL_MS});
|
|
1119
1357
|
}
|
|
1120
1358
|
|
|
1121
1359
|
setupWebSocket();
|
|
@@ -1149,8 +1387,6 @@ function testRuntimeToInject(config, groupId) {
|
|
|
1149
1387
|
// "no tests registered" warning (not a failure), so this gives a fast, clean result.
|
|
1150
1388
|
window.QUNIT_RESULT = { totalTests: 0, finishedTests: 0, failedTests: 0, currentTest: null };
|
|
1151
1389
|
window.socket.send(JSON.stringify({ event: 'done', details: { passed: 0, failed: 0, runtime: 0 } }));
|
|
1152
|
-
} else {
|
|
1153
|
-
window.testTimeout = ${config.timeout};
|
|
1154
1390
|
}
|
|
1155
1391
|
return;
|
|
1156
1392
|
}
|
|
@@ -1165,7 +1401,6 @@ function testRuntimeToInject(config, groupId) {
|
|
|
1165
1401
|
window.QUNIT_RESULT.currentTest = details.fullName.join(' | ');
|
|
1166
1402
|
});
|
|
1167
1403
|
window.QUnit.on('testEnd', (details) => { // NOTE: https://github.com/qunitjs/qunit/blob/master/src/html-reporter/diff.js
|
|
1168
|
-
window.testTimeout = 0;
|
|
1169
1404
|
window.QUNIT_RESULT.finishedTests++;
|
|
1170
1405
|
if (details.status === 'failed') window.QUNIT_RESULT.failedTests++;
|
|
1171
1406
|
window.QUNIT_RESULT.currentTest = null;
|
|
@@ -1182,16 +1417,10 @@ function testRuntimeToInject(config, groupId) {
|
|
|
1182
1417
|
window.QUnit.done((details) => {
|
|
1183
1418
|
if (navigator.webdriver) {
|
|
1184
1419
|
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
1420
|
}
|
|
1193
1421
|
});
|
|
1194
1422
|
|
|
1423
|
+
window.QUnit.config.testTimeout = ${config.timeout};
|
|
1195
1424
|
window.QUnit.start();
|
|
1196
1425
|
}
|
|
1197
1426
|
</script>`;
|
|
@@ -1292,8 +1521,8 @@ function buildNoTestsHTML(files) {
|
|
|
1292
1521
|
var retries = 0;
|
|
1293
1522
|
function connect() {
|
|
1294
1523
|
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++ <
|
|
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}); });
|
|
1297
1526
|
ws.addEventListener('error', function () { ws.close(); });
|
|
1298
1527
|
}
|
|
1299
1528
|
connect();
|
|
@@ -1394,8 +1623,8 @@ function buildErrorHTML(buildError) {
|
|
|
1394
1623
|
var retries = 0;
|
|
1395
1624
|
function connect() {
|
|
1396
1625
|
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++ <
|
|
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}); });
|
|
1399
1628
|
ws.addEventListener('error', function () { ws.close(); });
|
|
1400
1629
|
}
|
|
1401
1630
|
connect();
|
|
@@ -1424,13 +1653,11 @@ function registerGroupRoutes(server, groupConfig, groupCachedContent, groupId) {
|
|
|
1424
1653
|
server.get(`/group-${groupId}/`, (_req, res) => {
|
|
1425
1654
|
if (groupCachedContent._buildError) {
|
|
1426
1655
|
res.writeHead(200, HTML_HEADERS);
|
|
1427
|
-
res.end(buildErrorHTML(groupCachedContent._buildError));
|
|
1428
|
-
return;
|
|
1656
|
+
return res.end(buildErrorHTML(groupCachedContent._buildError));
|
|
1429
1657
|
}
|
|
1430
1658
|
if (groupCachedContent._noTestsWarning) {
|
|
1431
1659
|
res.writeHead(200, HTML_HEADERS);
|
|
1432
|
-
res.end(buildNoTestsHTML(groupCachedContent._noTestsWarning));
|
|
1433
|
-
return;
|
|
1660
|
+
return res.end(buildNoTestsHTML(groupCachedContent._noTestsWarning));
|
|
1434
1661
|
}
|
|
1435
1662
|
res.writeHead(200, HTML_HEADERS);
|
|
1436
1663
|
res.end(mainGroupHTML);
|
|
@@ -1440,10 +1667,9 @@ function registerGroupRoutes(server, groupConfig, groupCachedContent, groupId) {
|
|
|
1440
1667
|
const bytes = groupCachedContent.allTestCode?.length ?? null;
|
|
1441
1668
|
if (bytes === null) {
|
|
1442
1669
|
res.writeHead(503, { "Content-Type": "application/javascript", "Cache-Control": "no-store" });
|
|
1443
|
-
res.end(
|
|
1670
|
+
return res.end(
|
|
1444
1671
|
'console.error("[qunitx] /tests.js requested before bundle was built \u2014 allTestCode is null");'
|
|
1445
1672
|
);
|
|
1446
|
-
return;
|
|
1447
1673
|
}
|
|
1448
1674
|
groupConfig._onTestsJsServed?.();
|
|
1449
1675
|
res.writeHead(200, {
|
|
@@ -1495,7 +1721,7 @@ function setupGroupWSHandler(server, groupConfigs) {
|
|
|
1495
1721
|
);
|
|
1496
1722
|
}
|
|
1497
1723
|
config._resetTestTimeout?.();
|
|
1498
|
-
TAPDisplayTestResult(config.COUNTER, details);
|
|
1724
|
+
TAPDisplayTestResult(config.COUNTER, details, config._sourceMapDecoder, config.projectRoot);
|
|
1499
1725
|
} else if (event === "done") {
|
|
1500
1726
|
config._phase = "done";
|
|
1501
1727
|
config._lastQUnitResult = qunitResult ?? null;
|
|
@@ -1544,7 +1770,7 @@ function registerSharedStaticHandler(server, groupConfigs) {
|
|
|
1544
1770
|
});
|
|
1545
1771
|
});
|
|
1546
1772
|
}
|
|
1547
|
-
var fsPromise, HTML_HEADERS, NOT_FOUND_HTML;
|
|
1773
|
+
var fsPromise, HTML_HEADERS, WATCH_WS_RECONNECT_INTERVAL_MS, WATCH_WS_RECONNECT_MAX_RETRIES, WS_RETRY_INTERVAL_MS, NOT_FOUND_HTML;
|
|
1548
1774
|
var init_web_server = __esm({
|
|
1549
1775
|
"lib/setup/web-server.ts"() {
|
|
1550
1776
|
init_find_internal_assets_from_html();
|
|
@@ -1554,6 +1780,9 @@ var init_web_server = __esm({
|
|
|
1554
1780
|
init_http();
|
|
1555
1781
|
fsPromise = fs8.promises;
|
|
1556
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;
|
|
1557
1786
|
NOT_FOUND_HTML = `<!DOCTYPE html>
|
|
1558
1787
|
<html lang="en">
|
|
1559
1788
|
<head>
|
|
@@ -1637,12 +1866,9 @@ async function setupBrowser(config, cachedContent, existingBrowser = null, share
|
|
|
1637
1866
|
perfLog(`browser.js: newPage (shared server) took ${Date.now() - setupStart}ms`);
|
|
1638
1867
|
return [sharedServer, existingBrowser, newPage2];
|
|
1639
1868
|
}
|
|
1640
|
-
const
|
|
1641
|
-
setupWebServer(config, cachedContent),
|
|
1642
|
-
Promise.resolve(existingBrowser)
|
|
1643
|
-
]);
|
|
1869
|
+
const newServer = setupWebServer(config, cachedContent);
|
|
1644
1870
|
perfLog(`browser.js: setupWebServer took ${Date.now() - setupStart}ms`);
|
|
1645
|
-
const activeBrowser =
|
|
1871
|
+
const activeBrowser = existingBrowser ?? await launchBrowser(config);
|
|
1646
1872
|
const pageStart = Date.now();
|
|
1647
1873
|
const isHeadedWatchMode = config.open === true && config.watch;
|
|
1648
1874
|
const getPage = isHeadedWatchMode ? () => activeBrowser.contexts()[0]?.pages()[0] ?? activeBrowser.newPage() : () => activeBrowser.newPage();
|
|
@@ -1836,7 +2062,7 @@ async function buildTestBundle(config, cachedContent) {
|
|
|
1836
2062
|
}
|
|
1837
2063
|
const outfile = `${projectRoot}/${output}/tests.js`;
|
|
1838
2064
|
await fs9.mkdir(`${projectRoot}/${output}`, { recursive: true });
|
|
1839
|
-
const sourcemap =
|
|
2065
|
+
const sourcemap = "inline";
|
|
1840
2066
|
const needsDisk = true;
|
|
1841
2067
|
const buildOptions = {
|
|
1842
2068
|
stdin: {
|
|
@@ -1876,6 +2102,7 @@ async function buildTestBundle(config, cachedContent) {
|
|
|
1876
2102
|
)
|
|
1877
2103
|
]);
|
|
1878
2104
|
cachedContent.allTestCode = allTestCode;
|
|
2105
|
+
config._sourceMapDecoder = extractInlineSourceMap(allTestCode, `${projectRoot}/${output}`);
|
|
1879
2106
|
} catch (error) {
|
|
1880
2107
|
cachedContent._buildError = {
|
|
1881
2108
|
type: deriveBuildErrorType(error),
|
|
@@ -1907,9 +2134,13 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
1907
2134
|
const preBuildPromise = cachedContent._preBuildPromise;
|
|
1908
2135
|
cachedContent._preBuildPromise = null;
|
|
1909
2136
|
if (!cachedContent.allTestCode) {
|
|
1910
|
-
|
|
2137
|
+
if (preBuildPromise) {
|
|
2138
|
+
cachedContent._activeRebuild = preBuildPromise;
|
|
2139
|
+
} else {
|
|
2140
|
+
await buildTestBundle(config, cachedContent);
|
|
2141
|
+
}
|
|
1911
2142
|
}
|
|
1912
|
-
if (!cachedContent.allTestCode) {
|
|
2143
|
+
if (!cachedContent.allTestCode && !cachedContent._activeRebuild) {
|
|
1913
2144
|
return connections;
|
|
1914
2145
|
}
|
|
1915
2146
|
if (runHasFilter) {
|
|
@@ -1919,6 +2150,10 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
1919
2150
|
outputPath,
|
|
1920
2151
|
config
|
|
1921
2152
|
);
|
|
2153
|
+
config._sourceMapDecoder = extractInlineSourceMap(
|
|
2154
|
+
cachedContent.filteredTestCode,
|
|
2155
|
+
`${projectRoot}/${output}`
|
|
2156
|
+
);
|
|
1922
2157
|
}
|
|
1923
2158
|
const TIME_COUNTER = timeCounter();
|
|
1924
2159
|
if (runHasFilter) {
|
|
@@ -1930,6 +2165,17 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
1930
2165
|
)
|
|
1931
2166
|
);
|
|
1932
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
|
+
}
|
|
1933
2179
|
const TIME_TAKEN = TIME_COUNTER.stop();
|
|
1934
2180
|
if (!config._groupMode) {
|
|
1935
2181
|
if (config.COUNTER.testCount === 0 && !cachedContent._buildError) {
|
|
@@ -1961,6 +2207,7 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
1961
2207
|
}
|
|
1962
2208
|
}
|
|
1963
2209
|
} catch (error) {
|
|
2210
|
+
cachedContent._activeRebuild = null;
|
|
1964
2211
|
config.lastFailedTestFiles = config.lastRanTestFiles;
|
|
1965
2212
|
const exception = new BundleError(error);
|
|
1966
2213
|
if (!cachedContent._buildError && error.errors?.length) {
|
|
@@ -1985,8 +2232,8 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
1985
2232
|
return connections;
|
|
1986
2233
|
}
|
|
1987
2234
|
function buildFilteredTests(filteredTests, outputPath, config) {
|
|
1988
|
-
const sourcemap =
|
|
1989
|
-
const needsDisk =
|
|
2235
|
+
const sourcemap = "inline";
|
|
2236
|
+
const needsDisk = Boolean(config.open);
|
|
1990
2237
|
return buildWithOverlayfsRetry(
|
|
1991
2238
|
{
|
|
1992
2239
|
stdin: {
|
|
@@ -2060,9 +2307,9 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
|
|
|
2060
2307
|
let wsConnected = false;
|
|
2061
2308
|
try {
|
|
2062
2309
|
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 *
|
|
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);
|
|
2066
2313
|
let resolveTestRace;
|
|
2067
2314
|
const testRaceResult = new Promise((resolve) => {
|
|
2068
2315
|
resolveTestRace = resolve;
|
|
@@ -2080,7 +2327,7 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
|
|
|
2080
2327
|
config._resetTestTimeout = () => {
|
|
2081
2328
|
wsConnected = true;
|
|
2082
2329
|
clearTimeout(timeoutHandle);
|
|
2083
|
-
timeoutHandle = setTimeout(resolveTestRace, config.timeout);
|
|
2330
|
+
timeoutHandle = setTimeout(resolveTestRace, config.timeout + TEST_STALL_BUFFER_MS);
|
|
2084
2331
|
};
|
|
2085
2332
|
const targetUrl = `http://localhost:${config.port}${filePath}`;
|
|
2086
2333
|
const navOptions = { timeout: navMs, waitUntil: "commit" };
|
|
@@ -2150,7 +2397,7 @@ async function failOnNonWatchMode(watchMode = false, connections = {}, groupMode
|
|
|
2150
2397
|
process.exit(1);
|
|
2151
2398
|
}
|
|
2152
2399
|
}
|
|
2153
|
-
async function flushConsoleHandlers(handlers, deadline = Date.now() +
|
|
2400
|
+
async function flushConsoleHandlers(handlers, deadline = Date.now() + CONSOLE_FLUSH_TIMEOUT_MS) {
|
|
2154
2401
|
if (!handlers || handlers.size === 0 || Date.now() >= deadline) return;
|
|
2155
2402
|
await Promise.allSettled([...handlers]);
|
|
2156
2403
|
return flushConsoleHandlers(handlers, deadline);
|
|
@@ -2160,7 +2407,7 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
|
2160
2407
|
cachedContent._buildError = null;
|
|
2161
2408
|
cachedContent._noTestsWarning = null;
|
|
2162
2409
|
});
|
|
2163
|
-
const { projectRoot, debug,
|
|
2410
|
+
const { projectRoot, debug, browser } = groupConfigs[0];
|
|
2164
2411
|
const activeGroups = groupConfigs.reduce(
|
|
2165
2412
|
(acc, groupConfig, groupIndex) => {
|
|
2166
2413
|
const files = Object.keys(groupConfig.fsTree);
|
|
@@ -2184,7 +2431,7 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
|
2184
2431
|
(group) => fs9.mkdir(`${group.config.projectRoot}/${group.config.output}`, { recursive: true })
|
|
2185
2432
|
)
|
|
2186
2433
|
);
|
|
2187
|
-
const sourcemap =
|
|
2434
|
+
const sourcemap = "inline";
|
|
2188
2435
|
const groupEntryPlugin = {
|
|
2189
2436
|
name: "group-entry-loader",
|
|
2190
2437
|
setup(build) {
|
|
@@ -2201,6 +2448,7 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
|
2201
2448
|
});
|
|
2202
2449
|
}
|
|
2203
2450
|
};
|
|
2451
|
+
const esbuildOutdir = path5.join(projectRoot, "tmp");
|
|
2204
2452
|
const buildOptions = {
|
|
2205
2453
|
entryPoints: activeGroups.map((_, slotIndex) => ({
|
|
2206
2454
|
in: `group-entry-${slotIndex}`,
|
|
@@ -2212,7 +2460,7 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
|
2212
2460
|
logLevel: "silent",
|
|
2213
2461
|
// outdir only labels the paths in outputFiles[].path — nothing is written to disk
|
|
2214
2462
|
// (write:false). Use projectRoot/tmp as a stable sentinel; mkdir is not required.
|
|
2215
|
-
outdir:
|
|
2463
|
+
outdir: esbuildOutdir,
|
|
2216
2464
|
keepNames: true,
|
|
2217
2465
|
legalComments: "none",
|
|
2218
2466
|
target: esbuildTarget(browser),
|
|
@@ -2239,7 +2487,13 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
|
2239
2487
|
const isMap = Boolean(match[2]);
|
|
2240
2488
|
const { config, cachedContent } = activeGroups[slotIndex];
|
|
2241
2489
|
const destPath = `${config.projectRoot}/${config.output}/tests.js${isMap ? ".map" : ""}`;
|
|
2242
|
-
if (!isMap)
|
|
2490
|
+
if (!isMap) {
|
|
2491
|
+
cachedContent.allTestCode = Buffer.from(outputFile.contents);
|
|
2492
|
+
config._sourceMapDecoder = extractInlineSourceMap(
|
|
2493
|
+
cachedContent.allTestCode,
|
|
2494
|
+
esbuildOutdir
|
|
2495
|
+
);
|
|
2496
|
+
}
|
|
2243
2497
|
return fs9.writeFile(destPath, outputFile.contents);
|
|
2244
2498
|
})
|
|
2245
2499
|
);
|
|
@@ -2258,7 +2512,7 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
|
2258
2512
|
throw error;
|
|
2259
2513
|
}
|
|
2260
2514
|
}
|
|
2261
|
-
var BundleError, ancestorNodeModules, ANCESTOR_NODE_MODULES, RETRY_DELAY_MS, MAX_RETRIES, EMPTY_BUNDLE_THRESHOLD, GROUP_OUTPUT_REGEX;
|
|
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;
|
|
2262
2516
|
var init_tests_in_browser = __esm({
|
|
2263
2517
|
"lib/commands/run/tests-in-browser.ts"() {
|
|
2264
2518
|
init_color();
|
|
@@ -2267,6 +2521,7 @@ var init_tests_in_browser = __esm({
|
|
|
2267
2521
|
init_run_user_module();
|
|
2268
2522
|
init_display_final_result();
|
|
2269
2523
|
init_web_server();
|
|
2524
|
+
init_source_map_decoder();
|
|
2270
2525
|
BundleError = class extends Error {
|
|
2271
2526
|
constructor(message) {
|
|
2272
2527
|
super(message);
|
|
@@ -2281,6 +2536,11 @@ var init_tests_in_browser = __esm({
|
|
|
2281
2536
|
RETRY_DELAY_MS = 100;
|
|
2282
2537
|
MAX_RETRIES = 3;
|
|
2283
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;
|
|
2284
2544
|
GROUP_OUTPUT_REGEX = /group-(\d+)\.js(\.map)?$/;
|
|
2285
2545
|
}
|
|
2286
2546
|
});
|
|
@@ -2306,7 +2566,7 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
2306
2566
|
}
|
|
2307
2567
|
}
|
|
2308
2568
|
};
|
|
2309
|
-
fs10.watchFile(filePath, { interval:
|
|
2569
|
+
fs10.watchFile(filePath, { interval: SYMLINK_POLL_INTERVAL_MS, persistent: false }, handler);
|
|
2310
2570
|
symlinkPollers.set(filePath, () => fs10.unwatchFile(filePath, handler));
|
|
2311
2571
|
}
|
|
2312
2572
|
function untrackSymlink(filePath) {
|
|
@@ -2320,13 +2580,17 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
2320
2580
|
if (!ready || !filename) return;
|
|
2321
2581
|
const fullPath = filename === path6.basename(watchPath) ? watchPath : path6.join(watchPath, filename);
|
|
2322
2582
|
if (eventType === "change") {
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
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 {
|
|
2328
2593
|
}
|
|
2329
|
-
lastChangeMs[fullPath] = now;
|
|
2330
2594
|
}
|
|
2331
2595
|
return handleWatchEvent(config, extensions, "change", fullPath, onEventFunc, onFinishFunc);
|
|
2332
2596
|
}
|
|
@@ -2396,7 +2660,7 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
2396
2660
|
};
|
|
2397
2661
|
}
|
|
2398
2662
|
async function classifyRenameEvent(fullPath, fsTree) {
|
|
2399
|
-
for (const delay of [0,
|
|
2663
|
+
for (const delay of [0, OVERLAYFS_RENAME_RETRY_MS]) {
|
|
2400
2664
|
if (delay) await new Promise((resolve) => setTimeout(resolve, delay));
|
|
2401
2665
|
try {
|
|
2402
2666
|
const statResult = await stat(fullPath);
|
|
@@ -2464,11 +2728,13 @@ function colorEvent(event) {
|
|
|
2464
2728
|
if (event === "add" || event === "addDir") return green("ADDED:");
|
|
2465
2729
|
return red("REMOVED:");
|
|
2466
2730
|
}
|
|
2467
|
-
var CHANGE_DEDUPE_MS;
|
|
2731
|
+
var CHANGE_DEDUPE_MS, SYMLINK_POLL_INTERVAL_MS, OVERLAYFS_RENAME_RETRY_MS;
|
|
2468
2732
|
var init_file_watcher = __esm({
|
|
2469
2733
|
"lib/setup/file-watcher.ts"() {
|
|
2470
2734
|
init_color();
|
|
2471
2735
|
CHANGE_DEDUPE_MS = 10;
|
|
2736
|
+
SYMLINK_POLL_INTERVAL_MS = 500;
|
|
2737
|
+
OVERLAYFS_RENAME_RETRY_MS = 50;
|
|
2472
2738
|
}
|
|
2473
2739
|
});
|
|
2474
2740
|
|
|
@@ -2585,7 +2851,11 @@ import fs12 from "node:fs/promises";
|
|
|
2585
2851
|
import { normalize } from "node:path";
|
|
2586
2852
|
import { availableParallelism } from "node:os";
|
|
2587
2853
|
async function run(config) {
|
|
2588
|
-
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
|
+
]);
|
|
2589
2859
|
if (config.watch) {
|
|
2590
2860
|
const preBuildPromise = buildTestBundle(config, cachedContent);
|
|
2591
2861
|
preBuildPromise.catch(() => {
|
|
@@ -2595,7 +2865,7 @@ async function run(config) {
|
|
|
2595
2865
|
setupBrowser(config, cachedContent),
|
|
2596
2866
|
writeOutputStaticFiles(config, cachedContent)
|
|
2597
2867
|
]);
|
|
2598
|
-
config.
|
|
2868
|
+
config.webServer = connections.server;
|
|
2599
2869
|
setupKeyboardEvents(config, cachedContent, connections);
|
|
2600
2870
|
const isHeadedWatchMode = config.open === true && config.watch;
|
|
2601
2871
|
if (config.open && !isHeadedWatchMode) {
|
|
@@ -2614,7 +2884,10 @@ async function run(config) {
|
|
|
2614
2884
|
throw error;
|
|
2615
2885
|
}
|
|
2616
2886
|
if (isHeadedWatchMode && (cachedContent._buildError || cachedContent._noTestsWarning)) {
|
|
2617
|
-
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(() => {
|
|
2618
2891
|
});
|
|
2619
2892
|
}
|
|
2620
2893
|
if (config.watch) {
|
|
@@ -2631,6 +2904,10 @@ async function run(config) {
|
|
|
2631
2904
|
`# Rerun triggered: ${event} \u2192 ${file.replace(`${config.projectRoot}/`, "")}`
|
|
2632
2905
|
);
|
|
2633
2906
|
}
|
|
2907
|
+
const rebuildPromise = buildTestBundle(config, cachedContent);
|
|
2908
|
+
rebuildPromise.catch(() => {
|
|
2909
|
+
});
|
|
2910
|
+
cachedContent._preBuildPromise = rebuildPromise;
|
|
2634
2911
|
return await runTestsInBrowser(config, cachedContent, connections);
|
|
2635
2912
|
}
|
|
2636
2913
|
if (config.debug) {
|
|
@@ -2643,7 +2920,10 @@ async function run(config) {
|
|
|
2643
2920
|
async (_path, _event) => {
|
|
2644
2921
|
connections.server.publish("refresh");
|
|
2645
2922
|
if (isHeadedWatchMode && (cachedContent._buildError || cachedContent._noTestsWarning)) {
|
|
2646
|
-
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(() => {
|
|
2647
2927
|
});
|
|
2648
2928
|
}
|
|
2649
2929
|
}
|
|
@@ -2654,8 +2934,7 @@ async function run(config) {
|
|
|
2654
2934
|
} else {
|
|
2655
2935
|
const allFiles = Object.keys(config.fsTree);
|
|
2656
2936
|
const groupCount = Math.min(allFiles.length, availableParallelism());
|
|
2657
|
-
const
|
|
2658
|
-
const { groups, weights } = await splitIntoGroups(allFiles, groupCount, timings);
|
|
2937
|
+
const { groups, weights } = await splitIntoGroups(allFiles, groupCount, timings ?? {});
|
|
2659
2938
|
config.COUNTER = {
|
|
2660
2939
|
testCount: 0,
|
|
2661
2940
|
failCount: 0,
|
|
@@ -2687,7 +2966,7 @@ async function run(config) {
|
|
|
2687
2966
|
`
|
|
2688
2967
|
);
|
|
2689
2968
|
const [browser] = await Promise.all([
|
|
2690
|
-
|
|
2969
|
+
browserPromise,
|
|
2691
2970
|
sharedServer ? bindServerToPort(sharedServer, config).then(
|
|
2692
2971
|
() => groupConfigs.forEach((gc, i) => {
|
|
2693
2972
|
gc.port = config.port;
|
|
@@ -2708,7 +2987,7 @@ async function run(config) {
|
|
|
2708
2987
|
const wallTimes = /* @__PURE__ */ new Map();
|
|
2709
2988
|
const GROUP_TIMEOUT_MS = 3 * 60 * 1e3;
|
|
2710
2989
|
const keepAlive = setInterval(() => {
|
|
2711
|
-
},
|
|
2990
|
+
}, KEEP_ALIVE_INTERVAL_MS);
|
|
2712
2991
|
const groupResults = await Promise.allSettled(
|
|
2713
2992
|
groupConfigs.map((groupConfig, i) => {
|
|
2714
2993
|
const groupTimeout = new Promise((_, reject) => {
|
|
@@ -2734,7 +3013,7 @@ async function run(config) {
|
|
|
2734
3013
|
browser,
|
|
2735
3014
|
sharedServer
|
|
2736
3015
|
);
|
|
2737
|
-
groupConfig.
|
|
3016
|
+
groupConfig.webServer = connections.server;
|
|
2738
3017
|
if (config.before) {
|
|
2739
3018
|
await runUserModule(`${process.cwd()}/${config.before}`, groupConfig, "before");
|
|
2740
3019
|
}
|
|
@@ -2749,7 +3028,7 @@ async function run(config) {
|
|
|
2749
3028
|
Promise.race([
|
|
2750
3029
|
connections.page.close(),
|
|
2751
3030
|
new Promise((resolve) => {
|
|
2752
|
-
const pageCloseTimeoutId = setTimeout(resolve,
|
|
3031
|
+
const pageCloseTimeoutId = setTimeout(resolve, PAGE_CLOSE_GRACE_MS);
|
|
2753
3032
|
pageCloseTimeoutId.unref();
|
|
2754
3033
|
})
|
|
2755
3034
|
]).catch(() => {
|
|
@@ -2787,11 +3066,10 @@ async function run(config) {
|
|
|
2787
3066
|
if (config.after) {
|
|
2788
3067
|
await runUserModule(`${process.cwd()}/${config.after}`, config.COUNTER, "after");
|
|
2789
3068
|
}
|
|
2790
|
-
const exitTimer = setTimeout(() => process.exit(exitCode),
|
|
3069
|
+
const exitTimer = setTimeout(() => process.exit(exitCode), STDOUT_FLUSH_GRACE_MS);
|
|
2791
3070
|
exitTimer.unref();
|
|
2792
3071
|
process.stdout.write("\n", async () => {
|
|
2793
3072
|
clearTimeout(exitTimer);
|
|
2794
|
-
clearInterval(keepAlive);
|
|
2795
3073
|
await Promise.all([
|
|
2796
3074
|
sharedServer?.close().catch(
|
|
2797
3075
|
(err) => config.debug && process.stderr.write(`# [qunitx] server.close: ${err.message}
|
|
@@ -2803,6 +3081,7 @@ async function run(config) {
|
|
|
2803
3081
|
)
|
|
2804
3082
|
]);
|
|
2805
3083
|
await shutdownPrelaunch();
|
|
3084
|
+
clearInterval(keepAlive);
|
|
2806
3085
|
process.exit(exitCode);
|
|
2807
3086
|
});
|
|
2808
3087
|
}
|
|
@@ -2931,6 +3210,7 @@ function normalizeInternalAssetPathFromHTML(projectRoot, assetPath, htmlPath) {
|
|
|
2931
3210
|
const currentDirectory = htmlPath ? htmlPath.split("/").slice(0, -1).join("/") : projectRoot;
|
|
2932
3211
|
return assetPath.startsWith("./") ? normalize(`${currentDirectory}/${assetPath.slice(2)}`) : normalize(`${currentDirectory}/${assetPath}`);
|
|
2933
3212
|
}
|
|
3213
|
+
var WATCH_NAV_TIMEOUT_MS, PAGE_CLOSE_GRACE_MS, STDOUT_FLUSH_GRACE_MS, KEEP_ALIVE_INTERVAL_MS;
|
|
2934
3214
|
var init_run = __esm({
|
|
2935
3215
|
"lib/commands/run.ts"() {
|
|
2936
3216
|
init_browser();
|
|
@@ -2950,6 +3230,10 @@ var init_run = __esm({
|
|
|
2950
3230
|
init_display_final_result();
|
|
2951
3231
|
init_read_template();
|
|
2952
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;
|
|
2953
3237
|
}
|
|
2954
3238
|
});
|
|
2955
3239
|
|
|
@@ -2964,7 +3248,7 @@ init_color();
|
|
|
2964
3248
|
var package_default = {
|
|
2965
3249
|
name: "qunitx-cli",
|
|
2966
3250
|
type: "module",
|
|
2967
|
-
version: "0.
|
|
3251
|
+
version: "0.20.0",
|
|
2968
3252
|
description: "Browser runner for QUnitx: run your qunitx tests in google-chrome",
|
|
2969
3253
|
author: "Izel Nakri",
|
|
2970
3254
|
license: "MIT",
|
|
@@ -3019,8 +3303,6 @@ var package_default = {
|
|
|
3019
3303
|
ws: "^8.20.0"
|
|
3020
3304
|
},
|
|
3021
3305
|
devDependencies: {
|
|
3022
|
-
cors: "^2.8.6",
|
|
3023
|
-
express: "^5.2.1",
|
|
3024
3306
|
"js-yaml": "^4.1.1",
|
|
3025
3307
|
prettier: "^3.8.2",
|
|
3026
3308
|
qunitx: "^1.2.7",
|
|
@@ -3330,6 +3612,7 @@ function buildGlobFormat(path7) {
|
|
|
3330
3612
|
}
|
|
3331
3613
|
|
|
3332
3614
|
// lib/utils/parse-cli-flags.ts
|
|
3615
|
+
var FALLBACK_TIMEOUT_MS = 1e4;
|
|
3333
3616
|
function parseCliFlags(projectRoot) {
|
|
3334
3617
|
const providedFlags = process.argv.slice(2).reduce(
|
|
3335
3618
|
(result, arg) => {
|
|
@@ -3344,7 +3627,7 @@ function parseCliFlags(projectRoot) {
|
|
|
3344
3627
|
} else if (arg.startsWith("--failfast") || arg.startsWith("--failFast")) {
|
|
3345
3628
|
return Object.assign(result, { failFast: parseBoolean(arg.split("=")[1]) });
|
|
3346
3629
|
} else if (arg.startsWith("--timeout")) {
|
|
3347
|
-
return Object.assign(result, { timeout: Number(arg.split("=")[1]) ||
|
|
3630
|
+
return Object.assign(result, { timeout: Number(arg.split("=")[1]) || FALLBACK_TIMEOUT_MS });
|
|
3348
3631
|
} else if (arg.startsWith("--output")) {
|
|
3349
3632
|
return Object.assign(result, { output: arg.split("=")[1] });
|
|
3350
3633
|
} else if (arg.endsWith(".html")) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qunitx-cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.20.0",
|
|
5
5
|
"description": "Browser runner for QUnitx: run your qunitx tests in google-chrome",
|
|
6
6
|
"author": "Izel Nakri",
|
|
7
7
|
"license": "MIT",
|
|
@@ -56,8 +56,6 @@
|
|
|
56
56
|
"ws": "^8.20.0"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
|
-
"cors": "^2.8.6",
|
|
60
|
-
"express": "^5.2.1",
|
|
61
59
|
"js-yaml": "^4.1.1",
|
|
62
60
|
"prettier": "^3.8.2",
|
|
63
61
|
"qunitx": "^1.2.7",
|