qunitx-cli 0.21.1 → 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 +743 -652
- 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));
|
|
@@ -2266,174 +2286,10 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
2266
2286
|
}
|
|
2267
2287
|
return connections;
|
|
2268
2288
|
}
|
|
2269
|
-
function buildFilteredTests(filteredTests, outputPath, config) {
|
|
2270
|
-
const sourcemap = "inline";
|
|
2271
|
-
const needsDisk = Boolean(config.open);
|
|
2272
|
-
return buildWithOverlayfsRetry(
|
|
2273
|
-
{
|
|
2274
|
-
stdin: {
|
|
2275
|
-
contents: filteredTests.map((filePath) => `import "${filePath.replace(/\\/g, "/")}";`).join(""),
|
|
2276
|
-
resolveDir: process.cwd()
|
|
2277
|
-
},
|
|
2278
|
-
nodePaths: ANCESTOR_NODE_MODULES,
|
|
2279
|
-
bundle: true,
|
|
2280
|
-
logLevel: "silent",
|
|
2281
|
-
outfile: outputPath,
|
|
2282
|
-
legalComments: "none",
|
|
2283
|
-
target: esbuildTarget(config.browser),
|
|
2284
|
-
sourcemap,
|
|
2285
|
-
footer: { js: 'window.dispatchEvent(new CustomEvent("qunitx:tests-ready"));' }
|
|
2286
|
-
},
|
|
2287
|
-
needsDisk
|
|
2288
|
-
);
|
|
2289
|
-
}
|
|
2290
|
-
async function runWithOverlayfsRetry(getContents, needsDisk) {
|
|
2291
|
-
let { result, js } = await getContents();
|
|
2292
|
-
const initialSize = js.length;
|
|
2293
|
-
for (let retry = 1; retry <= MAX_RETRIES; retry++) {
|
|
2294
|
-
if (js.length >= EMPTY_BUNDLE_THRESHOLD) break;
|
|
2295
|
-
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
|
|
2296
|
-
({ result, js } = await getContents());
|
|
2297
|
-
}
|
|
2298
|
-
if (js.length < EMPTY_BUNDLE_THRESHOLD && js.length !== initialSize) {
|
|
2299
|
-
console.log(
|
|
2300
|
-
`# [buildWithOverlayfsRetry] bundle is ${js.length} bytes after ${MAX_RETRIES} retries \u2014 proceeding`
|
|
2301
|
-
);
|
|
2302
|
-
}
|
|
2303
|
-
if (needsDisk) {
|
|
2304
|
-
await Promise.all(
|
|
2305
|
-
result.outputFiles.map((outputFile) => fs9.writeFile(outputFile.path, outputFile.contents))
|
|
2306
|
-
);
|
|
2307
|
-
}
|
|
2308
|
-
return js;
|
|
2309
|
-
}
|
|
2310
|
-
function buildWithOverlayfsRetry(options, needsDisk) {
|
|
2311
|
-
const buildOpts = { ...options, write: false };
|
|
2312
|
-
return runWithOverlayfsRetry(async () => {
|
|
2313
|
-
const result = await esbuild.build(buildOpts);
|
|
2314
|
-
const jsFile = result.outputFiles.find((f) => !f.path.endsWith(".map"));
|
|
2315
|
-
return { result, js: Buffer.from(jsFile.contents) };
|
|
2316
|
-
}, needsDisk);
|
|
2317
|
-
}
|
|
2318
|
-
async function buildIncrementally(options, fileKey, cachedContent, needsDisk) {
|
|
2319
|
-
const buildOpts = { ...options, write: false };
|
|
2320
|
-
if (!cachedContent._esbuildContext || cachedContent._esbuildContextKey !== fileKey) {
|
|
2321
|
-
cachedContent._esbuildContext?.dispose().catch(() => {
|
|
2322
|
-
});
|
|
2323
|
-
cachedContent._esbuildContext = await esbuild.context(buildOpts);
|
|
2324
|
-
cachedContent._esbuildContextKey = fileKey;
|
|
2325
|
-
}
|
|
2326
|
-
const ctx = cachedContent._esbuildContext;
|
|
2327
|
-
return runWithOverlayfsRetry(async () => {
|
|
2328
|
-
const result = await ctx.rebuild();
|
|
2329
|
-
const jsFile = result.outputFiles.find((f) => !f.path.endsWith(".map"));
|
|
2330
|
-
return { result, js: Buffer.from(jsFile.contents) };
|
|
2331
|
-
}, needsDisk);
|
|
2332
|
-
}
|
|
2333
|
-
function esbuildTarget(browser) {
|
|
2334
|
-
if (browser === "firefox") return ["firefox115"];
|
|
2335
|
-
if (browser === "webkit") return ["safari16"];
|
|
2336
|
-
return ["chrome120"];
|
|
2337
|
-
}
|
|
2338
|
-
async function runTestInsideHTMLFile(filePath, { page, server, browser }, config) {
|
|
2339
|
-
let QUNIT_RESULT;
|
|
2340
|
-
let targetError;
|
|
2341
|
-
let timeoutHandle;
|
|
2342
|
-
let wsConnected = false;
|
|
2343
|
-
try {
|
|
2344
|
-
console.log("#", blue(`QUnitX running: http://localhost:${config.port}${filePath}`));
|
|
2345
|
-
const navMs = config.timeout + NAV_GRACE_MS;
|
|
2346
|
-
const startupMs = Math.max(config.timeout * STARTUP_TIMEOUT_FACTOR, navMs);
|
|
2347
|
-
const testsJsMs = Math.max(config.timeout * TESTS_JS_TIMEOUT_FACTOR, navMs);
|
|
2348
|
-
let resolveTestRace;
|
|
2349
|
-
const testRaceResult = new Promise((resolve) => {
|
|
2350
|
-
resolveTestRace = resolve;
|
|
2351
|
-
});
|
|
2352
|
-
config._testRunDone = resolveTestRace;
|
|
2353
|
-
config._onWsOpen = () => {
|
|
2354
|
-
wsConnected = true;
|
|
2355
|
-
clearTimeout(timeoutHandle);
|
|
2356
|
-
timeoutHandle = setTimeout(resolveTestRace, startupMs);
|
|
2357
|
-
};
|
|
2358
|
-
config._onTestsJsServed = () => {
|
|
2359
|
-
clearTimeout(timeoutHandle);
|
|
2360
|
-
timeoutHandle = setTimeout(resolveTestRace, testsJsMs);
|
|
2361
|
-
};
|
|
2362
|
-
config._resetTestTimeout = () => {
|
|
2363
|
-
wsConnected = true;
|
|
2364
|
-
clearTimeout(timeoutHandle);
|
|
2365
|
-
timeoutHandle = setTimeout(resolveTestRace, config.timeout + TEST_STALL_BUFFER_MS);
|
|
2366
|
-
};
|
|
2367
|
-
const targetUrl = `http://localhost:${config.port}${filePath}`;
|
|
2368
|
-
const navOptions = { timeout: navMs, waitUntil: "commit" };
|
|
2369
|
-
if (page.url().split("?")[0] === targetUrl) {
|
|
2370
|
-
await page.reload(navOptions);
|
|
2371
|
-
} else {
|
|
2372
|
-
await page.goto(targetUrl, navOptions);
|
|
2373
|
-
}
|
|
2374
|
-
clearTimeout(timeoutHandle);
|
|
2375
|
-
timeoutHandle = setTimeout(resolveTestRace, startupMs);
|
|
2376
|
-
await testRaceResult;
|
|
2377
|
-
QUNIT_RESULT = config._lastQUnitResult ?? await page.evaluate(() => window.QUNIT_RESULT);
|
|
2378
|
-
} catch (error) {
|
|
2379
|
-
targetError = error;
|
|
2380
|
-
console.log(error);
|
|
2381
|
-
console.error(error);
|
|
2382
|
-
} finally {
|
|
2383
|
-
clearTimeout(timeoutHandle);
|
|
2384
|
-
config._onWsOpen = null;
|
|
2385
|
-
config._onTestsJsServed = null;
|
|
2386
|
-
config._resetTestTimeout = null;
|
|
2387
|
-
config._testRunDone = null;
|
|
2388
|
-
config._lastQUnitResult = null;
|
|
2389
|
-
}
|
|
2390
|
-
if (!QUNIT_RESULT) {
|
|
2391
|
-
if (targetError) console.log(targetError);
|
|
2392
|
-
const wsReason = !wsConnected ? "WebSocket connection never received \u2014 Chrome may be CPU-starved or the page failed to load" : "WebSocket connected but no tests ran \u2014 QUnit may have failed to start";
|
|
2393
|
-
console.log(`# TIMEOUT: ${wsReason}`);
|
|
2394
|
-
console.log("BROWSER: runtime error thrown during executing tests");
|
|
2395
|
-
console.error("BROWSER: runtime error thrown during executing tests");
|
|
2396
|
-
await failOnNonWatchMode(
|
|
2397
|
-
config.watch,
|
|
2398
|
-
{ server, browser },
|
|
2399
|
-
config._groupMode,
|
|
2400
|
-
config._pendingConsoleHandlers
|
|
2401
|
-
);
|
|
2402
|
-
} else if (QUNIT_RESULT.totalTests === 0) {
|
|
2403
|
-
return;
|
|
2404
|
-
} else if (QUNIT_RESULT.totalTests > QUNIT_RESULT.finishedTests) {
|
|
2405
|
-
if (targetError) console.log(targetError);
|
|
2406
|
-
console.log(
|
|
2407
|
-
`# TIMEOUT: test stalled after ${QUNIT_RESULT.finishedTests}/${QUNIT_RESULT.totalTests} finished \u2014 last active: ${QUNIT_RESULT.currentTest}`
|
|
2408
|
-
);
|
|
2409
|
-
console.log(`BROWSER: TEST TIMED OUT: ${QUNIT_RESULT.currentTest}`);
|
|
2410
|
-
console.error(`BROWSER: TEST TIMED OUT: ${QUNIT_RESULT.currentTest}`);
|
|
2411
|
-
await failOnNonWatchMode(
|
|
2412
|
-
config.watch,
|
|
2413
|
-
{ server, browser },
|
|
2414
|
-
config._groupMode,
|
|
2415
|
-
config._pendingConsoleHandlers
|
|
2416
|
-
);
|
|
2417
|
-
} else if (QUNIT_RESULT.failedTests > config.COUNTER.failCount) {
|
|
2418
|
-
config.COUNTER.failCount = QUNIT_RESULT.failedTests;
|
|
2419
|
-
}
|
|
2420
|
-
}
|
|
2421
|
-
async function failOnNonWatchMode(watchMode = false, connections = {}, groupMode = false, pendingHandlers) {
|
|
2422
|
-
if (!watchMode) {
|
|
2423
|
-
if (groupMode) {
|
|
2424
|
-
throw new Error("Browser test run failed");
|
|
2425
|
-
}
|
|
2426
|
-
await flushConsoleHandlers(pendingHandlers);
|
|
2427
|
-
await Promise.all([
|
|
2428
|
-
connections.server && connections.server.close(),
|
|
2429
|
-
connections.browser && connections.browser.close()
|
|
2430
|
-
]);
|
|
2431
|
-
await shutdownPrelaunch();
|
|
2432
|
-
process.exit(1);
|
|
2433
|
-
}
|
|
2434
|
-
}
|
|
2435
2289
|
async function flushConsoleHandlers(handlers, deadline = Date.now() + CONSOLE_FLUSH_TIMEOUT_MS) {
|
|
2436
|
-
if (!handlers ||
|
|
2290
|
+
if (!handlers || Date.now() >= deadline) return;
|
|
2291
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
2292
|
+
if (handlers.size === 0) return;
|
|
2437
2293
|
await Promise.allSettled([...handlers]);
|
|
2438
2294
|
return flushConsoleHandlers(handlers, deadline);
|
|
2439
2295
|
}
|
|
@@ -2536,24 +2392,196 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
|
2536
2392
|
})
|
|
2537
2393
|
);
|
|
2538
2394
|
} 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
|
-
})
|
|
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
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
function buildFilteredTests(filteredTests, outputPath, config) {
|
|
2413
|
+
const sourcemap = "inline";
|
|
2414
|
+
const needsDisk = Boolean(config.open);
|
|
2415
|
+
return buildWithOverlayfsRetry(
|
|
2416
|
+
{
|
|
2417
|
+
stdin: {
|
|
2418
|
+
contents: filteredTests.map((filePath) => `import "${filePath.replace(/\\/g, "/")}";`).join(""),
|
|
2419
|
+
resolveDir: process.cwd()
|
|
2420
|
+
},
|
|
2421
|
+
nodePaths: ANCESTOR_NODE_MODULES,
|
|
2422
|
+
bundle: true,
|
|
2423
|
+
logLevel: "silent",
|
|
2424
|
+
outfile: outputPath,
|
|
2425
|
+
legalComments: "none",
|
|
2426
|
+
target: esbuildTarget(config.browser),
|
|
2427
|
+
sourcemap,
|
|
2428
|
+
footer: { js: 'window.dispatchEvent(new CustomEvent("qunitx:tests-ready"));' }
|
|
2429
|
+
},
|
|
2430
|
+
needsDisk
|
|
2431
|
+
);
|
|
2432
|
+
}
|
|
2433
|
+
async function runWithOverlayfsRetry(getContents, needsDisk) {
|
|
2434
|
+
let { result, js } = await getContents();
|
|
2435
|
+
const initialSize = js.length;
|
|
2436
|
+
for (let retry = 1; retry <= MAX_RETRIES; retry++) {
|
|
2437
|
+
if (js.length >= EMPTY_BUNDLE_THRESHOLD) break;
|
|
2438
|
+
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
|
|
2439
|
+
({ result, js } = await getContents());
|
|
2440
|
+
}
|
|
2441
|
+
if (js.length < EMPTY_BUNDLE_THRESHOLD && js.length !== initialSize) {
|
|
2442
|
+
console.log(
|
|
2443
|
+
`# [buildWithOverlayfsRetry] bundle is ${js.length} bytes after ${MAX_RETRIES} retries \u2014 proceeding`
|
|
2444
|
+
);
|
|
2445
|
+
}
|
|
2446
|
+
if (needsDisk) {
|
|
2447
|
+
await Promise.all(
|
|
2448
|
+
result.outputFiles.map((outputFile) => fs9.writeFile(outputFile.path, outputFile.contents))
|
|
2449
|
+
);
|
|
2450
|
+
}
|
|
2451
|
+
return js;
|
|
2452
|
+
}
|
|
2453
|
+
function buildWithOverlayfsRetry(options, needsDisk) {
|
|
2454
|
+
const buildOpts = { ...options, write: false };
|
|
2455
|
+
return runWithOverlayfsRetry(async () => {
|
|
2456
|
+
const result = await esbuild.build(buildOpts);
|
|
2457
|
+
const jsFile = result.outputFiles.find((f) => !f.path.endsWith(".map"));
|
|
2458
|
+
return { result, js: Buffer.from(jsFile.contents) };
|
|
2459
|
+
}, needsDisk);
|
|
2460
|
+
}
|
|
2461
|
+
async function buildIncrementally(options, fileKey, cachedContent, needsDisk) {
|
|
2462
|
+
const buildOpts = { ...options, write: false };
|
|
2463
|
+
if (!cachedContent._esbuildContext || cachedContent._esbuildContextKey !== fileKey) {
|
|
2464
|
+
cachedContent._esbuildContext?.dispose().catch(() => {
|
|
2465
|
+
});
|
|
2466
|
+
cachedContent._esbuildContext = await esbuild.context(buildOpts);
|
|
2467
|
+
cachedContent._esbuildContextKey = fileKey;
|
|
2468
|
+
}
|
|
2469
|
+
const ctx = cachedContent._esbuildContext;
|
|
2470
|
+
return runWithOverlayfsRetry(async () => {
|
|
2471
|
+
const result = await ctx.rebuild();
|
|
2472
|
+
const jsFile = result.outputFiles.find((f) => !f.path.endsWith(".map"));
|
|
2473
|
+
return { result, js: Buffer.from(jsFile.contents) };
|
|
2474
|
+
}, needsDisk);
|
|
2475
|
+
}
|
|
2476
|
+
function esbuildTarget(browser) {
|
|
2477
|
+
if (browser === "firefox") return ["firefox115"];
|
|
2478
|
+
if (browser === "webkit") return ["safari16"];
|
|
2479
|
+
return ["chrome120"];
|
|
2480
|
+
}
|
|
2481
|
+
async function runTestInsideHTMLFile(filePath, { page, server, browser }, config) {
|
|
2482
|
+
let QUNIT_RESULT;
|
|
2483
|
+
let targetError;
|
|
2484
|
+
let timeoutHandle;
|
|
2485
|
+
let wsConnected = false;
|
|
2486
|
+
try {
|
|
2487
|
+
console.log("#", blue(`QUnitX running: http://localhost:${config.port}${filePath}`));
|
|
2488
|
+
const navMs = Math.max(config.timeout + NAV_GRACE_MS, MIN_NAV_MS);
|
|
2489
|
+
const startupMs = Math.max(config.timeout * STARTUP_TIMEOUT_FACTOR, navMs);
|
|
2490
|
+
const testsJsMs = Math.max(config.timeout * TESTS_JS_TIMEOUT_FACTOR, navMs);
|
|
2491
|
+
let resolveTestRace;
|
|
2492
|
+
const testRaceResult = new Promise((resolve) => {
|
|
2493
|
+
resolveTestRace = resolve;
|
|
2494
|
+
});
|
|
2495
|
+
config._testRunDone = resolveTestRace;
|
|
2496
|
+
config._onWsOpen = () => {
|
|
2497
|
+
wsConnected = true;
|
|
2498
|
+
clearTimeout(timeoutHandle);
|
|
2499
|
+
timeoutHandle = setTimeout(resolveTestRace, startupMs);
|
|
2500
|
+
};
|
|
2501
|
+
config._onTestsJsServed = () => {
|
|
2502
|
+
clearTimeout(timeoutHandle);
|
|
2503
|
+
timeoutHandle = setTimeout(resolveTestRace, testsJsMs);
|
|
2504
|
+
};
|
|
2505
|
+
config._resetTestTimeout = () => {
|
|
2506
|
+
wsConnected = true;
|
|
2507
|
+
clearTimeout(timeoutHandle);
|
|
2508
|
+
timeoutHandle = setTimeout(resolveTestRace, config.timeout + TEST_STALL_BUFFER_MS);
|
|
2509
|
+
};
|
|
2510
|
+
const targetUrl = `http://localhost:${config.port}${filePath}`;
|
|
2511
|
+
const navOptions = { timeout: navMs, waitUntil: "commit" };
|
|
2512
|
+
if (page.url().split("?")[0] === targetUrl) {
|
|
2513
|
+
await page.reload(navOptions);
|
|
2514
|
+
} else {
|
|
2515
|
+
await page.goto(targetUrl, navOptions);
|
|
2516
|
+
}
|
|
2517
|
+
clearTimeout(timeoutHandle);
|
|
2518
|
+
timeoutHandle = setTimeout(resolveTestRace, startupMs);
|
|
2519
|
+
await testRaceResult;
|
|
2520
|
+
QUNIT_RESULT = config._lastQUnitResult ?? await page.evaluate(() => window.QUNIT_RESULT);
|
|
2521
|
+
} catch (error) {
|
|
2522
|
+
targetError = error;
|
|
2523
|
+
console.log(error);
|
|
2524
|
+
console.error(error);
|
|
2525
|
+
} finally {
|
|
2526
|
+
clearTimeout(timeoutHandle);
|
|
2527
|
+
config._onWsOpen = null;
|
|
2528
|
+
config._onTestsJsServed = null;
|
|
2529
|
+
config._resetTestTimeout = null;
|
|
2530
|
+
config._testRunDone = null;
|
|
2531
|
+
config._lastQUnitResult = null;
|
|
2532
|
+
}
|
|
2533
|
+
if (!QUNIT_RESULT) {
|
|
2534
|
+
if (targetError) console.log(targetError);
|
|
2535
|
+
const wsReason = !wsConnected ? "WebSocket connection never received \u2014 Chrome may be CPU-starved or the page failed to load" : "WebSocket connected but no tests ran \u2014 QUnit may have failed to start";
|
|
2536
|
+
console.log(`# TIMEOUT: ${wsReason}`);
|
|
2537
|
+
console.log("BROWSER: runtime error thrown during executing tests");
|
|
2538
|
+
console.error("BROWSER: runtime error thrown during executing tests");
|
|
2539
|
+
await failOnNonWatchMode(
|
|
2540
|
+
config.watch,
|
|
2541
|
+
{ server, browser },
|
|
2542
|
+
config._groupMode,
|
|
2543
|
+
config._pendingConsoleHandlers
|
|
2552
2544
|
);
|
|
2553
|
-
|
|
2545
|
+
} else if (QUNIT_RESULT.totalTests === 0) {
|
|
2546
|
+
return;
|
|
2547
|
+
} else if (QUNIT_RESULT.totalTests > QUNIT_RESULT.finishedTests) {
|
|
2548
|
+
if (targetError) console.log(targetError);
|
|
2549
|
+
console.log(
|
|
2550
|
+
`# TIMEOUT: test stalled after ${QUNIT_RESULT.finishedTests}/${QUNIT_RESULT.totalTests} finished \u2014 last active: ${QUNIT_RESULT.currentTest}`
|
|
2551
|
+
);
|
|
2552
|
+
console.log(`BROWSER: TEST TIMED OUT: ${QUNIT_RESULT.currentTest}`);
|
|
2553
|
+
console.error(`BROWSER: TEST TIMED OUT: ${QUNIT_RESULT.currentTest}`);
|
|
2554
|
+
await failOnNonWatchMode(
|
|
2555
|
+
config.watch,
|
|
2556
|
+
{ server, browser },
|
|
2557
|
+
config._groupMode,
|
|
2558
|
+
config._pendingConsoleHandlers
|
|
2559
|
+
);
|
|
2560
|
+
} else if (QUNIT_RESULT.failedTests > config.COUNTER.failCount) {
|
|
2561
|
+
config.COUNTER.failCount = QUNIT_RESULT.failedTests;
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
async function failOnNonWatchMode(watchMode = false, connections = {}, groupMode = false, pendingHandlers) {
|
|
2565
|
+
if (!watchMode) {
|
|
2566
|
+
if (groupMode) {
|
|
2567
|
+
throw new Error("Browser test run failed");
|
|
2568
|
+
}
|
|
2569
|
+
await flushConsoleHandlers(pendingHandlers);
|
|
2570
|
+
await Promise.all([
|
|
2571
|
+
connections.server && connections.server.close(),
|
|
2572
|
+
connections.browser && connections.browser.close()
|
|
2573
|
+
]);
|
|
2574
|
+
await shutdownPrelaunch();
|
|
2575
|
+
process.exit(1);
|
|
2554
2576
|
}
|
|
2555
2577
|
}
|
|
2556
|
-
|
|
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;
|
|
2583
|
+
}
|
|
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,22 +2599,31 @@ 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
|
|
|
2589
2618
|
// lib/setup/file-watcher.ts
|
|
2590
2619
|
import fs10 from "node:fs";
|
|
2591
|
-
import { stat, lstat } from "node:fs/promises";
|
|
2620
|
+
import { readdir, stat, lstat } from "node:fs/promises";
|
|
2592
2621
|
import path8 from "node:path";
|
|
2593
2622
|
function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFunc) {
|
|
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,10 +2650,27 @@ 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
|
-
if (!ready
|
|
2657
|
+
if (!ready) return;
|
|
2658
|
+
if (!filename) {
|
|
2659
|
+
if (process.platform === "darwin" && !rescanInProgress) {
|
|
2660
|
+
rescanInProgress = true;
|
|
2661
|
+
rescanDirectoryForDelta(
|
|
2662
|
+
watchPath,
|
|
2663
|
+
config,
|
|
2664
|
+
extensions,
|
|
2665
|
+
onEventFunc,
|
|
2666
|
+
onFinishFunc,
|
|
2667
|
+
trackSymlink
|
|
2668
|
+
).finally(() => {
|
|
2669
|
+
rescanInProgress = false;
|
|
2670
|
+
});
|
|
2671
|
+
}
|
|
2672
|
+
return;
|
|
2673
|
+
}
|
|
2627
2674
|
const fullPath = filename === path8.basename(watchPath) ? watchPath : path8.join(watchPath, filename);
|
|
2628
2675
|
if (eventType === "change") {
|
|
2629
2676
|
const now = Date.now();
|
|
@@ -2680,6 +2727,24 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
2680
2727
|
})
|
|
2681
2728
|
)
|
|
2682
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
|
+
}
|
|
2683
2748
|
}
|
|
2684
2749
|
readyPromises.push(
|
|
2685
2750
|
(async () => {
|
|
@@ -2699,28 +2764,13 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
2699
2764
|
killFileWatchers() {
|
|
2700
2765
|
Object.keys(fileWatchers).forEach((key) => fileWatchers[key].close());
|
|
2701
2766
|
parentWatchers.forEach((pw) => pw.close());
|
|
2767
|
+
rescanTimers.forEach((t) => clearInterval(t));
|
|
2702
2768
|
symlinkPollers.forEach((cancel) => cancel());
|
|
2703
2769
|
symlinkPollers.clear();
|
|
2704
2770
|
return fileWatchers;
|
|
2705
2771
|
}
|
|
2706
2772
|
};
|
|
2707
2773
|
}
|
|
2708
|
-
async function classifyRenameEvent(fullPath, fsTree) {
|
|
2709
|
-
for (const delay of [0, OVERLAYFS_RENAME_RETRY_MS]) {
|
|
2710
|
-
if (delay) await new Promise((resolve) => setTimeout(resolve, delay));
|
|
2711
|
-
try {
|
|
2712
|
-
const statResult = await stat(fullPath);
|
|
2713
|
-
if (statResult.isDirectory()) return "addDir";
|
|
2714
|
-
return fsTree && fullPath in fsTree ? "change" : "add";
|
|
2715
|
-
} catch {
|
|
2716
|
-
}
|
|
2717
|
-
}
|
|
2718
|
-
if (!fsTree) return null;
|
|
2719
|
-
if (fullPath in fsTree) return "unlink";
|
|
2720
|
-
return Object.keys(fsTree).some(
|
|
2721
|
-
(trackedPath) => trackedPath.startsWith(fullPath + "/") || trackedPath.startsWith(fullPath + "\\")
|
|
2722
|
-
) ? "unlinkDir" : null;
|
|
2723
|
-
}
|
|
2724
2774
|
function handleWatchEvent(config, extensions, event, filePath, onEventFunc, onFinishFunc) {
|
|
2725
2775
|
if (event !== "unlinkDir" && !extensions.some((ext) => filePath.endsWith(`.${ext}`)))
|
|
2726
2776
|
return Promise.resolve();
|
|
@@ -2759,6 +2809,44 @@ function handleWatchEvent(config, extensions, event, filePath, onEventFunc, onFi
|
|
|
2759
2809
|
}
|
|
2760
2810
|
});
|
|
2761
2811
|
}
|
|
2812
|
+
async function rescanDirectoryForDelta(watchPath, config, extensions, onEventFunc, onFinishFunc, trackSymlinkFn) {
|
|
2813
|
+
try {
|
|
2814
|
+
const entries = await readdir(watchPath, { withFileTypes: true, recursive: true });
|
|
2815
|
+
const presentPaths = /* @__PURE__ */ new Set();
|
|
2816
|
+
const presentDirs = /* @__PURE__ */ new Set();
|
|
2817
|
+
presentDirs.add(watchPath);
|
|
2818
|
+
for (const entry of entries) {
|
|
2819
|
+
if (entry.isDirectory()) {
|
|
2820
|
+
presentDirs.add(path8.join(entry.parentPath, entry.name));
|
|
2821
|
+
continue;
|
|
2822
|
+
}
|
|
2823
|
+
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
|
|
2824
|
+
const entryPath = path8.join(entry.parentPath, entry.name);
|
|
2825
|
+
presentDirs.add(entry.parentPath);
|
|
2826
|
+
if (!extensions.some((ext) => entryPath.endsWith(`.${ext}`))) continue;
|
|
2827
|
+
presentPaths.add(entryPath);
|
|
2828
|
+
if (!(entryPath in config.fsTree)) {
|
|
2829
|
+
if (entry.isSymbolicLink()) trackSymlinkFn?.(entryPath);
|
|
2830
|
+
handleWatchEvent(config, extensions, "add", entryPath, onEventFunc, onFinishFunc);
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
const watchPrefix = watchPath + path8.sep;
|
|
2834
|
+
const firedDirPrefixes = [];
|
|
2835
|
+
for (const trackedPath of Object.keys(config.fsTree)) {
|
|
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 {
|
|
2844
|
+
handleWatchEvent(config, extensions, "unlink", trackedPath, onEventFunc, onFinishFunc);
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
} catch {
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2762
2850
|
function mutateFSTree(fsTree, event, filePath) {
|
|
2763
2851
|
if (event === "add") {
|
|
2764
2852
|
fsTree[filePath] = null;
|
|
@@ -2771,18 +2859,35 @@ function mutateFSTree(fsTree, event, filePath) {
|
|
|
2771
2859
|
}
|
|
2772
2860
|
}
|
|
2773
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
|
+
}
|
|
2774
2878
|
function colorEvent(event) {
|
|
2775
2879
|
if (event === "change") return yellow("CHANGED:");
|
|
2776
2880
|
if (event === "add" || event === "addDir") return green("ADDED:");
|
|
2777
2881
|
return red("REMOVED:");
|
|
2778
2882
|
}
|
|
2779
|
-
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;
|
|
2780
2884
|
var init_file_watcher = __esm({
|
|
2781
2885
|
"lib/setup/file-watcher.ts"() {
|
|
2782
2886
|
init_color();
|
|
2783
2887
|
CHANGE_DEDUPE_MS = 10;
|
|
2784
2888
|
SYMLINK_POLL_INTERVAL_MS = 500;
|
|
2785
2889
|
OVERLAYFS_RENAME_RETRY_MS = 50;
|
|
2890
|
+
RESCAN_INTERVAL_MS = 1e3;
|
|
2786
2891
|
}
|
|
2787
2892
|
});
|
|
2788
2893
|
|
|
@@ -3269,7 +3374,7 @@ var init_run = __esm({
|
|
|
3269
3374
|
"lib/commands/run.ts"() {
|
|
3270
3375
|
init_browser();
|
|
3271
3376
|
init_chrome_prelaunch();
|
|
3272
|
-
|
|
3377
|
+
init_web();
|
|
3273
3378
|
init_bind_server_to_port();
|
|
3274
3379
|
init_web_server();
|
|
3275
3380
|
init_open_output_in_browser();
|
|
@@ -3303,7 +3408,7 @@ init_color();
|
|
|
3303
3408
|
var package_default = {
|
|
3304
3409
|
name: "qunitx-cli",
|
|
3305
3410
|
type: "module",
|
|
3306
|
-
version: "0.21.
|
|
3411
|
+
version: "0.21.3",
|
|
3307
3412
|
description: "Browser runner for QUnitx: run your qunitx tests in google-chrome",
|
|
3308
3413
|
author: "Izel Nakri",
|
|
3309
3414
|
license: "MIT",
|
|
@@ -3530,12 +3635,6 @@ function convertToPascalCase(str) {
|
|
|
3530
3635
|
}
|
|
3531
3636
|
|
|
3532
3637
|
// lib/commands/generate.ts
|
|
3533
|
-
function pathToModuleName(filePath) {
|
|
3534
|
-
const withoutExt = filePath.replace(/\.(js|ts)$/, "");
|
|
3535
|
-
const segments = withoutExt.split("/");
|
|
3536
|
-
const targetNames = segments[0] === "test" || segments[0] === "tests" ? segments.slice(1) : segments;
|
|
3537
|
-
return targetNames.map(convertToPascalCase).join(" | ");
|
|
3538
|
-
}
|
|
3539
3638
|
async function generateTestFiles() {
|
|
3540
3639
|
const projectRoot = await findProjectRoot();
|
|
3541
3640
|
const moduleName = pathToModuleName(process.argv[3]);
|
|
@@ -3551,6 +3650,12 @@ async function generateTestFiles() {
|
|
|
3551
3650
|
await fs5.writeFile(path10, testJSContent.replace("{{moduleName}}", moduleName));
|
|
3552
3651
|
console.log(green(`${path10} written`));
|
|
3553
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
|
+
}
|
|
3554
3659
|
|
|
3555
3660
|
// lib/setup/config.ts
|
|
3556
3661
|
import fs7 from "node:fs/promises";
|
|
@@ -3558,28 +3663,6 @@ import fs7 from "node:fs/promises";
|
|
|
3558
3663
|
// lib/setup/fs-tree.ts
|
|
3559
3664
|
import fs6, { glob as fsGlob } from "node:fs/promises";
|
|
3560
3665
|
import path3 from "node:path";
|
|
3561
|
-
function isGlob(str) {
|
|
3562
|
-
return /[*?{[]/.test(str);
|
|
3563
|
-
}
|
|
3564
|
-
async function readDirRecursive(dir, filter) {
|
|
3565
|
-
const entries = await fs6.readdir(dir, { recursive: true, withFileTypes: true });
|
|
3566
|
-
const candidates = entries.filter(
|
|
3567
|
-
(dirent) => (dirent.isFile() || dirent.isSymbolicLink()) && filter(dirent.name)
|
|
3568
|
-
);
|
|
3569
|
-
const resolvedPaths = await Promise.all(
|
|
3570
|
-
candidates.map(async (dirent) => {
|
|
3571
|
-
const fullPath = path3.join(dirent.parentPath, dirent.name);
|
|
3572
|
-
if (dirent.isFile()) return fullPath;
|
|
3573
|
-
try {
|
|
3574
|
-
const statResult = await fs6.stat(fullPath);
|
|
3575
|
-
return statResult.isFile() ? fullPath : null;
|
|
3576
|
-
} catch {
|
|
3577
|
-
return null;
|
|
3578
|
-
}
|
|
3579
|
-
})
|
|
3580
|
-
);
|
|
3581
|
-
return resolvedPaths.filter((resolvedPath) => resolvedPath !== null);
|
|
3582
|
-
}
|
|
3583
3666
|
async function buildFSTree(fileAbsolutePaths, config = {}) {
|
|
3584
3667
|
const targetExtensions = config.extensions || ["js", "ts"];
|
|
3585
3668
|
const fsTree = {};
|
|
@@ -3613,57 +3696,65 @@ async function buildFSTree(fileAbsolutePaths, config = {}) {
|
|
|
3613
3696
|
);
|
|
3614
3697
|
return fsTree;
|
|
3615
3698
|
}
|
|
3616
|
-
|
|
3617
|
-
// lib/setup/test-file-paths.ts
|
|
3618
|
-
import { matchesGlob } from "node:path";
|
|
3619
|
-
function isGlob2(str) {
|
|
3699
|
+
function isGlob(str) {
|
|
3620
3700
|
return /[*?{[]/.test(str);
|
|
3621
3701
|
}
|
|
3622
|
-
function
|
|
3623
|
-
const
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
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;
|
|
3630
3716
|
}
|
|
3631
|
-
|
|
3632
|
-
},
|
|
3633
|
-
[[], [], []]
|
|
3717
|
+
})
|
|
3634
3718
|
);
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
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 });
|
|
3644
3736
|
}
|
|
3645
3737
|
});
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
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);
|
|
3649
3745
|
}
|
|
3650
|
-
|
|
3651
|
-
|
|
3746
|
+
return acc;
|
|
3747
|
+
}, []);
|
|
3748
|
+
return dedupedFolders.concat(dedupedGlobFiles, dedupedPlainFiles).map((meta) => meta.input);
|
|
3652
3749
|
}
|
|
3653
3750
|
function pathIsFile(path10) {
|
|
3654
|
-
|
|
3655
|
-
return inputs2[inputs2.length - 1].includes(".");
|
|
3751
|
+
return path10.includes(".", path10.lastIndexOf("/") + 1);
|
|
3656
3752
|
}
|
|
3657
|
-
function
|
|
3658
|
-
return paths.some((path10) =>
|
|
3659
|
-
if (path10 === targetPath) {
|
|
3660
|
-
return false;
|
|
3661
|
-
}
|
|
3662
|
-
return matchesGlob(targetPath.input, buildGlobFormat(path10));
|
|
3663
|
-
});
|
|
3753
|
+
function isIncludedIn(paths, target) {
|
|
3754
|
+
return paths.some((path10) => path10 !== target && matchesGlob(target.input, path10.globFormat));
|
|
3664
3755
|
}
|
|
3665
|
-
function
|
|
3666
|
-
return
|
|
3756
|
+
function isGlob2(str) {
|
|
3757
|
+
return GLOB_CHARS.test(str);
|
|
3667
3758
|
}
|
|
3668
3759
|
|
|
3669
3760
|
// lib/utils/parse-cli-flags.ts
|
|
@@ -3766,7 +3857,7 @@ async function setupConfig() {
|
|
|
3766
3857
|
...cliConfigFlags,
|
|
3767
3858
|
projectRoot,
|
|
3768
3859
|
inputs: inputs2,
|
|
3769
|
-
testFileLookupPaths: setupTestFilePaths(
|
|
3860
|
+
testFileLookupPaths: setupTestFilePaths(inputs2),
|
|
3770
3861
|
lastFailedTestFiles: null,
|
|
3771
3862
|
lastRanTestFiles: null,
|
|
3772
3863
|
COUNTER: {
|