qunitx-cli 0.21.2 → 0.21.3
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 +651 -595
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -12,6 +12,9 @@ var __export = (target, all) => {
|
|
|
12
12
|
// lib/utils/find-chrome.ts
|
|
13
13
|
import { accessSync, constants } from "node:fs";
|
|
14
14
|
import { join } from "node:path";
|
|
15
|
+
function findChrome() {
|
|
16
|
+
return Promise.resolve(findChromeSync());
|
|
17
|
+
}
|
|
15
18
|
function findChromeSync() {
|
|
16
19
|
if (process.env.CHROME_BIN) return process.env.CHROME_BIN;
|
|
17
20
|
for (const dir of PATH_DIRS) {
|
|
@@ -26,9 +29,6 @@ function findChromeSync() {
|
|
|
26
29
|
}
|
|
27
30
|
return null;
|
|
28
31
|
}
|
|
29
|
-
function findChrome() {
|
|
30
|
-
return Promise.resolve(findChromeSync());
|
|
31
|
-
}
|
|
32
32
|
var CANDIDATES, PATH_DIRS;
|
|
33
33
|
var init_find_chrome = __esm({
|
|
34
34
|
"lib/utils/find-chrome.ts"() {
|
|
@@ -56,35 +56,6 @@ var init_kill_process_group = __esm({
|
|
|
56
56
|
|
|
57
57
|
// lib/utils/cleanup-browser-dir.ts
|
|
58
58
|
import fs from "node:fs/promises";
|
|
59
|
-
async function processReferencesDir(entry, dirPath, dirName) {
|
|
60
|
-
try {
|
|
61
|
-
const [cwd, cmdline] = await Promise.all([
|
|
62
|
-
fs.readlink(`/proc/${entry}/cwd`).catch(() => ""),
|
|
63
|
-
fs.readFile(`/proc/${entry}/cmdline`, "utf8").catch(() => "")
|
|
64
|
-
]);
|
|
65
|
-
if (cwd.startsWith(dirPath) || cmdline.includes(dirName)) return true;
|
|
66
|
-
const fds = await fs.readdir(`/proc/${entry}/fd`).catch(() => []);
|
|
67
|
-
const fdTargets = await Promise.all(
|
|
68
|
-
fds.map((fd) => fs.readlink(`/proc/${entry}/fd/${fd}`).catch(() => ""))
|
|
69
|
-
);
|
|
70
|
-
return fdTargets.some((target) => target.startsWith(dirPath));
|
|
71
|
-
} catch {
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
async function killAllReferencingProcesses(dirPath, dirName) {
|
|
76
|
-
const procEntries = await fs.readdir("/proc").catch(() => []);
|
|
77
|
-
await Promise.all(
|
|
78
|
-
procEntries.map(async (entry) => {
|
|
79
|
-
if (!/^\d+$/.test(entry)) return;
|
|
80
|
-
if (!await processReferencesDir(entry, dirPath, dirName)) return;
|
|
81
|
-
try {
|
|
82
|
-
process.kill(parseInt(entry), "SIGKILL");
|
|
83
|
-
} catch {
|
|
84
|
-
}
|
|
85
|
-
})
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
59
|
async function cleanupBrowserDir(dirPath) {
|
|
89
60
|
if (process.platform !== "linux") {
|
|
90
61
|
await fs.rm(dirPath, { recursive: true, force: true }).catch(() => {
|
|
@@ -123,6 +94,35 @@ async function cleanupBrowserDir(dirPath) {
|
|
|
123
94
|
})
|
|
124
95
|
);
|
|
125
96
|
}
|
|
97
|
+
async function processReferencesDir(entry, dirPath, dirName) {
|
|
98
|
+
try {
|
|
99
|
+
const [cwd, cmdline] = await Promise.all([
|
|
100
|
+
fs.readlink(`/proc/${entry}/cwd`).catch(() => ""),
|
|
101
|
+
fs.readFile(`/proc/${entry}/cmdline`, "utf8").catch(() => "")
|
|
102
|
+
]);
|
|
103
|
+
if (cwd.startsWith(dirPath) || cmdline.includes(dirName)) return true;
|
|
104
|
+
const fds = await fs.readdir(`/proc/${entry}/fd`).catch(() => []);
|
|
105
|
+
const fdTargets = await Promise.all(
|
|
106
|
+
fds.map((fd) => fs.readlink(`/proc/${entry}/fd/${fd}`).catch(() => ""))
|
|
107
|
+
);
|
|
108
|
+
return fdTargets.some((target) => target.startsWith(dirPath));
|
|
109
|
+
} catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function killAllReferencingProcesses(dirPath, dirName) {
|
|
114
|
+
const procEntries = await fs.readdir("/proc").catch(() => []);
|
|
115
|
+
await Promise.all(
|
|
116
|
+
procEntries.map(async (entry) => {
|
|
117
|
+
if (!/^\d+$/.test(entry)) return;
|
|
118
|
+
if (!await processReferencesDir(entry, dirPath, dirName)) return;
|
|
119
|
+
try {
|
|
120
|
+
process.kill(parseInt(entry), "SIGKILL");
|
|
121
|
+
} catch {
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
126
|
var CLEANUP_DEADLINE_MS, CLEANUP_RETRY_MS;
|
|
127
127
|
var init_cleanup_browser_dir = __esm({
|
|
128
128
|
"lib/utils/cleanup-browser-dir.ts"() {
|
|
@@ -447,6 +447,18 @@ var init_html = __esm({
|
|
|
447
447
|
});
|
|
448
448
|
|
|
449
449
|
// lib/tap/dump-yaml.ts
|
|
450
|
+
function dumpYaml({
|
|
451
|
+
name,
|
|
452
|
+
actual,
|
|
453
|
+
expected,
|
|
454
|
+
message,
|
|
455
|
+
stack,
|
|
456
|
+
source,
|
|
457
|
+
at
|
|
458
|
+
}) {
|
|
459
|
+
return `name: ${dumpString(name, "")}
|
|
460
|
+
` + 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) : "");
|
|
461
|
+
}
|
|
450
462
|
function needsQuoting(str) {
|
|
451
463
|
return NEEDS_QUOTING.test(str);
|
|
452
464
|
}
|
|
@@ -484,18 +496,6 @@ function yamlLine(key, value) {
|
|
|
484
496
|
` : `${key}: ${serialized}
|
|
485
497
|
`;
|
|
486
498
|
}
|
|
487
|
-
function dumpYaml({
|
|
488
|
-
name,
|
|
489
|
-
actual,
|
|
490
|
-
expected,
|
|
491
|
-
message,
|
|
492
|
-
stack,
|
|
493
|
-
source,
|
|
494
|
-
at
|
|
495
|
-
}) {
|
|
496
|
-
return `name: ${dumpString(name, "")}
|
|
497
|
-
` + yamlLine("actual", actual) + yamlLine("expected", expected) + (message !== null ? yamlLine("message", message) : "") + (stack !== null ? yamlLine("stack", stack) : "") + (source !== null ? yamlLine("source", source) : "") + (at !== null ? yamlLine("at", at) : "");
|
|
498
|
-
}
|
|
499
499
|
var NEEDS_QUOTING;
|
|
500
500
|
var init_dump_yaml = __esm({
|
|
501
501
|
"lib/tap/dump-yaml.ts"() {
|
|
@@ -518,43 +518,35 @@ var init_indent_string = __esm({
|
|
|
518
518
|
});
|
|
519
519
|
|
|
520
520
|
// lib/utils/source-map-decoder.ts
|
|
521
|
-
function readVLQ(s, pos) {
|
|
522
|
-
let accumulated = 0;
|
|
523
|
-
let shift = 0;
|
|
524
|
-
let digit;
|
|
525
|
-
do {
|
|
526
|
-
digit = BASE64_LOOKUP[s.charCodeAt(pos++)];
|
|
527
|
-
accumulated |= (digit & 31) << shift;
|
|
528
|
-
shift += 5;
|
|
529
|
-
} while (digit & 32);
|
|
530
|
-
return [accumulated & 1 ? -(accumulated >>> 1) : accumulated >>> 1, pos];
|
|
531
|
-
}
|
|
532
521
|
function decodeMappings(mappings) {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
522
|
+
const result = [];
|
|
523
|
+
const cursor = { position: 0 };
|
|
524
|
+
const mappingsLength = mappings.length;
|
|
525
|
+
let segments = [];
|
|
526
|
+
let generatedCol = 0, sourceIndex = 0, sourceLine = 0, sourceCol = 0;
|
|
527
|
+
for (; ; ) {
|
|
528
|
+
if (cursor.position >= mappingsLength) break;
|
|
529
|
+
const charCode = mappings.charCodeAt(cursor.position);
|
|
530
|
+
if (charCode !== COMMA && charCode !== SEMICOLON) {
|
|
531
|
+
generatedCol += readVlqAt(mappings, cursor);
|
|
532
|
+
if (atFieldStart(mappings, cursor.position)) {
|
|
533
|
+
sourceIndex += readVlqAt(mappings, cursor);
|
|
534
|
+
sourceLine += readVlqAt(mappings, cursor);
|
|
535
|
+
sourceCol += readVlqAt(mappings, cursor);
|
|
536
|
+
segments.push({ generatedCol, sourceIndex, sourceLine, sourceCol });
|
|
537
|
+
if (atFieldStart(mappings, cursor.position)) readVlqAt(mappings, cursor);
|
|
542
538
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
segments.push({ generatedCol, sourceIndex, sourceLine, sourceCol });
|
|
555
|
-
}
|
|
556
|
-
return segments;
|
|
557
|
-
});
|
|
539
|
+
} else if (charCode === COMMA) {
|
|
540
|
+
cursor.position++;
|
|
541
|
+
} else {
|
|
542
|
+
result.push(segments);
|
|
543
|
+
segments = [];
|
|
544
|
+
generatedCol = 0;
|
|
545
|
+
cursor.position++;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
result.push(segments);
|
|
549
|
+
return result;
|
|
558
550
|
}
|
|
559
551
|
function parseSourceMap(json, outDir) {
|
|
560
552
|
const map = JSON.parse(json);
|
|
@@ -566,151 +558,185 @@ function parseSourceMap(json, outDir) {
|
|
|
566
558
|
sourcesContent: map.sourcesContent ?? []
|
|
567
559
|
};
|
|
568
560
|
}
|
|
569
|
-
function base64DecodeUtf8(b64) {
|
|
570
|
-
const binary = atob(b64);
|
|
571
|
-
return UTF8.decode(Uint8Array.from(binary, (c) => c.charCodeAt(0)));
|
|
572
|
-
}
|
|
573
561
|
function extractInlineSourceMap(bundle, outDir) {
|
|
574
562
|
if (!bundle) return null;
|
|
575
|
-
const
|
|
576
|
-
|
|
577
|
-
/\/\/# sourceMappingURL=data:application\/json;base64,([A-Za-z0-9+/=]+)/
|
|
578
|
-
);
|
|
579
|
-
if (!match) return null;
|
|
563
|
+
const base64Payload = readMarkerPayload(bundle);
|
|
564
|
+
if (!base64Payload) return null;
|
|
580
565
|
try {
|
|
581
|
-
return parseSourceMap(
|
|
566
|
+
return parseSourceMap(decodeBase64Utf8(base64Payload), outDir);
|
|
582
567
|
} catch {
|
|
583
568
|
return null;
|
|
584
569
|
}
|
|
585
570
|
}
|
|
586
|
-
function normalizePosix(p) {
|
|
587
|
-
const abs = p.startsWith("/");
|
|
588
|
-
const parts = p.split("/");
|
|
589
|
-
const out = [];
|
|
590
|
-
for (const part of parts) {
|
|
591
|
-
if (part === "..") out.pop();
|
|
592
|
-
else if (part !== "" && part !== ".") out.push(part);
|
|
593
|
-
}
|
|
594
|
-
return (abs ? "/" : "") + out.join("/");
|
|
595
|
-
}
|
|
596
|
-
function posixResolve(base, relative) {
|
|
597
|
-
if (relative.startsWith("/")) return normalizePosix(relative);
|
|
598
|
-
return normalizePosix(base + "/" + relative);
|
|
599
|
-
}
|
|
600
|
-
function toAbsolutePath(raw, outDir, sourceRoot) {
|
|
601
|
-
if (raw.startsWith("file://")) return raw.slice(7);
|
|
602
|
-
if (raw.startsWith("/")) return raw;
|
|
603
|
-
const base = sourceRoot ? normalizePosix(outDir + "/" + sourceRoot) : outDir;
|
|
604
|
-
return posixResolve(base, raw);
|
|
605
|
-
}
|
|
606
571
|
function lookupPosition(decoder, generatedLine, generatedCol) {
|
|
607
572
|
const segments = decoder.segmentsByLine[generatedLine - 1];
|
|
608
573
|
if (!segments?.length) return null;
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
if (segments[mid].generatedCol <= col0) {
|
|
614
|
-
best = mid;
|
|
615
|
-
lo = mid + 1;
|
|
616
|
-
} else {
|
|
617
|
-
hi = mid - 1;
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
if (best === -1) return null;
|
|
621
|
-
const { sourceIndex, sourceLine, sourceCol } = segments[best];
|
|
622
|
-
const rawSource = decoder.sources[sourceIndex];
|
|
574
|
+
const targetCol = generatedCol - 1;
|
|
575
|
+
const segment = segments.findLast((s) => s.generatedCol <= targetCol);
|
|
576
|
+
if (!segment) return null;
|
|
577
|
+
const rawSource = decoder.sources[segment.sourceIndex];
|
|
623
578
|
if (!rawSource) return null;
|
|
624
|
-
const content = decoder.sourcesContent[sourceIndex];
|
|
625
|
-
const sourceText = content ? content.split("\n", sourceLine + 1)[sourceLine]?.trim() || null : null;
|
|
626
579
|
return {
|
|
627
580
|
absolutePath: toAbsolutePath(rawSource, decoder.outDir, decoder.sourceRoot),
|
|
628
|
-
line: sourceLine + 1,
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
// 0-based → 1-based
|
|
632
|
-
sourceText
|
|
581
|
+
line: segment.sourceLine + 1,
|
|
582
|
+
col: segment.sourceCol + 1,
|
|
583
|
+
sourceText: extractSourceLine(decoder.sourcesContent[segment.sourceIndex], segment.sourceLine)
|
|
633
584
|
};
|
|
634
585
|
}
|
|
635
|
-
function parseFrameLocation(
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
const colStr = s.slice(colSep + 1);
|
|
639
|
-
if (!/^\d+$/.test(colStr)) return null;
|
|
640
|
-
const lineSep = s.lastIndexOf(":", colSep - 1);
|
|
641
|
-
if (lineSep < 0) return null;
|
|
642
|
-
const lineStr = s.slice(lineSep + 1, colSep);
|
|
643
|
-
if (!/^\d+$/.test(lineStr)) return null;
|
|
644
|
-
return { url: s.slice(0, lineSep), line: +lineStr, col: +colStr };
|
|
586
|
+
function parseFrameLocation(text) {
|
|
587
|
+
const match = FRAME_LOCATION_RE.exec(text);
|
|
588
|
+
return match ? { url: match[1], line: +match[2], col: +match[3] } : null;
|
|
645
589
|
}
|
|
646
590
|
function isBundleUrl(url) {
|
|
647
|
-
|
|
648
|
-
|
|
591
|
+
return BUNDLE_URL_RE.test(url);
|
|
592
|
+
}
|
|
593
|
+
function resolveFrame(frame, decoder, projectRoot) {
|
|
594
|
+
const hit = FRAME_FORMATS.map((format) => ({ format, match: frame.match(format.pattern) })).find(
|
|
595
|
+
({ match }) => match !== null
|
|
596
|
+
);
|
|
597
|
+
if (!hit?.match) return null;
|
|
598
|
+
const original = tryResolve(hit.match[hit.format.urlGroup], decoder, projectRoot);
|
|
599
|
+
return original && {
|
|
600
|
+
resolved: hit.format.format(hit.match, original.display),
|
|
601
|
+
userPath: original.userPath,
|
|
602
|
+
sourceText: original.sourceText
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
function resolveStack(stack, decoder, projectRoot) {
|
|
606
|
+
const decoratedFrames = stack.split("\n").map((frame) => {
|
|
607
|
+
const resolution = resolveFrame(frame, decoder, projectRoot);
|
|
608
|
+
return {
|
|
609
|
+
line: resolution?.resolved ?? frame,
|
|
610
|
+
userPath: resolution?.userPath ?? null,
|
|
611
|
+
sourceText: resolution?.sourceText ?? null
|
|
612
|
+
};
|
|
613
|
+
});
|
|
614
|
+
const firstUser = decoratedFrames.find((entry) => entry.userPath !== null);
|
|
615
|
+
return {
|
|
616
|
+
resolvedStack: decoratedFrames.map((entry) => entry.line).join("\n"),
|
|
617
|
+
firstUserFrame: firstUser?.userPath ?? null,
|
|
618
|
+
firstUserSourceText: firstUser?.sourceText ?? null
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
function readVlqAt(text, cursor) {
|
|
622
|
+
let value = 0, bitShift = 0, position = cursor.position;
|
|
623
|
+
for (; ; ) {
|
|
624
|
+
const digit = BASE64_LOOKUP[text.charCodeAt(position++)];
|
|
625
|
+
value |= (digit & VLQ_DATA_MASK) << bitShift;
|
|
626
|
+
if (!(digit & VLQ_CONTINUATION)) break;
|
|
627
|
+
bitShift += 5;
|
|
628
|
+
}
|
|
629
|
+
cursor.position = position;
|
|
630
|
+
return value & 1 ? -(value >>> 1) : value >>> 1;
|
|
631
|
+
}
|
|
632
|
+
function atFieldStart(text, position) {
|
|
633
|
+
if (position >= text.length) return false;
|
|
634
|
+
const charCode = text.charCodeAt(position);
|
|
635
|
+
return charCode !== COMMA && charCode !== SEMICOLON;
|
|
636
|
+
}
|
|
637
|
+
function decodeBase64Utf8(base64) {
|
|
638
|
+
if (Buffer2) return Buffer2.from(base64, "base64").toString("utf8");
|
|
639
|
+
return UTF8_DECODER.decode(Uint8Array.from(atob(base64), (char) => char.charCodeAt(0)));
|
|
640
|
+
}
|
|
641
|
+
function readMarkerPayload(bundle) {
|
|
642
|
+
if (typeof bundle === "string") return sliceMarkerFromString(bundle);
|
|
643
|
+
if (Buffer2) return sliceMarkerFromBuffer(asBuffer(bundle, Buffer2));
|
|
644
|
+
return sliceMarkerFromString(UTF8_DECODER.decode(bundle));
|
|
645
|
+
}
|
|
646
|
+
function asBuffer(view, BufferCtor) {
|
|
647
|
+
return BufferCtor.isBuffer(view) ? view : BufferCtor.from(view.buffer, view.byteOffset, view.byteLength);
|
|
648
|
+
}
|
|
649
|
+
function sliceMarkerFromString(text) {
|
|
650
|
+
const markerStart = text.lastIndexOf(SOURCE_MAP_MARKER);
|
|
651
|
+
if (markerStart < 0) return null;
|
|
652
|
+
const payloadStart = markerStart + SOURCE_MAP_MARKER.length;
|
|
653
|
+
const payloadEnd = text.indexOf("\n", payloadStart);
|
|
654
|
+
return (payloadEnd < 0 ? text.slice(payloadStart) : text.slice(payloadStart, payloadEnd)).trim();
|
|
655
|
+
}
|
|
656
|
+
function sliceMarkerFromBuffer(buffer) {
|
|
657
|
+
const markerBytes = SOURCE_MAP_MARKER_BYTES;
|
|
658
|
+
const markerStart = buffer.lastIndexOf(markerBytes);
|
|
659
|
+
if (markerStart < 0) return null;
|
|
660
|
+
const payloadStart = markerStart + markerBytes.length;
|
|
661
|
+
const newlineIndex = buffer.indexOf(NEWLINE, payloadStart);
|
|
662
|
+
const payloadEnd = newlineIndex < 0 ? buffer.length : newlineIndex;
|
|
663
|
+
return buffer.toString("latin1", payloadStart, payloadEnd).trim();
|
|
664
|
+
}
|
|
665
|
+
function extractSourceLine(content, lineIndex) {
|
|
666
|
+
if (!content || lineIndex < 0) return null;
|
|
667
|
+
const line = content.split("\n", lineIndex + 1)[lineIndex];
|
|
668
|
+
return line?.trim() || null;
|
|
669
|
+
}
|
|
670
|
+
function normalizePosix(path10) {
|
|
671
|
+
const parts = path10.split("/").reduce((acc, part) => {
|
|
672
|
+
if (part === "..") acc.pop();
|
|
673
|
+
else if (part && part !== ".") acc.push(part);
|
|
674
|
+
return acc;
|
|
675
|
+
}, []);
|
|
676
|
+
return (path10.startsWith("/") ? "/" : "") + parts.join("/");
|
|
677
|
+
}
|
|
678
|
+
function toAbsolutePath(rawSource, outDir, sourceRoot) {
|
|
679
|
+
if (rawSource.startsWith("file://")) return rawSource.slice(7);
|
|
680
|
+
if (rawSource.startsWith("/")) return rawSource;
|
|
681
|
+
const base = sourceRoot ? normalizePosix(`${outDir}/${sourceRoot}`) : outDir;
|
|
682
|
+
return normalizePosix(`${base}/${rawSource}`);
|
|
649
683
|
}
|
|
650
|
-
function isNodeModulesPath(
|
|
651
|
-
return
|
|
684
|
+
function isNodeModulesPath(path10) {
|
|
685
|
+
return path10.includes("/node_modules/") || path10.includes("\\node_modules\\");
|
|
652
686
|
}
|
|
653
687
|
function makeDisplayPath(absolutePath, projectRoot) {
|
|
654
688
|
const prefix = projectRoot + "/";
|
|
655
689
|
return absolutePath.startsWith(prefix) ? absolutePath.slice(prefix.length) : absolutePath;
|
|
656
690
|
}
|
|
657
691
|
function tryResolve(urlLineCol, decoder, projectRoot) {
|
|
658
|
-
const
|
|
659
|
-
if (!
|
|
660
|
-
const
|
|
661
|
-
if (!
|
|
662
|
-
const display = `${makeDisplayPath(
|
|
692
|
+
const location = parseFrameLocation(urlLineCol);
|
|
693
|
+
if (!location || !isBundleUrl(location.url)) return null;
|
|
694
|
+
const original = lookupPosition(decoder, location.line, location.col);
|
|
695
|
+
if (!original) return null;
|
|
696
|
+
const display = `${makeDisplayPath(original.absolutePath, projectRoot)}:${original.line}:${original.col}`;
|
|
663
697
|
return {
|
|
664
698
|
display,
|
|
665
|
-
userPath: isNodeModulesPath(
|
|
666
|
-
sourceText:
|
|
699
|
+
userPath: isNodeModulesPath(original.absolutePath) ? null : display,
|
|
700
|
+
sourceText: original.sourceText
|
|
667
701
|
};
|
|
668
702
|
}
|
|
669
|
-
|
|
670
|
-
const chromeName = frame.match(/^(\s*at\s+)(.*?)\s+\(([^)]+)\)\s*$/);
|
|
671
|
-
if (chromeName) {
|
|
672
|
-
const r = tryResolve(chromeName[3], decoder, projectRoot);
|
|
673
|
-
return r ? {
|
|
674
|
-
resolved: `${chromeName[1]}${chromeName[2]} (${r.display})`,
|
|
675
|
-
userPath: r.userPath,
|
|
676
|
-
sourceText: r.sourceText
|
|
677
|
-
} : null;
|
|
678
|
-
}
|
|
679
|
-
const chromeAnon = frame.match(/^(\s*at\s+(?:async\s+)?)(.+)/);
|
|
680
|
-
if (chromeAnon) {
|
|
681
|
-
const r = tryResolve(chromeAnon[2], decoder, projectRoot);
|
|
682
|
-
return r ? { resolved: `${chromeAnon[1]}${r.display}`, userPath: r.userPath, sourceText: r.sourceText } : null;
|
|
683
|
-
}
|
|
684
|
-
const gecko = frame.match(/^([^@]*)@(.+)$/);
|
|
685
|
-
if (gecko) {
|
|
686
|
-
const r = tryResolve(gecko[2], decoder, projectRoot);
|
|
687
|
-
return r ? { resolved: `${gecko[1]}@${r.display}`, userPath: r.userPath, sourceText: r.sourceText } : null;
|
|
688
|
-
}
|
|
689
|
-
return null;
|
|
690
|
-
}
|
|
691
|
-
function resolveStack(stack, decoder, projectRoot) {
|
|
692
|
-
let firstUserFrame = null;
|
|
693
|
-
let firstUserSourceText = null;
|
|
694
|
-
const resolvedLines = stack.split("\n").map((frame) => {
|
|
695
|
-
const result = resolveFrame(frame, decoder, projectRoot);
|
|
696
|
-
if (!result) return frame;
|
|
697
|
-
if (!firstUserFrame && result.userPath) {
|
|
698
|
-
firstUserFrame = result.userPath;
|
|
699
|
-
firstUserSourceText = result.sourceText;
|
|
700
|
-
}
|
|
701
|
-
return result.resolved;
|
|
702
|
-
});
|
|
703
|
-
return { resolvedStack: resolvedLines.join("\n"), firstUserFrame, firstUserSourceText };
|
|
704
|
-
}
|
|
705
|
-
var BASE64, BASE64_LOOKUP, UTF8;
|
|
703
|
+
var BASE64_ALPHABET, BASE64_LOOKUP, COMMA, SEMICOLON, NEWLINE, VLQ_CONTINUATION, VLQ_DATA_MASK, SOURCE_MAP_MARKER, FRAME_LOCATION_RE, BUNDLE_URL_RE, FRAME_FORMATS, Buffer2, SOURCE_MAP_MARKER_BYTES, UTF8_DECODER;
|
|
706
704
|
var init_source_map_decoder = __esm({
|
|
707
705
|
"lib/utils/source-map-decoder.ts"() {
|
|
708
|
-
|
|
706
|
+
BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
709
707
|
BASE64_LOOKUP = new Uint8Array(128);
|
|
710
|
-
[...
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
708
|
+
[...BASE64_ALPHABET].forEach((char, index) => BASE64_LOOKUP[char.charCodeAt(0)] = index);
|
|
709
|
+
COMMA = 44;
|
|
710
|
+
SEMICOLON = 59;
|
|
711
|
+
NEWLINE = 10;
|
|
712
|
+
VLQ_CONTINUATION = 32;
|
|
713
|
+
VLQ_DATA_MASK = 31;
|
|
714
|
+
SOURCE_MAP_MARKER = "//# sourceMappingURL=data:application/json;base64,";
|
|
715
|
+
FRAME_LOCATION_RE = /^(.+):(\d+):(\d+)$/;
|
|
716
|
+
BUNDLE_URL_RE = /^(?:async )?https?:\/\/.*\/(?:tests|filtered-tests)\.js$/;
|
|
717
|
+
FRAME_FORMATS = [
|
|
718
|
+
// Chrome named: " at FUNC (URL:LINE:COL)"
|
|
719
|
+
{
|
|
720
|
+
pattern: /^(\s*at\s+)(.*?)\s+\(([^)]+)\)\s*$/,
|
|
721
|
+
urlGroup: 3,
|
|
722
|
+
format: (match, displayPath) => `${match[1]}${match[2]} (${displayPath})`
|
|
723
|
+
},
|
|
724
|
+
// Chrome anonymous (incl. async): " at [async] URL:LINE:COL"
|
|
725
|
+
{
|
|
726
|
+
pattern: /^(\s*at\s+(?:async\s+)?)(.+)/,
|
|
727
|
+
urlGroup: 2,
|
|
728
|
+
format: (match, displayPath) => `${match[1]}${displayPath}`
|
|
729
|
+
},
|
|
730
|
+
// Firefox / WebKit: "FUNC@URL:LINE:COL"
|
|
731
|
+
{
|
|
732
|
+
pattern: /^([^@]*)@(.+)$/,
|
|
733
|
+
urlGroup: 2,
|
|
734
|
+
format: (match, displayPath) => `${match[1]}@${displayPath}`
|
|
735
|
+
}
|
|
736
|
+
];
|
|
737
|
+
Buffer2 = globalThis.Buffer;
|
|
738
|
+
SOURCE_MAP_MARKER_BYTES = Buffer2 ? Buffer2.from(SOURCE_MAP_MARKER, "utf8") : null;
|
|
739
|
+
UTF8_DECODER = new TextDecoder();
|
|
714
740
|
}
|
|
715
741
|
});
|
|
716
742
|
|
|
@@ -836,12 +862,12 @@ var init_bind_server_to_port = __esm({
|
|
|
836
862
|
}
|
|
837
863
|
});
|
|
838
864
|
|
|
839
|
-
// lib/servers/
|
|
865
|
+
// lib/servers/web.ts
|
|
840
866
|
import http from "node:http";
|
|
841
867
|
import WebSocket, { WebSocketServer } from "ws";
|
|
842
868
|
var MIME_TYPES, HTTPServer;
|
|
843
|
-
var
|
|
844
|
-
"lib/servers/
|
|
869
|
+
var init_web = __esm({
|
|
870
|
+
"lib/servers/web.ts"() {
|
|
845
871
|
init_bind_server_to_port();
|
|
846
872
|
MIME_TYPES = {
|
|
847
873
|
html: "text/html; charset=UTF-8",
|
|
@@ -1298,166 +1324,18 @@ function setupWebServer(config, cachedContent) {
|
|
|
1298
1324
|
});
|
|
1299
1325
|
return server;
|
|
1300
1326
|
}
|
|
1301
|
-
function
|
|
1302
|
-
const
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
// setupQUnit runs exactly once, after both the WebSocket is open and tests.js has loaded.
|
|
1314
|
-
// Promise.all is naturally idempotent \u2014 resolving a Promise a second time is a no-op,
|
|
1315
|
-
// so WebKit firing WS error after open (causing a retry that re-opens) cannot double-start.
|
|
1316
|
-
let resolveWsReady = () => {};
|
|
1317
|
-
const wsReadyPromise = window.location.protocol === 'file:'
|
|
1318
|
-
? Promise.resolve()
|
|
1319
|
-
: new Promise(resolve => { resolveWsReady = resolve; });
|
|
1320
|
-
|
|
1321
|
-
// { once: true } auto-removes the listener after the first fire.
|
|
1322
|
-
const testsReadyPromise = new Promise(resolve => {
|
|
1323
|
-
window.addEventListener('qunitx:tests-ready', resolve, { once: true });
|
|
1324
|
-
});
|
|
1325
|
-
|
|
1326
|
-
Promise.all([wsReadyPromise, testsReadyPromise]).then(setupQUnit);
|
|
1327
|
-
|
|
1328
|
-
// For static files (file:// protocol) there is no WebSocket server; wsReadyPromise
|
|
1329
|
-
// is already resolved above, so setupQUnit fires as soon as tests load.
|
|
1330
|
-
if (window.location.protocol === 'file:') return;
|
|
1331
|
-
|
|
1332
|
-
let wsRetryCount = 0;
|
|
1333
|
-
const WS_MAX_RETRIES = Math.ceil(${config.timeout} / ${WS_RETRY_INTERVAL_MS}); // retry for the full test timeout window
|
|
1334
|
-
|
|
1335
|
-
function setupWebSocket() {
|
|
1336
|
-
try {
|
|
1337
|
-
window.socket = new WebSocket(\`ws://localhost:\${location.port}\`);
|
|
1338
|
-
} catch (error) {
|
|
1339
|
-
console.log(error);
|
|
1340
|
-
retryOrFail();
|
|
1341
|
-
return;
|
|
1342
|
-
}
|
|
1343
|
-
|
|
1344
|
-
window.socket.addEventListener('open', function() {
|
|
1345
|
-
resolveWsReady();
|
|
1346
|
-
// Notify Node.js that the WS socket is open. This fires immediately (< 1 s) because
|
|
1347
|
-
// this runtime script is tiny \u2014 tests.js background compilation hasn't finished yet.
|
|
1348
|
-
// Node.js uses this to distinguish "WS never connected" from "WS connected but bundle slow".
|
|
1349
|
-
if (navigator.webdriver) {
|
|
1350
|
-
window.socket.send(JSON.stringify({ event: 'wsOpen'${groupIdPart} }));
|
|
1351
|
-
}
|
|
1352
|
-
});
|
|
1353
|
-
window.socket.addEventListener('error', function() {
|
|
1354
|
-
retryOrFail();
|
|
1355
|
-
});
|
|
1356
|
-
window.socket.addEventListener('message', function(messageEvent) {
|
|
1357
|
-
if (!navigator.webdriver && messageEvent.data === 'refresh') {
|
|
1358
|
-
window.location.reload(true);
|
|
1359
|
-
} else if (navigator.webdriver && messageEvent.data === 'abort') {
|
|
1360
|
-
window.abortQUnit = true;
|
|
1361
|
-
window.QUnit.config.queue.length = 0;
|
|
1362
|
-
window.socket.send(JSON.stringify({ event: 'abort' }));
|
|
1363
|
-
}
|
|
1364
|
-
});
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
function retryOrFail() {
|
|
1368
|
-
wsRetryCount++;
|
|
1369
|
-
if (wsRetryCount > WS_MAX_RETRIES) {
|
|
1370
|
-
console.log('WebSocket connection failed after ' + WS_MAX_RETRIES + ' retries');
|
|
1371
|
-
return;
|
|
1372
|
-
}
|
|
1373
|
-
window.setTimeout(setupWebSocket, ${WS_RETRY_INTERVAL_MS});
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
setupWebSocket();
|
|
1377
|
-
})();
|
|
1378
|
-
|
|
1379
|
-
function getCircularReplacer() {
|
|
1380
|
-
const ancestors = [];
|
|
1381
|
-
return function (key, value) {
|
|
1382
|
-
if (typeof value !== "object" || value === null) {
|
|
1383
|
-
return value;
|
|
1384
|
-
}
|
|
1385
|
-
while (ancestors.length > 0 && ancestors.at(-1) !== this) {
|
|
1386
|
-
ancestors.pop();
|
|
1387
|
-
}
|
|
1388
|
-
if (ancestors.includes(value)) {
|
|
1389
|
-
return "[Circular]";
|
|
1390
|
-
}
|
|
1391
|
-
ancestors.push(value);
|
|
1392
|
-
return value;
|
|
1393
|
-
};
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
function setupQUnit() {
|
|
1397
|
-
window.QUNIT_RESULT = { totalTests: 0, finishedTests: 0, failedTests: 0, currentTest: '' };
|
|
1398
|
-
|
|
1399
|
-
if (!window.QUnit) {
|
|
1400
|
-
console.log('QUnit not found after WebSocket connected');
|
|
1401
|
-
if (navigator.webdriver) {
|
|
1402
|
-
// Signal the Playwright runner that the run is complete with 0 tests rather than
|
|
1403
|
-
// waiting for the inactivity timeout. The runner treats totalTests === 0 as a
|
|
1404
|
-
// "no tests registered" warning (not a failure), so this gives a fast, clean result.
|
|
1405
|
-
window.QUNIT_RESULT = { totalTests: 0, finishedTests: 0, failedTests: 0, currentTest: null };
|
|
1406
|
-
window.socket.send(JSON.stringify({ event: 'done', details: { passed: 0, failed: 0, runtime: 0 } }));
|
|
1407
|
-
}
|
|
1408
|
-
return;
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
|
-
window.QUnit.begin(() => { // NOTE: might be useful in future for hanged module tracking
|
|
1412
|
-
if (navigator.webdriver) {
|
|
1413
|
-
window.socket.send(JSON.stringify({ event: 'connection' }));
|
|
1414
|
-
}
|
|
1415
|
-
});
|
|
1416
|
-
window.QUnit.on('testStart', (details) => {
|
|
1417
|
-
window.QUNIT_RESULT.totalTests++;
|
|
1418
|
-
window.QUNIT_RESULT.currentTest = details.fullName.join(' | ');
|
|
1419
|
-
});
|
|
1420
|
-
window.QUnit.on('testEnd', (details) => { // NOTE: https://github.com/qunitjs/qunit/blob/master/src/html-reporter/diff.js
|
|
1421
|
-
window.QUNIT_RESULT.finishedTests++;
|
|
1422
|
-
if (details.status === 'failed') window.QUNIT_RESULT.failedTests++;
|
|
1423
|
-
window.QUNIT_RESULT.currentTest = null;
|
|
1424
|
-
if (navigator.webdriver) {
|
|
1425
|
-
const isFailed = details.status === 'failed';
|
|
1426
|
-
const payload = isFailed ? details : { status: details.status, fullName: details.fullName, runtime: details.runtime };
|
|
1427
|
-
window.socket.send(JSON.stringify({ event: 'testEnd', details: payload, abort: window.abortQUnit }, isFailed ? getCircularReplacer() : undefined));
|
|
1428
|
-
|
|
1429
|
-
if (${config.failFast} && details.status === 'failed') {
|
|
1430
|
-
window.QUnit.config.queue.length = 0;
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
});
|
|
1434
|
-
window.QUnit.done((details) => {
|
|
1435
|
-
if (navigator.webdriver) {
|
|
1436
|
-
window.socket.send(JSON.stringify({ event: 'done', details: details, qunitResult: window.QUNIT_RESULT, abort: window.abortQUnit }, getCircularReplacer()));
|
|
1437
|
-
}
|
|
1438
|
-
});
|
|
1439
|
-
|
|
1440
|
-
window.QUnit.config.testTimeout = ${config.timeout};
|
|
1441
|
-
window.QUnit.start();
|
|
1442
|
-
}
|
|
1443
|
-
</script>`;
|
|
1444
|
-
}
|
|
1445
|
-
function escapeAndInjectTestsToHTML(html, testRuntimeCode, testBundleUrl) {
|
|
1446
|
-
return injectScript(html, `${testRuntimeCode}
|
|
1447
|
-
<script src="${testBundleUrl}" async></script>`);
|
|
1448
|
-
}
|
|
1449
|
-
function buildNoTestsHTML(files) {
|
|
1450
|
-
const escaped = files.map((f) => f.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")).join("\n");
|
|
1451
|
-
return `<!DOCTYPE html>
|
|
1452
|
-
<html lang="en">
|
|
1453
|
-
<head>
|
|
1454
|
-
<meta charset="utf-8">
|
|
1455
|
-
<meta name="viewport" content="width=device-width">
|
|
1456
|
-
<title>No Tests Registered \u2014 qunitx</title>
|
|
1457
|
-
<style>
|
|
1458
|
-
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
1459
|
-
#qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-tests, #qunit-tests li {
|
|
1460
|
-
font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
|
|
1327
|
+
function buildNoTestsHTML(files) {
|
|
1328
|
+
const escaped = files.map((f) => f.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")).join("\n");
|
|
1329
|
+
return `<!DOCTYPE html>
|
|
1330
|
+
<html lang="en">
|
|
1331
|
+
<head>
|
|
1332
|
+
<meta charset="utf-8">
|
|
1333
|
+
<meta name="viewport" content="width=device-width">
|
|
1334
|
+
<title>No Tests Registered \u2014 qunitx</title>
|
|
1335
|
+
<style>
|
|
1336
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
1337
|
+
#qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-tests, #qunit-tests li {
|
|
1338
|
+
font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
|
|
1461
1339
|
}
|
|
1462
1340
|
#qunit-header {
|
|
1463
1341
|
padding: 0.5em 0 0.5em 1em;
|
|
@@ -1700,16 +1578,6 @@ function registerGroupRoutes(server, groupConfig, groupCachedContent, groupId) {
|
|
|
1700
1578
|
res.end(groupCachedContent.allTestCode);
|
|
1701
1579
|
});
|
|
1702
1580
|
}
|
|
1703
|
-
function debugGroupHeader(config) {
|
|
1704
|
-
const files = Object.keys(config.fsTree);
|
|
1705
|
-
const rel = files.map((f) => f.replace(`${config.projectRoot}/`, ""));
|
|
1706
|
-
const shown = rel.slice(0, 2);
|
|
1707
|
-
const rest = rel.length - shown.length;
|
|
1708
|
-
process.stdout.write(
|
|
1709
|
-
`# ${blue(`\u2500\u2500 ${shown.join(" ")}${rest > 0 ? ` +${rest} more` : ""} \u2500\u2500`)}
|
|
1710
|
-
`
|
|
1711
|
-
);
|
|
1712
|
-
}
|
|
1713
1581
|
function setupGroupWSHandler(server, groupConfigs) {
|
|
1714
1582
|
const socketToGroupId = /* @__PURE__ */ new WeakMap();
|
|
1715
1583
|
server.wss.on("connection", function connection(socket) {
|
|
@@ -1790,6 +1658,164 @@ function registerSharedStaticHandler(server, groupConfigs) {
|
|
|
1790
1658
|
});
|
|
1791
1659
|
});
|
|
1792
1660
|
}
|
|
1661
|
+
function replaceAssetPaths(html, htmlPath, projectRoot) {
|
|
1662
|
+
const assetPaths = findInternalAssetsFromHTML(html);
|
|
1663
|
+
const htmlDirectory = htmlPath.split("/").slice(0, -1).join("/");
|
|
1664
|
+
return assetPaths.reduce((result, assetPath) => {
|
|
1665
|
+
const normalizedFullAbsolutePath = path5.normalize(`${htmlDirectory}/${assetPath}`);
|
|
1666
|
+
return result.replace(assetPath, normalizedFullAbsolutePath.replace(projectRoot, "."));
|
|
1667
|
+
}, html);
|
|
1668
|
+
}
|
|
1669
|
+
function testRuntimeToInject(config, groupId) {
|
|
1670
|
+
const groupIdPart = groupId !== void 0 ? `, groupId: ${groupId}` : "";
|
|
1671
|
+
return `<script>
|
|
1672
|
+
(function() {
|
|
1673
|
+
// setupQUnit runs exactly once, after both the WebSocket is open and tests.js has loaded.
|
|
1674
|
+
// Promise.all is naturally idempotent \u2014 resolving a Promise a second time is a no-op,
|
|
1675
|
+
// so WebKit firing WS error after open (causing a retry that re-opens) cannot double-start.
|
|
1676
|
+
let resolveWsReady = () => {};
|
|
1677
|
+
const wsReadyPromise = window.location.protocol === 'file:'
|
|
1678
|
+
? Promise.resolve()
|
|
1679
|
+
: new Promise(resolve => { resolveWsReady = resolve; });
|
|
1680
|
+
|
|
1681
|
+
// { once: true } auto-removes the listener after the first fire.
|
|
1682
|
+
const testsReadyPromise = new Promise(resolve => {
|
|
1683
|
+
window.addEventListener('qunitx:tests-ready', resolve, { once: true });
|
|
1684
|
+
});
|
|
1685
|
+
|
|
1686
|
+
Promise.all([wsReadyPromise, testsReadyPromise]).then(setupQUnit);
|
|
1687
|
+
|
|
1688
|
+
// For static files (file:// protocol) there is no WebSocket server; wsReadyPromise
|
|
1689
|
+
// is already resolved above, so setupQUnit fires as soon as tests load.
|
|
1690
|
+
if (window.location.protocol === 'file:') return;
|
|
1691
|
+
|
|
1692
|
+
let wsRetryCount = 0;
|
|
1693
|
+
const WS_MAX_RETRIES = Math.ceil(${config.timeout} / ${WS_RETRY_INTERVAL_MS}); // retry for the full test timeout window
|
|
1694
|
+
|
|
1695
|
+
function setupWebSocket() {
|
|
1696
|
+
try {
|
|
1697
|
+
window.socket = new WebSocket(\`ws://localhost:\${location.port}\`);
|
|
1698
|
+
} catch (error) {
|
|
1699
|
+
console.log(error);
|
|
1700
|
+
retryOrFail();
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
window.socket.addEventListener('open', function() {
|
|
1705
|
+
resolveWsReady();
|
|
1706
|
+
// Notify Node.js that the WS socket is open. This fires immediately (< 1 s) because
|
|
1707
|
+
// this runtime script is tiny \u2014 tests.js background compilation hasn't finished yet.
|
|
1708
|
+
// Node.js uses this to distinguish "WS never connected" from "WS connected but bundle slow".
|
|
1709
|
+
if (navigator.webdriver) {
|
|
1710
|
+
window.socket.send(JSON.stringify({ event: 'wsOpen'${groupIdPart} }));
|
|
1711
|
+
}
|
|
1712
|
+
});
|
|
1713
|
+
window.socket.addEventListener('error', function() {
|
|
1714
|
+
retryOrFail();
|
|
1715
|
+
});
|
|
1716
|
+
window.socket.addEventListener('message', function(messageEvent) {
|
|
1717
|
+
if (!navigator.webdriver && messageEvent.data === 'refresh') {
|
|
1718
|
+
window.location.reload(true);
|
|
1719
|
+
} else if (navigator.webdriver && messageEvent.data === 'abort') {
|
|
1720
|
+
window.abortQUnit = true;
|
|
1721
|
+
window.QUnit.config.queue.length = 0;
|
|
1722
|
+
window.socket.send(JSON.stringify({ event: 'abort' }));
|
|
1723
|
+
}
|
|
1724
|
+
});
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
function retryOrFail() {
|
|
1728
|
+
wsRetryCount++;
|
|
1729
|
+
if (wsRetryCount > WS_MAX_RETRIES) {
|
|
1730
|
+
console.log('WebSocket connection failed after ' + WS_MAX_RETRIES + ' retries');
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
window.setTimeout(setupWebSocket, ${WS_RETRY_INTERVAL_MS});
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
setupWebSocket();
|
|
1737
|
+
})();
|
|
1738
|
+
|
|
1739
|
+
function getCircularReplacer() {
|
|
1740
|
+
const ancestors = [];
|
|
1741
|
+
return function (key, value) {
|
|
1742
|
+
if (typeof value !== "object" || value === null) {
|
|
1743
|
+
return value;
|
|
1744
|
+
}
|
|
1745
|
+
while (ancestors.length > 0 && ancestors.at(-1) !== this) {
|
|
1746
|
+
ancestors.pop();
|
|
1747
|
+
}
|
|
1748
|
+
if (ancestors.includes(value)) {
|
|
1749
|
+
return "[Circular]";
|
|
1750
|
+
}
|
|
1751
|
+
ancestors.push(value);
|
|
1752
|
+
return value;
|
|
1753
|
+
};
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
function setupQUnit() {
|
|
1757
|
+
window.QUNIT_RESULT = { totalTests: 0, finishedTests: 0, failedTests: 0, currentTest: '' };
|
|
1758
|
+
|
|
1759
|
+
if (!window.QUnit) {
|
|
1760
|
+
console.log('QUnit not found after WebSocket connected');
|
|
1761
|
+
if (navigator.webdriver) {
|
|
1762
|
+
// Signal the Playwright runner that the run is complete with 0 tests rather than
|
|
1763
|
+
// waiting for the inactivity timeout. The runner treats totalTests === 0 as a
|
|
1764
|
+
// "no tests registered" warning (not a failure), so this gives a fast, clean result.
|
|
1765
|
+
window.QUNIT_RESULT = { totalTests: 0, finishedTests: 0, failedTests: 0, currentTest: null };
|
|
1766
|
+
window.socket.send(JSON.stringify({ event: 'done', details: { passed: 0, failed: 0, runtime: 0 } }));
|
|
1767
|
+
}
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
window.QUnit.begin(() => { // NOTE: might be useful in future for hanged module tracking
|
|
1772
|
+
if (navigator.webdriver) {
|
|
1773
|
+
window.socket.send(JSON.stringify({ event: 'connection' }));
|
|
1774
|
+
}
|
|
1775
|
+
});
|
|
1776
|
+
window.QUnit.on('testStart', (details) => {
|
|
1777
|
+
window.QUNIT_RESULT.totalTests++;
|
|
1778
|
+
window.QUNIT_RESULT.currentTest = details.fullName.join(' | ');
|
|
1779
|
+
});
|
|
1780
|
+
window.QUnit.on('testEnd', (details) => { // NOTE: https://github.com/qunitjs/qunit/blob/master/src/html-reporter/diff.js
|
|
1781
|
+
window.QUNIT_RESULT.finishedTests++;
|
|
1782
|
+
if (details.status === 'failed') window.QUNIT_RESULT.failedTests++;
|
|
1783
|
+
window.QUNIT_RESULT.currentTest = null;
|
|
1784
|
+
if (navigator.webdriver) {
|
|
1785
|
+
const isFailed = details.status === 'failed';
|
|
1786
|
+
const payload = isFailed ? details : { status: details.status, fullName: details.fullName, runtime: details.runtime };
|
|
1787
|
+
window.socket.send(JSON.stringify({ event: 'testEnd', details: payload, abort: window.abortQUnit }, isFailed ? getCircularReplacer() : undefined));
|
|
1788
|
+
|
|
1789
|
+
if (${config.failFast} && details.status === 'failed') {
|
|
1790
|
+
window.QUnit.config.queue.length = 0;
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
});
|
|
1794
|
+
window.QUnit.done((details) => {
|
|
1795
|
+
if (navigator.webdriver) {
|
|
1796
|
+
window.socket.send(JSON.stringify({ event: 'done', details: details, qunitResult: window.QUNIT_RESULT, abort: window.abortQUnit }, getCircularReplacer()));
|
|
1797
|
+
}
|
|
1798
|
+
});
|
|
1799
|
+
|
|
1800
|
+
window.QUnit.config.testTimeout = ${config.timeout};
|
|
1801
|
+
window.QUnit.start();
|
|
1802
|
+
}
|
|
1803
|
+
</script>`;
|
|
1804
|
+
}
|
|
1805
|
+
function escapeAndInjectTestsToHTML(html, testRuntimeCode, testBundleUrl) {
|
|
1806
|
+
return injectScript(html, `${testRuntimeCode}
|
|
1807
|
+
<script src="${testBundleUrl}" async></script>`);
|
|
1808
|
+
}
|
|
1809
|
+
function debugGroupHeader(config) {
|
|
1810
|
+
const files = Object.keys(config.fsTree);
|
|
1811
|
+
const rel = files.map((f) => f.replace(`${config.projectRoot}/`, ""));
|
|
1812
|
+
const shown = rel.slice(0, 2);
|
|
1813
|
+
const rest = rel.length - shown.length;
|
|
1814
|
+
process.stdout.write(
|
|
1815
|
+
`# ${blue(`\u2500\u2500 ${shown.join(" ")}${rest > 0 ? ` +${rest} more` : ""} \u2500\u2500`)}
|
|
1816
|
+
`
|
|
1817
|
+
);
|
|
1818
|
+
}
|
|
1793
1819
|
var fsPromise, HTML_HEADERS, WATCH_WS_RECONNECT_INTERVAL_MS, WATCH_WS_RECONNECT_MAX_RETRIES, WS_RETRY_INTERVAL_MS, NOT_FOUND_HTML;
|
|
1794
1820
|
var init_web_server = __esm({
|
|
1795
1821
|
"lib/setup/web-server.ts"() {
|
|
@@ -1797,7 +1823,7 @@ var init_web_server = __esm({
|
|
|
1797
1823
|
init_html();
|
|
1798
1824
|
init_display_test_result();
|
|
1799
1825
|
init_color();
|
|
1800
|
-
|
|
1826
|
+
init_web();
|
|
1801
1827
|
fsPromise = fs8.promises;
|
|
1802
1828
|
HTML_HEADERS = { "Content-Type": "text/html", "Cache-Control": "no-store" };
|
|
1803
1829
|
WATCH_WS_RECONNECT_INTERVAL_MS = 1e3;
|
|
@@ -2058,12 +2084,6 @@ var init_display_final_result = __esm({
|
|
|
2058
2084
|
import fs9 from "node:fs/promises";
|
|
2059
2085
|
import path7 from "node:path";
|
|
2060
2086
|
import esbuild from "esbuild";
|
|
2061
|
-
function toEsbuildImportPath(filePath) {
|
|
2062
|
-
const rel = path7.relative(process.cwd(), filePath);
|
|
2063
|
-
const normalized = rel.replace(/\\/g, "/");
|
|
2064
|
-
if (path7.isAbsolute(rel)) return filePath.replace(/\\/g, "/");
|
|
2065
|
-
return normalized.startsWith(".") ? normalized : "./" + normalized;
|
|
2066
|
-
}
|
|
2067
2087
|
function deriveBuildErrorType(error) {
|
|
2068
2088
|
const msgs = error?.errors ?? [];
|
|
2069
2089
|
const text = msgs[0]?.text ?? (error instanceof Error ? error.message : String(error));
|
|
@@ -2264,7 +2284,130 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
2264
2284
|
throw exception;
|
|
2265
2285
|
}
|
|
2266
2286
|
}
|
|
2267
|
-
return connections;
|
|
2287
|
+
return connections;
|
|
2288
|
+
}
|
|
2289
|
+
async function flushConsoleHandlers(handlers, deadline = Date.now() + CONSOLE_FLUSH_TIMEOUT_MS) {
|
|
2290
|
+
if (!handlers || Date.now() >= deadline) return;
|
|
2291
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
2292
|
+
if (handlers.size === 0) return;
|
|
2293
|
+
await Promise.allSettled([...handlers]);
|
|
2294
|
+
return flushConsoleHandlers(handlers, deadline);
|
|
2295
|
+
}
|
|
2296
|
+
async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
2297
|
+
groupCachedContents.forEach((cachedContent) => {
|
|
2298
|
+
cachedContent._buildError = null;
|
|
2299
|
+
cachedContent._noTestsWarning = null;
|
|
2300
|
+
});
|
|
2301
|
+
const { projectRoot, debug, browser } = groupConfigs[0];
|
|
2302
|
+
const activeGroups = groupConfigs.reduce(
|
|
2303
|
+
(acc, groupConfig, groupIndex) => {
|
|
2304
|
+
const files = Object.keys(groupConfig.fsTree);
|
|
2305
|
+
if (files.length > 0)
|
|
2306
|
+
acc.push({
|
|
2307
|
+
groupIndex,
|
|
2308
|
+
config: groupConfig,
|
|
2309
|
+
cachedContent: groupCachedContents[groupIndex],
|
|
2310
|
+
files
|
|
2311
|
+
});
|
|
2312
|
+
return acc;
|
|
2313
|
+
},
|
|
2314
|
+
[]
|
|
2315
|
+
);
|
|
2316
|
+
if (activeGroups.length === 0)
|
|
2317
|
+
return console.log(
|
|
2318
|
+
"# [buildAllGroupBundles] all groups empty \u2014 skipping build (no test files found)"
|
|
2319
|
+
);
|
|
2320
|
+
await Promise.all(
|
|
2321
|
+
activeGroups.map(
|
|
2322
|
+
(group) => fs9.mkdir(path7.resolve(group.config.projectRoot, group.config.output), { recursive: true })
|
|
2323
|
+
)
|
|
2324
|
+
);
|
|
2325
|
+
const sourcemap = "inline";
|
|
2326
|
+
const groupEntryPlugin = {
|
|
2327
|
+
name: "group-entry-loader",
|
|
2328
|
+
setup(build) {
|
|
2329
|
+
build.onResolve({ filter: /^group-entry-\d+$/ }, (args) => ({
|
|
2330
|
+
path: args.path,
|
|
2331
|
+
namespace: "group-entry"
|
|
2332
|
+
}));
|
|
2333
|
+
build.onLoad({ filter: /.*/, namespace: "group-entry" }, (args) => {
|
|
2334
|
+
const slotIndex = parseInt(args.path.replace("group-entry-", ""));
|
|
2335
|
+
return {
|
|
2336
|
+
contents: activeGroups[slotIndex].files.map((filePath) => `import "${toEsbuildImportPath(filePath)}";`).join(""),
|
|
2337
|
+
resolveDir: process.cwd()
|
|
2338
|
+
};
|
|
2339
|
+
});
|
|
2340
|
+
}
|
|
2341
|
+
};
|
|
2342
|
+
const esbuildOutdir = path7.join(projectRoot, "tmp");
|
|
2343
|
+
const buildOptions = {
|
|
2344
|
+
entryPoints: activeGroups.map((_, slotIndex) => ({
|
|
2345
|
+
in: `group-entry-${slotIndex}`,
|
|
2346
|
+
out: `group-${slotIndex}`
|
|
2347
|
+
})),
|
|
2348
|
+
plugins: [groupEntryPlugin],
|
|
2349
|
+
nodePaths: ANCESTOR_NODE_MODULES,
|
|
2350
|
+
bundle: true,
|
|
2351
|
+
logLevel: "silent",
|
|
2352
|
+
// outdir only labels the paths in outputFiles[].path — nothing is written to disk
|
|
2353
|
+
// (write:false). Use projectRoot/tmp as a stable sentinel; mkdir is not required.
|
|
2354
|
+
outdir: esbuildOutdir,
|
|
2355
|
+
keepNames: true,
|
|
2356
|
+
legalComments: "none",
|
|
2357
|
+
target: esbuildTarget(browser),
|
|
2358
|
+
sourcemap,
|
|
2359
|
+
write: false,
|
|
2360
|
+
footer: { js: 'window.dispatchEvent(new CustomEvent("qunitx:tests-ready"));' }
|
|
2361
|
+
};
|
|
2362
|
+
const hasSmallOutput = (result) => (result.outputFiles ?? []).some(
|
|
2363
|
+
(outputFile) => GROUP_OUTPUT_REGEX.test(outputFile.path) && !outputFile.path.endsWith(".map") && outputFile.contents.length < EMPTY_BUNDLE_THRESHOLD
|
|
2364
|
+
);
|
|
2365
|
+
const buildWithRetry = async (retriesLeft) => {
|
|
2366
|
+
const result = await esbuild.build(buildOptions);
|
|
2367
|
+
if (!hasSmallOutput(result) || retriesLeft === 0) return result;
|
|
2368
|
+
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
|
|
2369
|
+
return buildWithRetry(retriesLeft - 1);
|
|
2370
|
+
};
|
|
2371
|
+
try {
|
|
2372
|
+
const result = await buildWithRetry(MAX_RETRIES);
|
|
2373
|
+
await Promise.all(
|
|
2374
|
+
(result.outputFiles ?? []).map((outputFile) => {
|
|
2375
|
+
const match = GROUP_OUTPUT_REGEX.exec(outputFile.path);
|
|
2376
|
+
if (!match) return Promise.resolve();
|
|
2377
|
+
const slotIndex = parseInt(match[1]);
|
|
2378
|
+
const isMap = Boolean(match[2]);
|
|
2379
|
+
const { config, cachedContent } = activeGroups[slotIndex];
|
|
2380
|
+
const destPath = path7.join(
|
|
2381
|
+
path7.resolve(config.projectRoot, config.output),
|
|
2382
|
+
"tests.js" + (isMap ? ".map" : "")
|
|
2383
|
+
);
|
|
2384
|
+
if (!isMap) {
|
|
2385
|
+
cachedContent.allTestCode = Buffer.from(outputFile.contents);
|
|
2386
|
+
config._sourceMapDecoder = extractInlineSourceMap(
|
|
2387
|
+
cachedContent.allTestCode,
|
|
2388
|
+
esbuildOutdir
|
|
2389
|
+
);
|
|
2390
|
+
}
|
|
2391
|
+
return fs9.writeFile(destPath, outputFile.contents);
|
|
2392
|
+
})
|
|
2393
|
+
);
|
|
2394
|
+
} catch (error) {
|
|
2395
|
+
const buildError = { type: deriveBuildErrorType(error), formatted: formatBuildErrors(error) };
|
|
2396
|
+
const errorHtml = buildErrorHTML(buildError);
|
|
2397
|
+
await Promise.all(
|
|
2398
|
+
activeGroups.map((group) => {
|
|
2399
|
+
group.cachedContent._buildError = buildError;
|
|
2400
|
+
return fs9.writeFile(
|
|
2401
|
+
path7.join(path7.resolve(group.config.projectRoot, group.config.output), "index.html"),
|
|
2402
|
+
errorHtml
|
|
2403
|
+
).catch(
|
|
2404
|
+
(err) => debug && process.stderr.write(`# [qunitx] writeFile index.html: ${err.message}
|
|
2405
|
+
`)
|
|
2406
|
+
);
|
|
2407
|
+
})
|
|
2408
|
+
);
|
|
2409
|
+
throw error;
|
|
2410
|
+
}
|
|
2268
2411
|
}
|
|
2269
2412
|
function buildFilteredTests(filteredTests, outputPath, config) {
|
|
2270
2413
|
const sourcemap = "inline";
|
|
@@ -2342,7 +2485,7 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
|
|
|
2342
2485
|
let wsConnected = false;
|
|
2343
2486
|
try {
|
|
2344
2487
|
console.log("#", blue(`QUnitX running: http://localhost:${config.port}${filePath}`));
|
|
2345
|
-
const navMs = config.timeout + NAV_GRACE_MS;
|
|
2488
|
+
const navMs = Math.max(config.timeout + NAV_GRACE_MS, MIN_NAV_MS);
|
|
2346
2489
|
const startupMs = Math.max(config.timeout * STARTUP_TIMEOUT_FACTOR, navMs);
|
|
2347
2490
|
const testsJsMs = Math.max(config.timeout * TESTS_JS_TIMEOUT_FACTOR, navMs);
|
|
2348
2491
|
let resolveTestRace;
|
|
@@ -2432,128 +2575,13 @@ async function failOnNonWatchMode(watchMode = false, connections = {}, groupMode
|
|
|
2432
2575
|
process.exit(1);
|
|
2433
2576
|
}
|
|
2434
2577
|
}
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
return
|
|
2439
|
-
|
|
2440
|
-
async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
2441
|
-
groupCachedContents.forEach((cachedContent) => {
|
|
2442
|
-
cachedContent._buildError = null;
|
|
2443
|
-
cachedContent._noTestsWarning = null;
|
|
2444
|
-
});
|
|
2445
|
-
const { projectRoot, debug, browser } = groupConfigs[0];
|
|
2446
|
-
const activeGroups = groupConfigs.reduce(
|
|
2447
|
-
(acc, groupConfig, groupIndex) => {
|
|
2448
|
-
const files = Object.keys(groupConfig.fsTree);
|
|
2449
|
-
if (files.length > 0)
|
|
2450
|
-
acc.push({
|
|
2451
|
-
groupIndex,
|
|
2452
|
-
config: groupConfig,
|
|
2453
|
-
cachedContent: groupCachedContents[groupIndex],
|
|
2454
|
-
files
|
|
2455
|
-
});
|
|
2456
|
-
return acc;
|
|
2457
|
-
},
|
|
2458
|
-
[]
|
|
2459
|
-
);
|
|
2460
|
-
if (activeGroups.length === 0)
|
|
2461
|
-
return console.log(
|
|
2462
|
-
"# [buildAllGroupBundles] all groups empty \u2014 skipping build (no test files found)"
|
|
2463
|
-
);
|
|
2464
|
-
await Promise.all(
|
|
2465
|
-
activeGroups.map(
|
|
2466
|
-
(group) => fs9.mkdir(path7.resolve(group.config.projectRoot, group.config.output), { recursive: true })
|
|
2467
|
-
)
|
|
2468
|
-
);
|
|
2469
|
-
const sourcemap = "inline";
|
|
2470
|
-
const groupEntryPlugin = {
|
|
2471
|
-
name: "group-entry-loader",
|
|
2472
|
-
setup(build) {
|
|
2473
|
-
build.onResolve({ filter: /^group-entry-\d+$/ }, (args) => ({
|
|
2474
|
-
path: args.path,
|
|
2475
|
-
namespace: "group-entry"
|
|
2476
|
-
}));
|
|
2477
|
-
build.onLoad({ filter: /.*/, namespace: "group-entry" }, (args) => {
|
|
2478
|
-
const slotIndex = parseInt(args.path.replace("group-entry-", ""));
|
|
2479
|
-
return {
|
|
2480
|
-
contents: activeGroups[slotIndex].files.map((filePath) => `import "${toEsbuildImportPath(filePath)}";`).join(""),
|
|
2481
|
-
resolveDir: process.cwd()
|
|
2482
|
-
};
|
|
2483
|
-
});
|
|
2484
|
-
}
|
|
2485
|
-
};
|
|
2486
|
-
const esbuildOutdir = path7.join(projectRoot, "tmp");
|
|
2487
|
-
const buildOptions = {
|
|
2488
|
-
entryPoints: activeGroups.map((_, slotIndex) => ({
|
|
2489
|
-
in: `group-entry-${slotIndex}`,
|
|
2490
|
-
out: `group-${slotIndex}`
|
|
2491
|
-
})),
|
|
2492
|
-
plugins: [groupEntryPlugin],
|
|
2493
|
-
nodePaths: ANCESTOR_NODE_MODULES,
|
|
2494
|
-
bundle: true,
|
|
2495
|
-
logLevel: "silent",
|
|
2496
|
-
// outdir only labels the paths in outputFiles[].path — nothing is written to disk
|
|
2497
|
-
// (write:false). Use projectRoot/tmp as a stable sentinel; mkdir is not required.
|
|
2498
|
-
outdir: esbuildOutdir,
|
|
2499
|
-
keepNames: true,
|
|
2500
|
-
legalComments: "none",
|
|
2501
|
-
target: esbuildTarget(browser),
|
|
2502
|
-
sourcemap,
|
|
2503
|
-
write: false,
|
|
2504
|
-
footer: { js: 'window.dispatchEvent(new CustomEvent("qunitx:tests-ready"));' }
|
|
2505
|
-
};
|
|
2506
|
-
const hasSmallOutput = (result) => (result.outputFiles ?? []).some(
|
|
2507
|
-
(outputFile) => GROUP_OUTPUT_REGEX.test(outputFile.path) && !outputFile.path.endsWith(".map") && outputFile.contents.length < EMPTY_BUNDLE_THRESHOLD
|
|
2508
|
-
);
|
|
2509
|
-
const buildWithRetry = async (retriesLeft) => {
|
|
2510
|
-
const result = await esbuild.build(buildOptions);
|
|
2511
|
-
if (!hasSmallOutput(result) || retriesLeft === 0) return result;
|
|
2512
|
-
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
|
|
2513
|
-
return buildWithRetry(retriesLeft - 1);
|
|
2514
|
-
};
|
|
2515
|
-
try {
|
|
2516
|
-
const result = await buildWithRetry(MAX_RETRIES);
|
|
2517
|
-
await Promise.all(
|
|
2518
|
-
(result.outputFiles ?? []).map((outputFile) => {
|
|
2519
|
-
const match = GROUP_OUTPUT_REGEX.exec(outputFile.path);
|
|
2520
|
-
if (!match) return Promise.resolve();
|
|
2521
|
-
const slotIndex = parseInt(match[1]);
|
|
2522
|
-
const isMap = Boolean(match[2]);
|
|
2523
|
-
const { config, cachedContent } = activeGroups[slotIndex];
|
|
2524
|
-
const destPath = path7.join(
|
|
2525
|
-
path7.resolve(config.projectRoot, config.output),
|
|
2526
|
-
"tests.js" + (isMap ? ".map" : "")
|
|
2527
|
-
);
|
|
2528
|
-
if (!isMap) {
|
|
2529
|
-
cachedContent.allTestCode = Buffer.from(outputFile.contents);
|
|
2530
|
-
config._sourceMapDecoder = extractInlineSourceMap(
|
|
2531
|
-
cachedContent.allTestCode,
|
|
2532
|
-
esbuildOutdir
|
|
2533
|
-
);
|
|
2534
|
-
}
|
|
2535
|
-
return fs9.writeFile(destPath, outputFile.contents);
|
|
2536
|
-
})
|
|
2537
|
-
);
|
|
2538
|
-
} catch (error) {
|
|
2539
|
-
const buildError = { type: deriveBuildErrorType(error), formatted: formatBuildErrors(error) };
|
|
2540
|
-
const errorHtml = buildErrorHTML(buildError);
|
|
2541
|
-
await Promise.all(
|
|
2542
|
-
activeGroups.map((group) => {
|
|
2543
|
-
group.cachedContent._buildError = buildError;
|
|
2544
|
-
return fs9.writeFile(
|
|
2545
|
-
path7.join(path7.resolve(group.config.projectRoot, group.config.output), "index.html"),
|
|
2546
|
-
errorHtml
|
|
2547
|
-
).catch(
|
|
2548
|
-
(err) => debug && process.stderr.write(`# [qunitx] writeFile index.html: ${err.message}
|
|
2549
|
-
`)
|
|
2550
|
-
);
|
|
2551
|
-
})
|
|
2552
|
-
);
|
|
2553
|
-
throw error;
|
|
2554
|
-
}
|
|
2578
|
+
function toEsbuildImportPath(filePath) {
|
|
2579
|
+
const rel = path7.relative(process.cwd(), filePath);
|
|
2580
|
+
const normalized = rel.replace(/\\/g, "/");
|
|
2581
|
+
if (path7.isAbsolute(rel)) return filePath.replace(/\\/g, "/");
|
|
2582
|
+
return normalized.startsWith(".") ? normalized : "./" + normalized;
|
|
2555
2583
|
}
|
|
2556
|
-
var
|
|
2584
|
+
var ancestorNodeModules, ANCESTOR_NODE_MODULES, RETRY_DELAY_MS, MAX_RETRIES, EMPTY_BUNDLE_THRESHOLD, NAV_GRACE_MS, MIN_NAV_MS, STARTUP_TIMEOUT_FACTOR, TESTS_JS_TIMEOUT_FACTOR, CONSOLE_FLUSH_TIMEOUT_MS, TEST_STALL_BUFFER_MS, GROUP_OUTPUT_REGEX, BundleError;
|
|
2557
2585
|
var init_tests_in_browser = __esm({
|
|
2558
2586
|
"lib/commands/run/tests-in-browser.ts"() {
|
|
2559
2587
|
init_color();
|
|
@@ -2563,13 +2591,6 @@ var init_tests_in_browser = __esm({
|
|
|
2563
2591
|
init_display_final_result();
|
|
2564
2592
|
init_web_server();
|
|
2565
2593
|
init_source_map_decoder();
|
|
2566
|
-
BundleError = class extends Error {
|
|
2567
|
-
constructor(message) {
|
|
2568
|
-
super(message);
|
|
2569
|
-
this.name = "BundleError";
|
|
2570
|
-
this.message = `esbuild Bundle Error: ${message}`.split("\n").join("\n# ");
|
|
2571
|
-
}
|
|
2572
|
-
};
|
|
2573
2594
|
ancestorNodeModules = (dir) => dir.split(path7.sep).map(
|
|
2574
2595
|
(_, i, parts) => path7.join(parts.slice(0, parts.length - i).join(path7.sep) || path7.sep, "node_modules")
|
|
2575
2596
|
);
|
|
@@ -2578,11 +2599,19 @@ var init_tests_in_browser = __esm({
|
|
|
2578
2599
|
MAX_RETRIES = 3;
|
|
2579
2600
|
EMPTY_BUNDLE_THRESHOLD = 500;
|
|
2580
2601
|
NAV_GRACE_MS = 1e4;
|
|
2602
|
+
MIN_NAV_MS = 3e4;
|
|
2581
2603
|
STARTUP_TIMEOUT_FACTOR = 3;
|
|
2582
2604
|
TESTS_JS_TIMEOUT_FACTOR = 4;
|
|
2583
2605
|
CONSOLE_FLUSH_TIMEOUT_MS = 2e3;
|
|
2584
2606
|
TEST_STALL_BUFFER_MS = 5e3;
|
|
2585
2607
|
GROUP_OUTPUT_REGEX = /group-(\d+)\.js(\.map)?$/;
|
|
2608
|
+
BundleError = class extends Error {
|
|
2609
|
+
constructor(message) {
|
|
2610
|
+
super(message);
|
|
2611
|
+
this.name = "BundleError";
|
|
2612
|
+
this.message = `esbuild Bundle Error: ${message}`.split("\n").join("\n# ");
|
|
2613
|
+
}
|
|
2614
|
+
};
|
|
2586
2615
|
}
|
|
2587
2616
|
});
|
|
2588
2617
|
|
|
@@ -2594,6 +2623,7 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
2594
2623
|
const extensions = config.extensions || ["js", "ts"];
|
|
2595
2624
|
const readyPromises = [];
|
|
2596
2625
|
const parentWatchers = [];
|
|
2626
|
+
const rescanTimers = [];
|
|
2597
2627
|
const fileWatchers = {};
|
|
2598
2628
|
const symlinkPollers = /* @__PURE__ */ new Map();
|
|
2599
2629
|
function trackSymlink(filePath) {
|
|
@@ -2620,20 +2650,24 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
2620
2650
|
}
|
|
2621
2651
|
for (const watchPath of testFileLookupPaths) {
|
|
2622
2652
|
let ready = false;
|
|
2653
|
+
let rescanInProgress = false;
|
|
2623
2654
|
const lastEventMs = {};
|
|
2624
2655
|
const seenMtimeMs = {};
|
|
2625
2656
|
const childWatcher = fs10.watch(watchPath, { recursive: true }, async (eventType, filename) => {
|
|
2626
2657
|
if (!ready) return;
|
|
2627
2658
|
if (!filename) {
|
|
2628
|
-
if (process.platform === "darwin") {
|
|
2629
|
-
|
|
2659
|
+
if (process.platform === "darwin" && !rescanInProgress) {
|
|
2660
|
+
rescanInProgress = true;
|
|
2661
|
+
rescanDirectoryForDelta(
|
|
2630
2662
|
watchPath,
|
|
2631
2663
|
config,
|
|
2632
2664
|
extensions,
|
|
2633
2665
|
onEventFunc,
|
|
2634
2666
|
onFinishFunc,
|
|
2635
2667
|
trackSymlink
|
|
2636
|
-
)
|
|
2668
|
+
).finally(() => {
|
|
2669
|
+
rescanInProgress = false;
|
|
2670
|
+
});
|
|
2637
2671
|
}
|
|
2638
2672
|
return;
|
|
2639
2673
|
}
|
|
@@ -2693,6 +2727,24 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
2693
2727
|
})
|
|
2694
2728
|
)
|
|
2695
2729
|
);
|
|
2730
|
+
if (process.platform === "darwin") {
|
|
2731
|
+
rescanTimers.push(
|
|
2732
|
+
setInterval(() => {
|
|
2733
|
+
if (!ready || rescanInProgress) return;
|
|
2734
|
+
rescanInProgress = true;
|
|
2735
|
+
rescanDirectoryForDelta(
|
|
2736
|
+
watchPath,
|
|
2737
|
+
config,
|
|
2738
|
+
extensions,
|
|
2739
|
+
onEventFunc,
|
|
2740
|
+
onFinishFunc,
|
|
2741
|
+
trackSymlink
|
|
2742
|
+
).finally(() => {
|
|
2743
|
+
rescanInProgress = false;
|
|
2744
|
+
});
|
|
2745
|
+
}, RESCAN_INTERVAL_MS).unref()
|
|
2746
|
+
);
|
|
2747
|
+
}
|
|
2696
2748
|
}
|
|
2697
2749
|
readyPromises.push(
|
|
2698
2750
|
(async () => {
|
|
@@ -2712,28 +2764,13 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
2712
2764
|
killFileWatchers() {
|
|
2713
2765
|
Object.keys(fileWatchers).forEach((key) => fileWatchers[key].close());
|
|
2714
2766
|
parentWatchers.forEach((pw) => pw.close());
|
|
2767
|
+
rescanTimers.forEach((t) => clearInterval(t));
|
|
2715
2768
|
symlinkPollers.forEach((cancel) => cancel());
|
|
2716
2769
|
symlinkPollers.clear();
|
|
2717
2770
|
return fileWatchers;
|
|
2718
2771
|
}
|
|
2719
2772
|
};
|
|
2720
2773
|
}
|
|
2721
|
-
async function classifyRenameEvent(fullPath, fsTree) {
|
|
2722
|
-
for (const delay of [0, OVERLAYFS_RENAME_RETRY_MS]) {
|
|
2723
|
-
if (delay) await new Promise((resolve) => setTimeout(resolve, delay));
|
|
2724
|
-
try {
|
|
2725
|
-
const statResult = await stat(fullPath);
|
|
2726
|
-
if (statResult.isDirectory()) return "addDir";
|
|
2727
|
-
return fsTree && fullPath in fsTree ? "change" : "add";
|
|
2728
|
-
} catch {
|
|
2729
|
-
}
|
|
2730
|
-
}
|
|
2731
|
-
if (!fsTree) return null;
|
|
2732
|
-
if (fullPath in fsTree) return "unlink";
|
|
2733
|
-
return Object.keys(fsTree).some(
|
|
2734
|
-
(trackedPath) => trackedPath.startsWith(fullPath + "/") || trackedPath.startsWith(fullPath + "\\")
|
|
2735
|
-
) ? "unlinkDir" : null;
|
|
2736
|
-
}
|
|
2737
2774
|
function handleWatchEvent(config, extensions, event, filePath, onEventFunc, onFinishFunc) {
|
|
2738
2775
|
if (event !== "unlinkDir" && !extensions.some((ext) => filePath.endsWith(`.${ext}`)))
|
|
2739
2776
|
return Promise.resolve();
|
|
@@ -2776,9 +2813,16 @@ async function rescanDirectoryForDelta(watchPath, config, extensions, onEventFun
|
|
|
2776
2813
|
try {
|
|
2777
2814
|
const entries = await readdir(watchPath, { withFileTypes: true, recursive: true });
|
|
2778
2815
|
const presentPaths = /* @__PURE__ */ new Set();
|
|
2816
|
+
const presentDirs = /* @__PURE__ */ new Set();
|
|
2817
|
+
presentDirs.add(watchPath);
|
|
2779
2818
|
for (const entry of entries) {
|
|
2819
|
+
if (entry.isDirectory()) {
|
|
2820
|
+
presentDirs.add(path8.join(entry.parentPath, entry.name));
|
|
2821
|
+
continue;
|
|
2822
|
+
}
|
|
2780
2823
|
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
|
|
2781
2824
|
const entryPath = path8.join(entry.parentPath, entry.name);
|
|
2825
|
+
presentDirs.add(entry.parentPath);
|
|
2782
2826
|
if (!extensions.some((ext) => entryPath.endsWith(`.${ext}`))) continue;
|
|
2783
2827
|
presentPaths.add(entryPath);
|
|
2784
2828
|
if (!(entryPath in config.fsTree)) {
|
|
@@ -2787,9 +2831,18 @@ async function rescanDirectoryForDelta(watchPath, config, extensions, onEventFun
|
|
|
2787
2831
|
}
|
|
2788
2832
|
}
|
|
2789
2833
|
const watchPrefix = watchPath + path8.sep;
|
|
2834
|
+
const firedDirPrefixes = [];
|
|
2790
2835
|
for (const trackedPath of Object.keys(config.fsTree)) {
|
|
2791
|
-
if (trackedPath.startsWith(watchPrefix)
|
|
2836
|
+
if (!trackedPath.startsWith(watchPrefix) || presentPaths.has(trackedPath)) continue;
|
|
2837
|
+
if (firedDirPrefixes.some((p) => trackedPath.startsWith(p + path8.sep))) continue;
|
|
2838
|
+
const parts = trackedPath.slice(watchPrefix.length).split(path8.sep);
|
|
2839
|
+
const goneDirPath = parts.slice(0, -1).map((_, i) => watchPrefix + parts.slice(0, i + 1).join(path8.sep)).find((p) => !presentDirs.has(p)) ?? null;
|
|
2840
|
+
if (goneDirPath !== null) {
|
|
2841
|
+
firedDirPrefixes.push(goneDirPath);
|
|
2842
|
+
handleWatchEvent(config, extensions, "unlinkDir", goneDirPath, onEventFunc, onFinishFunc);
|
|
2843
|
+
} else {
|
|
2792
2844
|
handleWatchEvent(config, extensions, "unlink", trackedPath, onEventFunc, onFinishFunc);
|
|
2845
|
+
}
|
|
2793
2846
|
}
|
|
2794
2847
|
} catch {
|
|
2795
2848
|
}
|
|
@@ -2806,18 +2859,35 @@ function mutateFSTree(fsTree, event, filePath) {
|
|
|
2806
2859
|
}
|
|
2807
2860
|
}
|
|
2808
2861
|
}
|
|
2862
|
+
async function classifyRenameEvent(fullPath, fsTree) {
|
|
2863
|
+
for (const delay of [0, OVERLAYFS_RENAME_RETRY_MS]) {
|
|
2864
|
+
if (delay) await new Promise((resolve) => setTimeout(resolve, delay));
|
|
2865
|
+
try {
|
|
2866
|
+
const statResult = await stat(fullPath);
|
|
2867
|
+
if (statResult.isDirectory()) return "addDir";
|
|
2868
|
+
return fsTree && fullPath in fsTree ? "change" : "add";
|
|
2869
|
+
} catch {
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
if (!fsTree) return null;
|
|
2873
|
+
if (fullPath in fsTree) return "unlink";
|
|
2874
|
+
return Object.keys(fsTree).some(
|
|
2875
|
+
(trackedPath) => trackedPath.startsWith(fullPath + "/") || trackedPath.startsWith(fullPath + "\\")
|
|
2876
|
+
) ? "unlinkDir" : null;
|
|
2877
|
+
}
|
|
2809
2878
|
function colorEvent(event) {
|
|
2810
2879
|
if (event === "change") return yellow("CHANGED:");
|
|
2811
2880
|
if (event === "add" || event === "addDir") return green("ADDED:");
|
|
2812
2881
|
return red("REMOVED:");
|
|
2813
2882
|
}
|
|
2814
|
-
var CHANGE_DEDUPE_MS, SYMLINK_POLL_INTERVAL_MS, OVERLAYFS_RENAME_RETRY_MS;
|
|
2883
|
+
var CHANGE_DEDUPE_MS, SYMLINK_POLL_INTERVAL_MS, OVERLAYFS_RENAME_RETRY_MS, RESCAN_INTERVAL_MS;
|
|
2815
2884
|
var init_file_watcher = __esm({
|
|
2816
2885
|
"lib/setup/file-watcher.ts"() {
|
|
2817
2886
|
init_color();
|
|
2818
2887
|
CHANGE_DEDUPE_MS = 10;
|
|
2819
2888
|
SYMLINK_POLL_INTERVAL_MS = 500;
|
|
2820
2889
|
OVERLAYFS_RENAME_RETRY_MS = 50;
|
|
2890
|
+
RESCAN_INTERVAL_MS = 1e3;
|
|
2821
2891
|
}
|
|
2822
2892
|
});
|
|
2823
2893
|
|
|
@@ -3304,7 +3374,7 @@ var init_run = __esm({
|
|
|
3304
3374
|
"lib/commands/run.ts"() {
|
|
3305
3375
|
init_browser();
|
|
3306
3376
|
init_chrome_prelaunch();
|
|
3307
|
-
|
|
3377
|
+
init_web();
|
|
3308
3378
|
init_bind_server_to_port();
|
|
3309
3379
|
init_web_server();
|
|
3310
3380
|
init_open_output_in_browser();
|
|
@@ -3338,7 +3408,7 @@ init_color();
|
|
|
3338
3408
|
var package_default = {
|
|
3339
3409
|
name: "qunitx-cli",
|
|
3340
3410
|
type: "module",
|
|
3341
|
-
version: "0.21.
|
|
3411
|
+
version: "0.21.3",
|
|
3342
3412
|
description: "Browser runner for QUnitx: run your qunitx tests in google-chrome",
|
|
3343
3413
|
author: "Izel Nakri",
|
|
3344
3414
|
license: "MIT",
|
|
@@ -3565,12 +3635,6 @@ function convertToPascalCase(str) {
|
|
|
3565
3635
|
}
|
|
3566
3636
|
|
|
3567
3637
|
// lib/commands/generate.ts
|
|
3568
|
-
function pathToModuleName(filePath) {
|
|
3569
|
-
const withoutExt = filePath.replace(/\.(js|ts)$/, "");
|
|
3570
|
-
const segments = withoutExt.split("/");
|
|
3571
|
-
const targetNames = segments[0] === "test" || segments[0] === "tests" ? segments.slice(1) : segments;
|
|
3572
|
-
return targetNames.map(convertToPascalCase).join(" | ");
|
|
3573
|
-
}
|
|
3574
3638
|
async function generateTestFiles() {
|
|
3575
3639
|
const projectRoot = await findProjectRoot();
|
|
3576
3640
|
const moduleName = pathToModuleName(process.argv[3]);
|
|
@@ -3586,6 +3650,12 @@ async function generateTestFiles() {
|
|
|
3586
3650
|
await fs5.writeFile(path10, testJSContent.replace("{{moduleName}}", moduleName));
|
|
3587
3651
|
console.log(green(`${path10} written`));
|
|
3588
3652
|
}
|
|
3653
|
+
function pathToModuleName(filePath) {
|
|
3654
|
+
const withoutExt = filePath.replace(/\.(js|ts)$/, "");
|
|
3655
|
+
const segments = withoutExt.split("/");
|
|
3656
|
+
const targetNames = segments[0] === "test" || segments[0] === "tests" ? segments.slice(1) : segments;
|
|
3657
|
+
return targetNames.map(convertToPascalCase).join(" | ");
|
|
3658
|
+
}
|
|
3589
3659
|
|
|
3590
3660
|
// lib/setup/config.ts
|
|
3591
3661
|
import fs7 from "node:fs/promises";
|
|
@@ -3593,28 +3663,6 @@ import fs7 from "node:fs/promises";
|
|
|
3593
3663
|
// lib/setup/fs-tree.ts
|
|
3594
3664
|
import fs6, { glob as fsGlob } from "node:fs/promises";
|
|
3595
3665
|
import path3 from "node:path";
|
|
3596
|
-
function isGlob(str) {
|
|
3597
|
-
return /[*?{[]/.test(str);
|
|
3598
|
-
}
|
|
3599
|
-
async function readDirRecursive(dir, filter) {
|
|
3600
|
-
const entries = await fs6.readdir(dir, { recursive: true, withFileTypes: true });
|
|
3601
|
-
const candidates = entries.filter(
|
|
3602
|
-
(dirent) => (dirent.isFile() || dirent.isSymbolicLink()) && filter(dirent.name)
|
|
3603
|
-
);
|
|
3604
|
-
const resolvedPaths = await Promise.all(
|
|
3605
|
-
candidates.map(async (dirent) => {
|
|
3606
|
-
const fullPath = path3.join(dirent.parentPath, dirent.name);
|
|
3607
|
-
if (dirent.isFile()) return fullPath;
|
|
3608
|
-
try {
|
|
3609
|
-
const statResult = await fs6.stat(fullPath);
|
|
3610
|
-
return statResult.isFile() ? fullPath : null;
|
|
3611
|
-
} catch {
|
|
3612
|
-
return null;
|
|
3613
|
-
}
|
|
3614
|
-
})
|
|
3615
|
-
);
|
|
3616
|
-
return resolvedPaths.filter((resolvedPath) => resolvedPath !== null);
|
|
3617
|
-
}
|
|
3618
3666
|
async function buildFSTree(fileAbsolutePaths, config = {}) {
|
|
3619
3667
|
const targetExtensions = config.extensions || ["js", "ts"];
|
|
3620
3668
|
const fsTree = {};
|
|
@@ -3648,57 +3696,65 @@ async function buildFSTree(fileAbsolutePaths, config = {}) {
|
|
|
3648
3696
|
);
|
|
3649
3697
|
return fsTree;
|
|
3650
3698
|
}
|
|
3651
|
-
|
|
3652
|
-
// lib/setup/test-file-paths.ts
|
|
3653
|
-
import { matchesGlob } from "node:path";
|
|
3654
|
-
function isGlob2(str) {
|
|
3699
|
+
function isGlob(str) {
|
|
3655
3700
|
return /[*?{[]/.test(str);
|
|
3656
3701
|
}
|
|
3657
|
-
function
|
|
3658
|
-
const
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3702
|
+
async function readDirRecursive(dir, filter) {
|
|
3703
|
+
const entries = await fs6.readdir(dir, { recursive: true, withFileTypes: true });
|
|
3704
|
+
const candidates = entries.filter(
|
|
3705
|
+
(dirent) => (dirent.isFile() || dirent.isSymbolicLink()) && filter(dirent.name)
|
|
3706
|
+
);
|
|
3707
|
+
const resolvedPaths = await Promise.all(
|
|
3708
|
+
candidates.map(async (dirent) => {
|
|
3709
|
+
const fullPath = path3.join(dirent.parentPath, dirent.name);
|
|
3710
|
+
if (dirent.isFile()) return fullPath;
|
|
3711
|
+
try {
|
|
3712
|
+
const statResult = await fs6.stat(fullPath);
|
|
3713
|
+
return statResult.isFile() ? fullPath : null;
|
|
3714
|
+
} catch {
|
|
3715
|
+
return null;
|
|
3665
3716
|
}
|
|
3666
|
-
|
|
3667
|
-
},
|
|
3668
|
-
[[], [], []]
|
|
3717
|
+
})
|
|
3669
3718
|
);
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3719
|
+
return resolvedPaths.filter((resolvedPath) => resolvedPath !== null);
|
|
3720
|
+
}
|
|
3721
|
+
|
|
3722
|
+
// lib/setup/test-file-paths.ts
|
|
3723
|
+
import { matchesGlob } from "node:path";
|
|
3724
|
+
var GLOB_CHARS = /[*?{[]/;
|
|
3725
|
+
function setupTestFilePaths(inputs2) {
|
|
3726
|
+
const folders = [];
|
|
3727
|
+
const filesWithGlob = [];
|
|
3728
|
+
const filesWithoutGlob = [];
|
|
3729
|
+
inputs2.forEach((input) => {
|
|
3730
|
+
if (!pathIsFile(input)) {
|
|
3731
|
+
folders.push({ input, globFormat: `${input}/**` });
|
|
3732
|
+
} else if (isGlob2(input)) {
|
|
3733
|
+
filesWithGlob.push({ input, globFormat: input });
|
|
3734
|
+
} else {
|
|
3735
|
+
filesWithoutGlob.push({ input, globFormat: input });
|
|
3679
3736
|
}
|
|
3680
3737
|
});
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3738
|
+
const dedupedFolders = folders.filter((folder) => !isIncludedIn(folders, folder));
|
|
3739
|
+
const dedupedGlobFiles = filesWithGlob.filter(
|
|
3740
|
+
(file) => !isIncludedIn(dedupedFolders, file) && !isIncludedIn(filesWithGlob, file)
|
|
3741
|
+
);
|
|
3742
|
+
const dedupedPlainFiles = filesWithoutGlob.reduce((acc, file) => {
|
|
3743
|
+
if (!isIncludedIn(dedupedFolders, file) && !isIncludedIn(dedupedGlobFiles, file) && !isIncludedIn(acc, file)) {
|
|
3744
|
+
acc.push(file);
|
|
3684
3745
|
}
|
|
3685
|
-
|
|
3686
|
-
|
|
3746
|
+
return acc;
|
|
3747
|
+
}, []);
|
|
3748
|
+
return dedupedFolders.concat(dedupedGlobFiles, dedupedPlainFiles).map((meta) => meta.input);
|
|
3687
3749
|
}
|
|
3688
3750
|
function pathIsFile(path10) {
|
|
3689
|
-
|
|
3690
|
-
return inputs2[inputs2.length - 1].includes(".");
|
|
3751
|
+
return path10.includes(".", path10.lastIndexOf("/") + 1);
|
|
3691
3752
|
}
|
|
3692
|
-
function
|
|
3693
|
-
return paths.some((path10) =>
|
|
3694
|
-
if (path10 === targetPath) {
|
|
3695
|
-
return false;
|
|
3696
|
-
}
|
|
3697
|
-
return matchesGlob(targetPath.input, buildGlobFormat(path10));
|
|
3698
|
-
});
|
|
3753
|
+
function isIncludedIn(paths, target) {
|
|
3754
|
+
return paths.some((path10) => path10 !== target && matchesGlob(target.input, path10.globFormat));
|
|
3699
3755
|
}
|
|
3700
|
-
function
|
|
3701
|
-
return
|
|
3756
|
+
function isGlob2(str) {
|
|
3757
|
+
return GLOB_CHARS.test(str);
|
|
3702
3758
|
}
|
|
3703
3759
|
|
|
3704
3760
|
// lib/utils/parse-cli-flags.ts
|
|
@@ -3801,7 +3857,7 @@ async function setupConfig() {
|
|
|
3801
3857
|
...cliConfigFlags,
|
|
3802
3858
|
projectRoot,
|
|
3803
3859
|
inputs: inputs2,
|
|
3804
|
-
testFileLookupPaths: setupTestFilePaths(
|
|
3860
|
+
testFileLookupPaths: setupTestFilePaths(inputs2),
|
|
3805
3861
|
lastFailedTestFiles: null,
|
|
3806
3862
|
lastRanTestFiles: null,
|
|
3807
3863
|
COUNTER: {
|