qunitx-cli 0.21.2 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -15
- package/dist/cli.js +787 -678
- package/package.json +6 -3
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"() {
|
|
@@ -377,6 +377,21 @@ var init_color = __esm({
|
|
|
377
377
|
}
|
|
378
378
|
});
|
|
379
379
|
|
|
380
|
+
// lib/setup/default-project-config-values.ts
|
|
381
|
+
var defaultProjectConfigValues;
|
|
382
|
+
var init_default_project_config_values = __esm({
|
|
383
|
+
"lib/setup/default-project-config-values.ts"() {
|
|
384
|
+
defaultProjectConfigValues = {
|
|
385
|
+
output: "tmp",
|
|
386
|
+
timeout: 2e4,
|
|
387
|
+
failFast: false,
|
|
388
|
+
port: 1234,
|
|
389
|
+
extensions: ["js", "ts", "jsx", "tsx"],
|
|
390
|
+
browser: "chromium"
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
380
395
|
// lib/utils/read-template.ts
|
|
381
396
|
import fs3 from "node:fs/promises";
|
|
382
397
|
import { dirname, join as join2 } from "node:path";
|
|
@@ -447,6 +462,18 @@ var init_html = __esm({
|
|
|
447
462
|
});
|
|
448
463
|
|
|
449
464
|
// lib/tap/dump-yaml.ts
|
|
465
|
+
function dumpYaml({
|
|
466
|
+
name,
|
|
467
|
+
actual,
|
|
468
|
+
expected,
|
|
469
|
+
message,
|
|
470
|
+
stack,
|
|
471
|
+
source,
|
|
472
|
+
at
|
|
473
|
+
}) {
|
|
474
|
+
return `name: ${dumpString(name, "")}
|
|
475
|
+
` + 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) : "");
|
|
476
|
+
}
|
|
450
477
|
function needsQuoting(str) {
|
|
451
478
|
return NEEDS_QUOTING.test(str);
|
|
452
479
|
}
|
|
@@ -484,18 +511,6 @@ function yamlLine(key, value) {
|
|
|
484
511
|
` : `${key}: ${serialized}
|
|
485
512
|
`;
|
|
486
513
|
}
|
|
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
514
|
var NEEDS_QUOTING;
|
|
500
515
|
var init_dump_yaml = __esm({
|
|
501
516
|
"lib/tap/dump-yaml.ts"() {
|
|
@@ -518,43 +533,35 @@ var init_indent_string = __esm({
|
|
|
518
533
|
});
|
|
519
534
|
|
|
520
535
|
// 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
536
|
function decodeMappings(mappings) {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
537
|
+
const result = [];
|
|
538
|
+
const cursor = { position: 0 };
|
|
539
|
+
const mappingsLength = mappings.length;
|
|
540
|
+
let segments = [];
|
|
541
|
+
let generatedCol = 0, sourceIndex = 0, sourceLine = 0, sourceCol = 0;
|
|
542
|
+
for (; ; ) {
|
|
543
|
+
if (cursor.position >= mappingsLength) break;
|
|
544
|
+
const charCode = mappings.charCodeAt(cursor.position);
|
|
545
|
+
if (charCode !== COMMA && charCode !== SEMICOLON) {
|
|
546
|
+
generatedCol += readVlqAt(mappings, cursor);
|
|
547
|
+
if (atFieldStart(mappings, cursor.position)) {
|
|
548
|
+
sourceIndex += readVlqAt(mappings, cursor);
|
|
549
|
+
sourceLine += readVlqAt(mappings, cursor);
|
|
550
|
+
sourceCol += readVlqAt(mappings, cursor);
|
|
551
|
+
segments.push({ generatedCol, sourceIndex, sourceLine, sourceCol });
|
|
552
|
+
if (atFieldStart(mappings, cursor.position)) readVlqAt(mappings, cursor);
|
|
542
553
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
segments.push({ generatedCol, sourceIndex, sourceLine, sourceCol });
|
|
555
|
-
}
|
|
556
|
-
return segments;
|
|
557
|
-
});
|
|
554
|
+
} else if (charCode === COMMA) {
|
|
555
|
+
cursor.position++;
|
|
556
|
+
} else {
|
|
557
|
+
result.push(segments);
|
|
558
|
+
segments = [];
|
|
559
|
+
generatedCol = 0;
|
|
560
|
+
cursor.position++;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
result.push(segments);
|
|
564
|
+
return result;
|
|
558
565
|
}
|
|
559
566
|
function parseSourceMap(json, outDir) {
|
|
560
567
|
const map = JSON.parse(json);
|
|
@@ -566,151 +573,185 @@ function parseSourceMap(json, outDir) {
|
|
|
566
573
|
sourcesContent: map.sourcesContent ?? []
|
|
567
574
|
};
|
|
568
575
|
}
|
|
569
|
-
function base64DecodeUtf8(b64) {
|
|
570
|
-
const binary = atob(b64);
|
|
571
|
-
return UTF8.decode(Uint8Array.from(binary, (c) => c.charCodeAt(0)));
|
|
572
|
-
}
|
|
573
576
|
function extractInlineSourceMap(bundle, outDir) {
|
|
574
577
|
if (!bundle) return null;
|
|
575
|
-
const
|
|
576
|
-
|
|
577
|
-
/\/\/# sourceMappingURL=data:application\/json;base64,([A-Za-z0-9+/=]+)/
|
|
578
|
-
);
|
|
579
|
-
if (!match) return null;
|
|
578
|
+
const base64Payload = readMarkerPayload(bundle);
|
|
579
|
+
if (!base64Payload) return null;
|
|
580
580
|
try {
|
|
581
|
-
return parseSourceMap(
|
|
581
|
+
return parseSourceMap(decodeBase64Utf8(base64Payload), outDir);
|
|
582
582
|
} catch {
|
|
583
583
|
return null;
|
|
584
584
|
}
|
|
585
585
|
}
|
|
586
|
-
function normalizePosix(p) {
|
|
587
|
-
const abs = p.startsWith("/");
|
|
588
|
-
const parts = p.split("/");
|
|
589
|
-
const out = [];
|
|
590
|
-
for (const part of parts) {
|
|
591
|
-
if (part === "..") out.pop();
|
|
592
|
-
else if (part !== "" && part !== ".") out.push(part);
|
|
593
|
-
}
|
|
594
|
-
return (abs ? "/" : "") + out.join("/");
|
|
595
|
-
}
|
|
596
|
-
function posixResolve(base, relative) {
|
|
597
|
-
if (relative.startsWith("/")) return normalizePosix(relative);
|
|
598
|
-
return normalizePosix(base + "/" + relative);
|
|
599
|
-
}
|
|
600
|
-
function toAbsolutePath(raw, outDir, sourceRoot) {
|
|
601
|
-
if (raw.startsWith("file://")) return raw.slice(7);
|
|
602
|
-
if (raw.startsWith("/")) return raw;
|
|
603
|
-
const base = sourceRoot ? normalizePosix(outDir + "/" + sourceRoot) : outDir;
|
|
604
|
-
return posixResolve(base, raw);
|
|
605
|
-
}
|
|
606
586
|
function lookupPosition(decoder, generatedLine, generatedCol) {
|
|
607
587
|
const segments = decoder.segmentsByLine[generatedLine - 1];
|
|
608
588
|
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];
|
|
589
|
+
const targetCol = generatedCol - 1;
|
|
590
|
+
const segment = segments.findLast((s) => s.generatedCol <= targetCol);
|
|
591
|
+
if (!segment) return null;
|
|
592
|
+
const rawSource = decoder.sources[segment.sourceIndex];
|
|
623
593
|
if (!rawSource) return null;
|
|
624
|
-
const content = decoder.sourcesContent[sourceIndex];
|
|
625
|
-
const sourceText = content ? content.split("\n", sourceLine + 1)[sourceLine]?.trim() || null : null;
|
|
626
594
|
return {
|
|
627
595
|
absolutePath: toAbsolutePath(rawSource, decoder.outDir, decoder.sourceRoot),
|
|
628
|
-
line: sourceLine + 1,
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
// 0-based → 1-based
|
|
632
|
-
sourceText
|
|
596
|
+
line: segment.sourceLine + 1,
|
|
597
|
+
col: segment.sourceCol + 1,
|
|
598
|
+
sourceText: extractSourceLine(decoder.sourcesContent[segment.sourceIndex], segment.sourceLine)
|
|
633
599
|
};
|
|
634
600
|
}
|
|
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 };
|
|
601
|
+
function parseFrameLocation(text) {
|
|
602
|
+
const match = FRAME_LOCATION_RE.exec(text);
|
|
603
|
+
return match ? { url: match[1], line: +match[2], col: +match[3] } : null;
|
|
645
604
|
}
|
|
646
605
|
function isBundleUrl(url) {
|
|
647
|
-
|
|
648
|
-
|
|
606
|
+
return BUNDLE_URL_RE.test(url);
|
|
607
|
+
}
|
|
608
|
+
function resolveFrame(frame, decoder, projectRoot) {
|
|
609
|
+
const hit = FRAME_FORMATS.map((format) => ({ format, match: frame.match(format.pattern) })).find(
|
|
610
|
+
({ match }) => match !== null
|
|
611
|
+
);
|
|
612
|
+
if (!hit?.match) return null;
|
|
613
|
+
const original = tryResolve(hit.match[hit.format.urlGroup], decoder, projectRoot);
|
|
614
|
+
return original && {
|
|
615
|
+
resolved: hit.format.format(hit.match, original.display),
|
|
616
|
+
userPath: original.userPath,
|
|
617
|
+
sourceText: original.sourceText
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
function resolveStack(stack, decoder, projectRoot) {
|
|
621
|
+
const decoratedFrames = stack.split("\n").map((frame) => {
|
|
622
|
+
const resolution = resolveFrame(frame, decoder, projectRoot);
|
|
623
|
+
return {
|
|
624
|
+
line: resolution?.resolved ?? frame,
|
|
625
|
+
userPath: resolution?.userPath ?? null,
|
|
626
|
+
sourceText: resolution?.sourceText ?? null
|
|
627
|
+
};
|
|
628
|
+
});
|
|
629
|
+
const firstUser = decoratedFrames.find((entry) => entry.userPath !== null);
|
|
630
|
+
return {
|
|
631
|
+
resolvedStack: decoratedFrames.map((entry) => entry.line).join("\n"),
|
|
632
|
+
firstUserFrame: firstUser?.userPath ?? null,
|
|
633
|
+
firstUserSourceText: firstUser?.sourceText ?? null
|
|
634
|
+
};
|
|
649
635
|
}
|
|
650
|
-
function
|
|
651
|
-
|
|
636
|
+
function readVlqAt(text, cursor) {
|
|
637
|
+
let value = 0, bitShift = 0, position = cursor.position;
|
|
638
|
+
for (; ; ) {
|
|
639
|
+
const digit = BASE64_LOOKUP[text.charCodeAt(position++)];
|
|
640
|
+
value |= (digit & VLQ_DATA_MASK) << bitShift;
|
|
641
|
+
if (!(digit & VLQ_CONTINUATION)) break;
|
|
642
|
+
bitShift += 5;
|
|
643
|
+
}
|
|
644
|
+
cursor.position = position;
|
|
645
|
+
return value & 1 ? -(value >>> 1) : value >>> 1;
|
|
646
|
+
}
|
|
647
|
+
function atFieldStart(text, position) {
|
|
648
|
+
if (position >= text.length) return false;
|
|
649
|
+
const charCode = text.charCodeAt(position);
|
|
650
|
+
return charCode !== COMMA && charCode !== SEMICOLON;
|
|
651
|
+
}
|
|
652
|
+
function decodeBase64Utf8(base64) {
|
|
653
|
+
if (Buffer2) return Buffer2.from(base64, "base64").toString("utf8");
|
|
654
|
+
return UTF8_DECODER.decode(Uint8Array.from(atob(base64), (char) => char.charCodeAt(0)));
|
|
655
|
+
}
|
|
656
|
+
function readMarkerPayload(bundle) {
|
|
657
|
+
if (typeof bundle === "string") return sliceMarkerFromString(bundle);
|
|
658
|
+
if (Buffer2) return sliceMarkerFromBuffer(asBuffer(bundle, Buffer2));
|
|
659
|
+
return sliceMarkerFromString(UTF8_DECODER.decode(bundle));
|
|
660
|
+
}
|
|
661
|
+
function asBuffer(view, BufferCtor) {
|
|
662
|
+
return BufferCtor.isBuffer(view) ? view : BufferCtor.from(view.buffer, view.byteOffset, view.byteLength);
|
|
663
|
+
}
|
|
664
|
+
function sliceMarkerFromString(text) {
|
|
665
|
+
const markerStart = text.lastIndexOf(SOURCE_MAP_MARKER);
|
|
666
|
+
if (markerStart < 0) return null;
|
|
667
|
+
const payloadStart = markerStart + SOURCE_MAP_MARKER.length;
|
|
668
|
+
const payloadEnd = text.indexOf("\n", payloadStart);
|
|
669
|
+
return (payloadEnd < 0 ? text.slice(payloadStart) : text.slice(payloadStart, payloadEnd)).trim();
|
|
670
|
+
}
|
|
671
|
+
function sliceMarkerFromBuffer(buffer) {
|
|
672
|
+
const markerBytes = SOURCE_MAP_MARKER_BYTES;
|
|
673
|
+
const markerStart = buffer.lastIndexOf(markerBytes);
|
|
674
|
+
if (markerStart < 0) return null;
|
|
675
|
+
const payloadStart = markerStart + markerBytes.length;
|
|
676
|
+
const newlineIndex = buffer.indexOf(NEWLINE, payloadStart);
|
|
677
|
+
const payloadEnd = newlineIndex < 0 ? buffer.length : newlineIndex;
|
|
678
|
+
return buffer.toString("latin1", payloadStart, payloadEnd).trim();
|
|
679
|
+
}
|
|
680
|
+
function extractSourceLine(content, lineIndex) {
|
|
681
|
+
if (!content || lineIndex < 0) return null;
|
|
682
|
+
const line = content.split("\n", lineIndex + 1)[lineIndex];
|
|
683
|
+
return line?.trim() || null;
|
|
684
|
+
}
|
|
685
|
+
function normalizePosix(path10) {
|
|
686
|
+
const parts = path10.split("/").reduce((acc, part) => {
|
|
687
|
+
if (part === "..") acc.pop();
|
|
688
|
+
else if (part && part !== ".") acc.push(part);
|
|
689
|
+
return acc;
|
|
690
|
+
}, []);
|
|
691
|
+
return (path10.startsWith("/") ? "/" : "") + parts.join("/");
|
|
692
|
+
}
|
|
693
|
+
function toAbsolutePath(rawSource, outDir, sourceRoot) {
|
|
694
|
+
if (rawSource.startsWith("file://")) return rawSource.slice(7);
|
|
695
|
+
if (rawSource.startsWith("/")) return rawSource;
|
|
696
|
+
const base = sourceRoot ? normalizePosix(`${outDir}/${sourceRoot}`) : outDir;
|
|
697
|
+
return normalizePosix(`${base}/${rawSource}`);
|
|
698
|
+
}
|
|
699
|
+
function isNodeModulesPath(path10) {
|
|
700
|
+
return path10.includes("/node_modules/") || path10.includes("\\node_modules\\");
|
|
652
701
|
}
|
|
653
702
|
function makeDisplayPath(absolutePath, projectRoot) {
|
|
654
703
|
const prefix = projectRoot + "/";
|
|
655
704
|
return absolutePath.startsWith(prefix) ? absolutePath.slice(prefix.length) : absolutePath;
|
|
656
705
|
}
|
|
657
706
|
function tryResolve(urlLineCol, decoder, projectRoot) {
|
|
658
|
-
const
|
|
659
|
-
if (!
|
|
660
|
-
const
|
|
661
|
-
if (!
|
|
662
|
-
const display = `${makeDisplayPath(
|
|
707
|
+
const location = parseFrameLocation(urlLineCol);
|
|
708
|
+
if (!location || !isBundleUrl(location.url)) return null;
|
|
709
|
+
const original = lookupPosition(decoder, location.line, location.col);
|
|
710
|
+
if (!original) return null;
|
|
711
|
+
const display = `${makeDisplayPath(original.absolutePath, projectRoot)}:${original.line}:${original.col}`;
|
|
663
712
|
return {
|
|
664
713
|
display,
|
|
665
|
-
userPath: isNodeModulesPath(
|
|
666
|
-
sourceText:
|
|
714
|
+
userPath: isNodeModulesPath(original.absolutePath) ? null : display,
|
|
715
|
+
sourceText: original.sourceText
|
|
667
716
|
};
|
|
668
717
|
}
|
|
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;
|
|
718
|
+
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
719
|
var init_source_map_decoder = __esm({
|
|
707
720
|
"lib/utils/source-map-decoder.ts"() {
|
|
708
|
-
|
|
721
|
+
BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
709
722
|
BASE64_LOOKUP = new Uint8Array(128);
|
|
710
|
-
[...
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
723
|
+
[...BASE64_ALPHABET].forEach((char, index) => BASE64_LOOKUP[char.charCodeAt(0)] = index);
|
|
724
|
+
COMMA = 44;
|
|
725
|
+
SEMICOLON = 59;
|
|
726
|
+
NEWLINE = 10;
|
|
727
|
+
VLQ_CONTINUATION = 32;
|
|
728
|
+
VLQ_DATA_MASK = 31;
|
|
729
|
+
SOURCE_MAP_MARKER = "//# sourceMappingURL=data:application/json;base64,";
|
|
730
|
+
FRAME_LOCATION_RE = /^(.+):(\d+):(\d+)$/;
|
|
731
|
+
BUNDLE_URL_RE = /^(?:async )?https?:\/\/.*\/(?:tests|filtered-tests)\.js$/;
|
|
732
|
+
FRAME_FORMATS = [
|
|
733
|
+
// Chrome named: " at FUNC (URL:LINE:COL)"
|
|
734
|
+
{
|
|
735
|
+
pattern: /^(\s*at\s+)(.*?)\s+\(([^)]+)\)\s*$/,
|
|
736
|
+
urlGroup: 3,
|
|
737
|
+
format: (match, displayPath) => `${match[1]}${match[2]} (${displayPath})`
|
|
738
|
+
},
|
|
739
|
+
// Chrome anonymous (incl. async): " at [async] URL:LINE:COL"
|
|
740
|
+
{
|
|
741
|
+
pattern: /^(\s*at\s+(?:async\s+)?)(.+)/,
|
|
742
|
+
urlGroup: 2,
|
|
743
|
+
format: (match, displayPath) => `${match[1]}${displayPath}`
|
|
744
|
+
},
|
|
745
|
+
// Firefox / WebKit: "FUNC@URL:LINE:COL"
|
|
746
|
+
{
|
|
747
|
+
pattern: /^([^@]*)@(.+)$/,
|
|
748
|
+
urlGroup: 2,
|
|
749
|
+
format: (match, displayPath) => `${match[1]}@${displayPath}`
|
|
750
|
+
}
|
|
751
|
+
];
|
|
752
|
+
Buffer2 = globalThis.Buffer;
|
|
753
|
+
SOURCE_MAP_MARKER_BYTES = Buffer2 ? Buffer2.from(SOURCE_MAP_MARKER, "utf8") : null;
|
|
754
|
+
UTF8_DECODER = new TextDecoder();
|
|
714
755
|
}
|
|
715
756
|
});
|
|
716
757
|
|
|
@@ -836,12 +877,12 @@ var init_bind_server_to_port = __esm({
|
|
|
836
877
|
}
|
|
837
878
|
});
|
|
838
879
|
|
|
839
|
-
// lib/servers/
|
|
880
|
+
// lib/servers/web.ts
|
|
840
881
|
import http from "node:http";
|
|
841
882
|
import WebSocket, { WebSocketServer } from "ws";
|
|
842
883
|
var MIME_TYPES, HTTPServer;
|
|
843
|
-
var
|
|
844
|
-
"lib/servers/
|
|
884
|
+
var init_web = __esm({
|
|
885
|
+
"lib/servers/web.ts"() {
|
|
845
886
|
init_bind_server_to_port();
|
|
846
887
|
MIME_TYPES = {
|
|
847
888
|
html: "text/html; charset=UTF-8",
|
|
@@ -1298,166 +1339,18 @@ function setupWebServer(config, cachedContent) {
|
|
|
1298
1339
|
});
|
|
1299
1340
|
return server;
|
|
1300
1341
|
}
|
|
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;
|
|
1342
|
+
function buildNoTestsHTML(files) {
|
|
1343
|
+
const escaped = files.map((f) => f.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")).join("\n");
|
|
1344
|
+
return `<!DOCTYPE html>
|
|
1345
|
+
<html lang="en">
|
|
1346
|
+
<head>
|
|
1347
|
+
<meta charset="utf-8">
|
|
1348
|
+
<meta name="viewport" content="width=device-width">
|
|
1349
|
+
<title>No Tests Registered \u2014 qunitx</title>
|
|
1350
|
+
<style>
|
|
1351
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
1352
|
+
#qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-tests, #qunit-tests li {
|
|
1353
|
+
font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
|
|
1461
1354
|
}
|
|
1462
1355
|
#qunit-header {
|
|
1463
1356
|
padding: 0.5em 0 0.5em 1em;
|
|
@@ -1700,16 +1593,6 @@ function registerGroupRoutes(server, groupConfig, groupCachedContent, groupId) {
|
|
|
1700
1593
|
res.end(groupCachedContent.allTestCode);
|
|
1701
1594
|
});
|
|
1702
1595
|
}
|
|
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
1596
|
function setupGroupWSHandler(server, groupConfigs) {
|
|
1714
1597
|
const socketToGroupId = /* @__PURE__ */ new WeakMap();
|
|
1715
1598
|
server.wss.on("connection", function connection(socket) {
|
|
@@ -1790,6 +1673,164 @@ function registerSharedStaticHandler(server, groupConfigs) {
|
|
|
1790
1673
|
});
|
|
1791
1674
|
});
|
|
1792
1675
|
}
|
|
1676
|
+
function replaceAssetPaths(html, htmlPath, projectRoot) {
|
|
1677
|
+
const assetPaths = findInternalAssetsFromHTML(html);
|
|
1678
|
+
const htmlDirectory = htmlPath.split("/").slice(0, -1).join("/");
|
|
1679
|
+
return assetPaths.reduce((result, assetPath) => {
|
|
1680
|
+
const normalizedFullAbsolutePath = path5.normalize(`${htmlDirectory}/${assetPath}`);
|
|
1681
|
+
return result.replace(assetPath, normalizedFullAbsolutePath.replace(projectRoot, "."));
|
|
1682
|
+
}, html);
|
|
1683
|
+
}
|
|
1684
|
+
function testRuntimeToInject(config, groupId) {
|
|
1685
|
+
const groupIdPart = groupId !== void 0 ? `, groupId: ${groupId}` : "";
|
|
1686
|
+
return `<script>
|
|
1687
|
+
(function() {
|
|
1688
|
+
// setupQUnit runs exactly once, after both the WebSocket is open and tests.js has loaded.
|
|
1689
|
+
// Promise.all is naturally idempotent \u2014 resolving a Promise a second time is a no-op,
|
|
1690
|
+
// so WebKit firing WS error after open (causing a retry that re-opens) cannot double-start.
|
|
1691
|
+
let resolveWsReady = () => {};
|
|
1692
|
+
const wsReadyPromise = window.location.protocol === 'file:'
|
|
1693
|
+
? Promise.resolve()
|
|
1694
|
+
: new Promise(resolve => { resolveWsReady = resolve; });
|
|
1695
|
+
|
|
1696
|
+
// { once: true } auto-removes the listener after the first fire.
|
|
1697
|
+
const testsReadyPromise = new Promise(resolve => {
|
|
1698
|
+
window.addEventListener('qunitx:tests-ready', resolve, { once: true });
|
|
1699
|
+
});
|
|
1700
|
+
|
|
1701
|
+
Promise.all([wsReadyPromise, testsReadyPromise]).then(setupQUnit);
|
|
1702
|
+
|
|
1703
|
+
// For static files (file:// protocol) there is no WebSocket server; wsReadyPromise
|
|
1704
|
+
// is already resolved above, so setupQUnit fires as soon as tests load.
|
|
1705
|
+
if (window.location.protocol === 'file:') return;
|
|
1706
|
+
|
|
1707
|
+
let wsRetryCount = 0;
|
|
1708
|
+
const WS_MAX_RETRIES = Math.ceil(${config.timeout} / ${WS_RETRY_INTERVAL_MS}); // retry for the full test timeout window
|
|
1709
|
+
|
|
1710
|
+
function setupWebSocket() {
|
|
1711
|
+
try {
|
|
1712
|
+
window.socket = new WebSocket(\`ws://localhost:\${location.port}\`);
|
|
1713
|
+
} catch (error) {
|
|
1714
|
+
console.log(error);
|
|
1715
|
+
retryOrFail();
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
window.socket.addEventListener('open', function() {
|
|
1720
|
+
resolveWsReady();
|
|
1721
|
+
// Notify Node.js that the WS socket is open. This fires immediately (< 1 s) because
|
|
1722
|
+
// this runtime script is tiny \u2014 tests.js background compilation hasn't finished yet.
|
|
1723
|
+
// Node.js uses this to distinguish "WS never connected" from "WS connected but bundle slow".
|
|
1724
|
+
if (navigator.webdriver) {
|
|
1725
|
+
window.socket.send(JSON.stringify({ event: 'wsOpen'${groupIdPart} }));
|
|
1726
|
+
}
|
|
1727
|
+
});
|
|
1728
|
+
window.socket.addEventListener('error', function() {
|
|
1729
|
+
retryOrFail();
|
|
1730
|
+
});
|
|
1731
|
+
window.socket.addEventListener('message', function(messageEvent) {
|
|
1732
|
+
if (!navigator.webdriver && messageEvent.data === 'refresh') {
|
|
1733
|
+
window.location.reload(true);
|
|
1734
|
+
} else if (navigator.webdriver && messageEvent.data === 'abort') {
|
|
1735
|
+
window.abortQUnit = true;
|
|
1736
|
+
window.QUnit.config.queue.length = 0;
|
|
1737
|
+
window.socket.send(JSON.stringify({ event: 'abort' }));
|
|
1738
|
+
}
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
function retryOrFail() {
|
|
1743
|
+
wsRetryCount++;
|
|
1744
|
+
if (wsRetryCount > WS_MAX_RETRIES) {
|
|
1745
|
+
console.log('WebSocket connection failed after ' + WS_MAX_RETRIES + ' retries');
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
window.setTimeout(setupWebSocket, ${WS_RETRY_INTERVAL_MS});
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
setupWebSocket();
|
|
1752
|
+
})();
|
|
1753
|
+
|
|
1754
|
+
function getCircularReplacer() {
|
|
1755
|
+
const ancestors = [];
|
|
1756
|
+
return function (key, value) {
|
|
1757
|
+
if (typeof value !== "object" || value === null) {
|
|
1758
|
+
return value;
|
|
1759
|
+
}
|
|
1760
|
+
while (ancestors.length > 0 && ancestors.at(-1) !== this) {
|
|
1761
|
+
ancestors.pop();
|
|
1762
|
+
}
|
|
1763
|
+
if (ancestors.includes(value)) {
|
|
1764
|
+
return "[Circular]";
|
|
1765
|
+
}
|
|
1766
|
+
ancestors.push(value);
|
|
1767
|
+
return value;
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
function setupQUnit() {
|
|
1772
|
+
window.QUNIT_RESULT = { totalTests: 0, finishedTests: 0, failedTests: 0, currentTest: '' };
|
|
1773
|
+
|
|
1774
|
+
if (!window.QUnit) {
|
|
1775
|
+
console.log('QUnit not found after WebSocket connected');
|
|
1776
|
+
if (navigator.webdriver) {
|
|
1777
|
+
// Signal the Playwright runner that the run is complete with 0 tests rather than
|
|
1778
|
+
// waiting for the inactivity timeout. The runner treats totalTests === 0 as a
|
|
1779
|
+
// "no tests registered" warning (not a failure), so this gives a fast, clean result.
|
|
1780
|
+
window.QUNIT_RESULT = { totalTests: 0, finishedTests: 0, failedTests: 0, currentTest: null };
|
|
1781
|
+
window.socket.send(JSON.stringify({ event: 'done', details: { passed: 0, failed: 0, runtime: 0 } }));
|
|
1782
|
+
}
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
window.QUnit.begin(() => { // NOTE: might be useful in future for hanged module tracking
|
|
1787
|
+
if (navigator.webdriver) {
|
|
1788
|
+
window.socket.send(JSON.stringify({ event: 'connection' }));
|
|
1789
|
+
}
|
|
1790
|
+
});
|
|
1791
|
+
window.QUnit.on('testStart', (details) => {
|
|
1792
|
+
window.QUNIT_RESULT.totalTests++;
|
|
1793
|
+
window.QUNIT_RESULT.currentTest = details.fullName.join(' | ');
|
|
1794
|
+
});
|
|
1795
|
+
window.QUnit.on('testEnd', (details) => { // NOTE: https://github.com/qunitjs/qunit/blob/master/src/html-reporter/diff.js
|
|
1796
|
+
window.QUNIT_RESULT.finishedTests++;
|
|
1797
|
+
if (details.status === 'failed') window.QUNIT_RESULT.failedTests++;
|
|
1798
|
+
window.QUNIT_RESULT.currentTest = null;
|
|
1799
|
+
if (navigator.webdriver) {
|
|
1800
|
+
const isFailed = details.status === 'failed';
|
|
1801
|
+
const payload = isFailed ? details : { status: details.status, fullName: details.fullName, runtime: details.runtime };
|
|
1802
|
+
window.socket.send(JSON.stringify({ event: 'testEnd', details: payload, abort: window.abortQUnit }, isFailed ? getCircularReplacer() : undefined));
|
|
1803
|
+
|
|
1804
|
+
if (${config.failFast} && details.status === 'failed') {
|
|
1805
|
+
window.QUnit.config.queue.length = 0;
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
});
|
|
1809
|
+
window.QUnit.done((details) => {
|
|
1810
|
+
if (navigator.webdriver) {
|
|
1811
|
+
window.socket.send(JSON.stringify({ event: 'done', details: details, qunitResult: window.QUNIT_RESULT, abort: window.abortQUnit }, getCircularReplacer()));
|
|
1812
|
+
}
|
|
1813
|
+
});
|
|
1814
|
+
|
|
1815
|
+
window.QUnit.config.testTimeout = ${config.timeout};
|
|
1816
|
+
window.QUnit.start();
|
|
1817
|
+
}
|
|
1818
|
+
</script>`;
|
|
1819
|
+
}
|
|
1820
|
+
function escapeAndInjectTestsToHTML(html, testRuntimeCode, testBundleUrl) {
|
|
1821
|
+
return injectScript(html, `${testRuntimeCode}
|
|
1822
|
+
<script src="${testBundleUrl}" async></script>`);
|
|
1823
|
+
}
|
|
1824
|
+
function debugGroupHeader(config) {
|
|
1825
|
+
const files = Object.keys(config.fsTree);
|
|
1826
|
+
const rel = files.map((f) => f.replace(`${config.projectRoot}/`, ""));
|
|
1827
|
+
const shown = rel.slice(0, 2);
|
|
1828
|
+
const rest = rel.length - shown.length;
|
|
1829
|
+
process.stdout.write(
|
|
1830
|
+
`# ${blue(`\u2500\u2500 ${shown.join(" ")}${rest > 0 ? ` +${rest} more` : ""} \u2500\u2500`)}
|
|
1831
|
+
`
|
|
1832
|
+
);
|
|
1833
|
+
}
|
|
1793
1834
|
var fsPromise, HTML_HEADERS, WATCH_WS_RECONNECT_INTERVAL_MS, WATCH_WS_RECONNECT_MAX_RETRIES, WS_RETRY_INTERVAL_MS, NOT_FOUND_HTML;
|
|
1794
1835
|
var init_web_server = __esm({
|
|
1795
1836
|
"lib/setup/web-server.ts"() {
|
|
@@ -1797,7 +1838,7 @@ var init_web_server = __esm({
|
|
|
1797
1838
|
init_html();
|
|
1798
1839
|
init_display_test_result();
|
|
1799
1840
|
init_color();
|
|
1800
|
-
|
|
1841
|
+
init_web();
|
|
1801
1842
|
fsPromise = fs8.promises;
|
|
1802
1843
|
HTML_HEADERS = { "Content-Type": "text/html", "Cache-Control": "no-store" };
|
|
1803
1844
|
WATCH_WS_RECONNECT_INTERVAL_MS = 1e3;
|
|
@@ -1961,10 +2002,10 @@ var init_browser = __esm({
|
|
|
1961
2002
|
// lib/utils/open-output-in-browser.ts
|
|
1962
2003
|
import { spawn as spawn2 } from "node:child_process";
|
|
1963
2004
|
import path6 from "node:path";
|
|
1964
|
-
import { pathToFileURL } from "node:url";
|
|
2005
|
+
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
1965
2006
|
async function openOutputInBrowser(config) {
|
|
1966
2007
|
try {
|
|
1967
|
-
const outputFile = config.watch ? `http://localhost:${config.port}` :
|
|
2008
|
+
const outputFile = config.watch ? `http://localhost:${config.port}` : pathToFileURL2(path6.join(path6.resolve(config.projectRoot, config.output), "index.html")).href;
|
|
1968
2009
|
if (typeof config.open === "string") {
|
|
1969
2010
|
spawnDetached(config.open, [outputFile]);
|
|
1970
2011
|
return;
|
|
@@ -2010,10 +2051,10 @@ var init_time_counter = __esm({
|
|
|
2010
2051
|
});
|
|
2011
2052
|
|
|
2012
2053
|
// lib/utils/run-user-module.ts
|
|
2013
|
-
import { pathToFileURL as
|
|
2054
|
+
import { pathToFileURL as pathToFileURL3 } from "node:url";
|
|
2014
2055
|
async function runUserModule(modulePath, params, scriptPosition) {
|
|
2015
2056
|
try {
|
|
2016
|
-
const func = await import(
|
|
2057
|
+
const func = await import(pathToFileURL3(modulePath).href);
|
|
2017
2058
|
if (func) {
|
|
2018
2059
|
func.default ? await func.default(params) : typeof func === "function" ? await func(params) : null;
|
|
2019
2060
|
}
|
|
@@ -2058,12 +2099,6 @@ var init_display_final_result = __esm({
|
|
|
2058
2099
|
import fs9 from "node:fs/promises";
|
|
2059
2100
|
import path7 from "node:path";
|
|
2060
2101
|
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
2102
|
function deriveBuildErrorType(error) {
|
|
2068
2103
|
const msgs = error?.errors ?? [];
|
|
2069
2104
|
const text = msgs[0]?.text ?? (error instanceof Error ? error.message : String(error));
|
|
@@ -2120,6 +2155,11 @@ async function buildTestBundle(config, cachedContent) {
|
|
|
2120
2155
|
legalComments: "none",
|
|
2121
2156
|
target: esbuildTarget(config.browser),
|
|
2122
2157
|
sourcemap,
|
|
2158
|
+
// jsx: 'automatic' is a no-op for .ts/.js files (extension-gated by esbuild) and emits
|
|
2159
|
+
// `import { jsx } from 'react/jsx-runtime'` for .tsx/.jsx files. Per-file overrides via
|
|
2160
|
+
// tsconfig's `jsxImportSource` or a `@jsxImportSource <pkg>` pragma cover Vue/Preact/Solid.
|
|
2161
|
+
jsx: "automatic",
|
|
2162
|
+
plugins: config.plugins,
|
|
2123
2163
|
// Signal the runtime that all test modules are registered. The runtime's maybeStart()
|
|
2124
2164
|
// waits for both this event and the WebSocket 'open' event before calling QUnit.start().
|
|
2125
2165
|
// Dispatching from the bundle (rather than from a script onload attr) is reliable across
|
|
@@ -2266,174 +2306,10 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
2266
2306
|
}
|
|
2267
2307
|
return connections;
|
|
2268
2308
|
}
|
|
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
2309
|
async function flushConsoleHandlers(handlers, deadline = Date.now() + CONSOLE_FLUSH_TIMEOUT_MS) {
|
|
2436
|
-
if (!handlers ||
|
|
2310
|
+
if (!handlers || Date.now() >= deadline) return;
|
|
2311
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
2312
|
+
if (handlers.size === 0) return;
|
|
2437
2313
|
await Promise.allSettled([...handlers]);
|
|
2438
2314
|
return flushConsoleHandlers(handlers, deadline);
|
|
2439
2315
|
}
|
|
@@ -2489,7 +2365,10 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
|
2489
2365
|
in: `group-entry-${slotIndex}`,
|
|
2490
2366
|
out: `group-${slotIndex}`
|
|
2491
2367
|
})),
|
|
2492
|
-
|
|
2368
|
+
// groupEntryPlugin must run first — it owns the virtual entry-point modules every other
|
|
2369
|
+
// plugin sees. User plugins follow and apply to the resolved test files just like in
|
|
2370
|
+
// single-group mode (`buildTestBundle`).
|
|
2371
|
+
plugins: [groupEntryPlugin, ...groupConfigs[0].plugins ?? []],
|
|
2493
2372
|
nodePaths: ANCESTOR_NODE_MODULES,
|
|
2494
2373
|
bundle: true,
|
|
2495
2374
|
logLevel: "silent",
|
|
@@ -2501,6 +2380,7 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
|
2501
2380
|
target: esbuildTarget(browser),
|
|
2502
2381
|
sourcemap,
|
|
2503
2382
|
write: false,
|
|
2383
|
+
jsx: "automatic",
|
|
2504
2384
|
footer: { js: 'window.dispatchEvent(new CustomEvent("qunitx:tests-ready"));' }
|
|
2505
2385
|
};
|
|
2506
2386
|
const hasSmallOutput = (result) => (result.outputFiles ?? []).some(
|
|
@@ -2536,24 +2416,198 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
|
2536
2416
|
})
|
|
2537
2417
|
);
|
|
2538
2418
|
} 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
|
-
})
|
|
2419
|
+
const buildError = { type: deriveBuildErrorType(error), formatted: formatBuildErrors(error) };
|
|
2420
|
+
const errorHtml = buildErrorHTML(buildError);
|
|
2421
|
+
await Promise.all(
|
|
2422
|
+
activeGroups.map((group) => {
|
|
2423
|
+
group.cachedContent._buildError = buildError;
|
|
2424
|
+
return fs9.writeFile(
|
|
2425
|
+
path7.join(path7.resolve(group.config.projectRoot, group.config.output), "index.html"),
|
|
2426
|
+
errorHtml
|
|
2427
|
+
).catch(
|
|
2428
|
+
(err) => debug && process.stderr.write(`# [qunitx] writeFile index.html: ${err.message}
|
|
2429
|
+
`)
|
|
2430
|
+
);
|
|
2431
|
+
})
|
|
2432
|
+
);
|
|
2433
|
+
throw error;
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
function buildFilteredTests(filteredTests, outputPath, config) {
|
|
2437
|
+
const sourcemap = "inline";
|
|
2438
|
+
const needsDisk = Boolean(config.open);
|
|
2439
|
+
return buildWithOverlayfsRetry(
|
|
2440
|
+
{
|
|
2441
|
+
stdin: {
|
|
2442
|
+
contents: filteredTests.map((filePath) => `import "${filePath.replace(/\\/g, "/")}";`).join(""),
|
|
2443
|
+
resolveDir: process.cwd()
|
|
2444
|
+
},
|
|
2445
|
+
nodePaths: ANCESTOR_NODE_MODULES,
|
|
2446
|
+
bundle: true,
|
|
2447
|
+
logLevel: "silent",
|
|
2448
|
+
outfile: outputPath,
|
|
2449
|
+
legalComments: "none",
|
|
2450
|
+
target: esbuildTarget(config.browser),
|
|
2451
|
+
sourcemap,
|
|
2452
|
+
jsx: "automatic",
|
|
2453
|
+
plugins: config.plugins,
|
|
2454
|
+
footer: { js: 'window.dispatchEvent(new CustomEvent("qunitx:tests-ready"));' }
|
|
2455
|
+
},
|
|
2456
|
+
needsDisk
|
|
2457
|
+
);
|
|
2458
|
+
}
|
|
2459
|
+
async function runWithOverlayfsRetry(getContents, needsDisk) {
|
|
2460
|
+
let { result, js } = await getContents();
|
|
2461
|
+
const initialSize = js.length;
|
|
2462
|
+
for (let retry = 1; retry <= MAX_RETRIES; retry++) {
|
|
2463
|
+
if (js.length >= EMPTY_BUNDLE_THRESHOLD) break;
|
|
2464
|
+
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
|
|
2465
|
+
({ result, js } = await getContents());
|
|
2466
|
+
}
|
|
2467
|
+
if (js.length < EMPTY_BUNDLE_THRESHOLD && js.length !== initialSize) {
|
|
2468
|
+
console.log(
|
|
2469
|
+
`# [buildWithOverlayfsRetry] bundle is ${js.length} bytes after ${MAX_RETRIES} retries \u2014 proceeding`
|
|
2470
|
+
);
|
|
2471
|
+
}
|
|
2472
|
+
if (needsDisk) {
|
|
2473
|
+
await Promise.all(
|
|
2474
|
+
result.outputFiles.map((outputFile) => fs9.writeFile(outputFile.path, outputFile.contents))
|
|
2475
|
+
);
|
|
2476
|
+
}
|
|
2477
|
+
return js;
|
|
2478
|
+
}
|
|
2479
|
+
function buildWithOverlayfsRetry(options, needsDisk) {
|
|
2480
|
+
const buildOpts = { ...options, write: false };
|
|
2481
|
+
return runWithOverlayfsRetry(async () => {
|
|
2482
|
+
const result = await esbuild.build(buildOpts);
|
|
2483
|
+
const jsFile = result.outputFiles.find((f) => !f.path.endsWith(".map"));
|
|
2484
|
+
return { result, js: Buffer.from(jsFile.contents) };
|
|
2485
|
+
}, needsDisk);
|
|
2486
|
+
}
|
|
2487
|
+
async function buildIncrementally(options, fileKey, cachedContent, needsDisk) {
|
|
2488
|
+
const buildOpts = { ...options, write: false };
|
|
2489
|
+
if (!cachedContent._esbuildContext || cachedContent._esbuildContextKey !== fileKey) {
|
|
2490
|
+
cachedContent._esbuildContext?.dispose().catch(() => {
|
|
2491
|
+
});
|
|
2492
|
+
cachedContent._esbuildContext = await esbuild.context(buildOpts);
|
|
2493
|
+
cachedContent._esbuildContextKey = fileKey;
|
|
2494
|
+
}
|
|
2495
|
+
const ctx = cachedContent._esbuildContext;
|
|
2496
|
+
return runWithOverlayfsRetry(async () => {
|
|
2497
|
+
const result = await ctx.rebuild();
|
|
2498
|
+
const jsFile = result.outputFiles.find((f) => !f.path.endsWith(".map"));
|
|
2499
|
+
return { result, js: Buffer.from(jsFile.contents) };
|
|
2500
|
+
}, needsDisk);
|
|
2501
|
+
}
|
|
2502
|
+
function esbuildTarget(browser) {
|
|
2503
|
+
if (browser === "firefox") return ["firefox115"];
|
|
2504
|
+
if (browser === "webkit") return ["safari16"];
|
|
2505
|
+
return ["chrome120"];
|
|
2506
|
+
}
|
|
2507
|
+
async function runTestInsideHTMLFile(filePath, { page, server, browser }, config) {
|
|
2508
|
+
let QUNIT_RESULT;
|
|
2509
|
+
let targetError;
|
|
2510
|
+
let timeoutHandle;
|
|
2511
|
+
let wsConnected = false;
|
|
2512
|
+
try {
|
|
2513
|
+
console.log("#", blue(`QUnitX running: http://localhost:${config.port}${filePath}`));
|
|
2514
|
+
const navMs = Math.max(config.timeout + NAV_GRACE_MS, MIN_NAV_MS);
|
|
2515
|
+
const startupMs = Math.max(config.timeout * STARTUP_TIMEOUT_FACTOR, navMs);
|
|
2516
|
+
const testsJsMs = Math.max(config.timeout * TESTS_JS_TIMEOUT_FACTOR, navMs);
|
|
2517
|
+
let resolveTestRace;
|
|
2518
|
+
const testRaceResult = new Promise((resolve) => {
|
|
2519
|
+
resolveTestRace = resolve;
|
|
2520
|
+
});
|
|
2521
|
+
config._testRunDone = resolveTestRace;
|
|
2522
|
+
config._onWsOpen = () => {
|
|
2523
|
+
wsConnected = true;
|
|
2524
|
+
clearTimeout(timeoutHandle);
|
|
2525
|
+
timeoutHandle = setTimeout(resolveTestRace, startupMs);
|
|
2526
|
+
};
|
|
2527
|
+
config._onTestsJsServed = () => {
|
|
2528
|
+
clearTimeout(timeoutHandle);
|
|
2529
|
+
timeoutHandle = setTimeout(resolveTestRace, testsJsMs);
|
|
2530
|
+
};
|
|
2531
|
+
config._resetTestTimeout = () => {
|
|
2532
|
+
wsConnected = true;
|
|
2533
|
+
clearTimeout(timeoutHandle);
|
|
2534
|
+
timeoutHandle = setTimeout(resolveTestRace, config.timeout + TEST_STALL_BUFFER_MS);
|
|
2535
|
+
};
|
|
2536
|
+
const targetUrl = `http://localhost:${config.port}${filePath}`;
|
|
2537
|
+
const navOptions = { timeout: navMs, waitUntil: "commit" };
|
|
2538
|
+
if (page.url().split("?")[0] === targetUrl) {
|
|
2539
|
+
await page.reload(navOptions);
|
|
2540
|
+
} else {
|
|
2541
|
+
await page.goto(targetUrl, navOptions);
|
|
2542
|
+
}
|
|
2543
|
+
clearTimeout(timeoutHandle);
|
|
2544
|
+
timeoutHandle = setTimeout(resolveTestRace, startupMs);
|
|
2545
|
+
await testRaceResult;
|
|
2546
|
+
QUNIT_RESULT = config._lastQUnitResult ?? await page.evaluate(() => window.QUNIT_RESULT);
|
|
2547
|
+
} catch (error) {
|
|
2548
|
+
targetError = error;
|
|
2549
|
+
console.log(error);
|
|
2550
|
+
console.error(error);
|
|
2551
|
+
} finally {
|
|
2552
|
+
clearTimeout(timeoutHandle);
|
|
2553
|
+
config._onWsOpen = null;
|
|
2554
|
+
config._onTestsJsServed = null;
|
|
2555
|
+
config._resetTestTimeout = null;
|
|
2556
|
+
config._testRunDone = null;
|
|
2557
|
+
config._lastQUnitResult = null;
|
|
2558
|
+
}
|
|
2559
|
+
if (!QUNIT_RESULT) {
|
|
2560
|
+
if (targetError) console.log(targetError);
|
|
2561
|
+
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";
|
|
2562
|
+
console.log(`# TIMEOUT: ${wsReason}`);
|
|
2563
|
+
console.log("BROWSER: runtime error thrown during executing tests");
|
|
2564
|
+
console.error("BROWSER: runtime error thrown during executing tests");
|
|
2565
|
+
await failOnNonWatchMode(
|
|
2566
|
+
config.watch,
|
|
2567
|
+
{ server, browser },
|
|
2568
|
+
config._groupMode,
|
|
2569
|
+
config._pendingConsoleHandlers
|
|
2552
2570
|
);
|
|
2553
|
-
|
|
2571
|
+
} else if (QUNIT_RESULT.totalTests === 0) {
|
|
2572
|
+
return;
|
|
2573
|
+
} else if (QUNIT_RESULT.totalTests > QUNIT_RESULT.finishedTests) {
|
|
2574
|
+
if (targetError) console.log(targetError);
|
|
2575
|
+
console.log(
|
|
2576
|
+
`# TIMEOUT: test stalled after ${QUNIT_RESULT.finishedTests}/${QUNIT_RESULT.totalTests} finished \u2014 last active: ${QUNIT_RESULT.currentTest}`
|
|
2577
|
+
);
|
|
2578
|
+
console.log(`BROWSER: TEST TIMED OUT: ${QUNIT_RESULT.currentTest}`);
|
|
2579
|
+
console.error(`BROWSER: TEST TIMED OUT: ${QUNIT_RESULT.currentTest}`);
|
|
2580
|
+
await failOnNonWatchMode(
|
|
2581
|
+
config.watch,
|
|
2582
|
+
{ server, browser },
|
|
2583
|
+
config._groupMode,
|
|
2584
|
+
config._pendingConsoleHandlers
|
|
2585
|
+
);
|
|
2586
|
+
} else if (QUNIT_RESULT.failedTests > config.COUNTER.failCount) {
|
|
2587
|
+
config.COUNTER.failCount = QUNIT_RESULT.failedTests;
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
async function failOnNonWatchMode(watchMode = false, connections = {}, groupMode = false, pendingHandlers) {
|
|
2591
|
+
if (!watchMode) {
|
|
2592
|
+
if (groupMode) {
|
|
2593
|
+
throw new Error("Browser test run failed");
|
|
2594
|
+
}
|
|
2595
|
+
await flushConsoleHandlers(pendingHandlers);
|
|
2596
|
+
await Promise.all([
|
|
2597
|
+
connections.server && connections.server.close(),
|
|
2598
|
+
connections.browser && connections.browser.close()
|
|
2599
|
+
]);
|
|
2600
|
+
await shutdownPrelaunch();
|
|
2601
|
+
process.exit(1);
|
|
2554
2602
|
}
|
|
2555
2603
|
}
|
|
2556
|
-
|
|
2604
|
+
function toEsbuildImportPath(filePath) {
|
|
2605
|
+
const rel = path7.relative(process.cwd(), filePath);
|
|
2606
|
+
const normalized = rel.replace(/\\/g, "/");
|
|
2607
|
+
if (path7.isAbsolute(rel)) return filePath.replace(/\\/g, "/");
|
|
2608
|
+
return normalized.startsWith(".") ? normalized : "./" + normalized;
|
|
2609
|
+
}
|
|
2610
|
+
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
2611
|
var init_tests_in_browser = __esm({
|
|
2558
2612
|
"lib/commands/run/tests-in-browser.ts"() {
|
|
2559
2613
|
init_color();
|
|
@@ -2563,13 +2617,6 @@ var init_tests_in_browser = __esm({
|
|
|
2563
2617
|
init_display_final_result();
|
|
2564
2618
|
init_web_server();
|
|
2565
2619
|
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
2620
|
ancestorNodeModules = (dir) => dir.split(path7.sep).map(
|
|
2574
2621
|
(_, i, parts) => path7.join(parts.slice(0, parts.length - i).join(path7.sep) || path7.sep, "node_modules")
|
|
2575
2622
|
);
|
|
@@ -2578,11 +2625,19 @@ var init_tests_in_browser = __esm({
|
|
|
2578
2625
|
MAX_RETRIES = 3;
|
|
2579
2626
|
EMPTY_BUNDLE_THRESHOLD = 500;
|
|
2580
2627
|
NAV_GRACE_MS = 1e4;
|
|
2628
|
+
MIN_NAV_MS = 3e4;
|
|
2581
2629
|
STARTUP_TIMEOUT_FACTOR = 3;
|
|
2582
2630
|
TESTS_JS_TIMEOUT_FACTOR = 4;
|
|
2583
2631
|
CONSOLE_FLUSH_TIMEOUT_MS = 2e3;
|
|
2584
2632
|
TEST_STALL_BUFFER_MS = 5e3;
|
|
2585
2633
|
GROUP_OUTPUT_REGEX = /group-(\d+)\.js(\.map)?$/;
|
|
2634
|
+
BundleError = class extends Error {
|
|
2635
|
+
constructor(message) {
|
|
2636
|
+
super(message);
|
|
2637
|
+
this.name = "BundleError";
|
|
2638
|
+
this.message = `esbuild Bundle Error: ${message}`.split("\n").join("\n# ");
|
|
2639
|
+
}
|
|
2640
|
+
};
|
|
2586
2641
|
}
|
|
2587
2642
|
});
|
|
2588
2643
|
|
|
@@ -2591,9 +2646,10 @@ import fs10 from "node:fs";
|
|
|
2591
2646
|
import { readdir, stat, lstat } from "node:fs/promises";
|
|
2592
2647
|
import path8 from "node:path";
|
|
2593
2648
|
function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFunc) {
|
|
2594
|
-
const extensions = config.extensions ||
|
|
2649
|
+
const extensions = config.extensions || defaultProjectConfigValues.extensions;
|
|
2595
2650
|
const readyPromises = [];
|
|
2596
2651
|
const parentWatchers = [];
|
|
2652
|
+
const rescanTimers = [];
|
|
2597
2653
|
const fileWatchers = {};
|
|
2598
2654
|
const symlinkPollers = /* @__PURE__ */ new Map();
|
|
2599
2655
|
function trackSymlink(filePath) {
|
|
@@ -2620,20 +2676,24 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
2620
2676
|
}
|
|
2621
2677
|
for (const watchPath of testFileLookupPaths) {
|
|
2622
2678
|
let ready = false;
|
|
2679
|
+
let rescanInProgress = false;
|
|
2623
2680
|
const lastEventMs = {};
|
|
2624
2681
|
const seenMtimeMs = {};
|
|
2625
2682
|
const childWatcher = fs10.watch(watchPath, { recursive: true }, async (eventType, filename) => {
|
|
2626
2683
|
if (!ready) return;
|
|
2627
2684
|
if (!filename) {
|
|
2628
|
-
if (process.platform === "darwin") {
|
|
2629
|
-
|
|
2685
|
+
if (process.platform === "darwin" && !rescanInProgress) {
|
|
2686
|
+
rescanInProgress = true;
|
|
2687
|
+
rescanDirectoryForDelta(
|
|
2630
2688
|
watchPath,
|
|
2631
2689
|
config,
|
|
2632
2690
|
extensions,
|
|
2633
2691
|
onEventFunc,
|
|
2634
2692
|
onFinishFunc,
|
|
2635
2693
|
trackSymlink
|
|
2636
|
-
)
|
|
2694
|
+
).finally(() => {
|
|
2695
|
+
rescanInProgress = false;
|
|
2696
|
+
});
|
|
2637
2697
|
}
|
|
2638
2698
|
return;
|
|
2639
2699
|
}
|
|
@@ -2693,6 +2753,24 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
2693
2753
|
})
|
|
2694
2754
|
)
|
|
2695
2755
|
);
|
|
2756
|
+
if (process.platform === "darwin") {
|
|
2757
|
+
rescanTimers.push(
|
|
2758
|
+
setInterval(() => {
|
|
2759
|
+
if (!ready || rescanInProgress) return;
|
|
2760
|
+
rescanInProgress = true;
|
|
2761
|
+
rescanDirectoryForDelta(
|
|
2762
|
+
watchPath,
|
|
2763
|
+
config,
|
|
2764
|
+
extensions,
|
|
2765
|
+
onEventFunc,
|
|
2766
|
+
onFinishFunc,
|
|
2767
|
+
trackSymlink
|
|
2768
|
+
).finally(() => {
|
|
2769
|
+
rescanInProgress = false;
|
|
2770
|
+
});
|
|
2771
|
+
}, RESCAN_INTERVAL_MS).unref()
|
|
2772
|
+
);
|
|
2773
|
+
}
|
|
2696
2774
|
}
|
|
2697
2775
|
readyPromises.push(
|
|
2698
2776
|
(async () => {
|
|
@@ -2712,28 +2790,13 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
2712
2790
|
killFileWatchers() {
|
|
2713
2791
|
Object.keys(fileWatchers).forEach((key) => fileWatchers[key].close());
|
|
2714
2792
|
parentWatchers.forEach((pw) => pw.close());
|
|
2793
|
+
rescanTimers.forEach((t) => clearInterval(t));
|
|
2715
2794
|
symlinkPollers.forEach((cancel) => cancel());
|
|
2716
2795
|
symlinkPollers.clear();
|
|
2717
2796
|
return fileWatchers;
|
|
2718
2797
|
}
|
|
2719
2798
|
};
|
|
2720
2799
|
}
|
|
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
2800
|
function handleWatchEvent(config, extensions, event, filePath, onEventFunc, onFinishFunc) {
|
|
2738
2801
|
if (event !== "unlinkDir" && !extensions.some((ext) => filePath.endsWith(`.${ext}`)))
|
|
2739
2802
|
return Promise.resolve();
|
|
@@ -2776,9 +2839,16 @@ async function rescanDirectoryForDelta(watchPath, config, extensions, onEventFun
|
|
|
2776
2839
|
try {
|
|
2777
2840
|
const entries = await readdir(watchPath, { withFileTypes: true, recursive: true });
|
|
2778
2841
|
const presentPaths = /* @__PURE__ */ new Set();
|
|
2842
|
+
const presentDirs = /* @__PURE__ */ new Set();
|
|
2843
|
+
presentDirs.add(watchPath);
|
|
2779
2844
|
for (const entry of entries) {
|
|
2845
|
+
if (entry.isDirectory()) {
|
|
2846
|
+
presentDirs.add(path8.join(entry.parentPath, entry.name));
|
|
2847
|
+
continue;
|
|
2848
|
+
}
|
|
2780
2849
|
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
|
|
2781
2850
|
const entryPath = path8.join(entry.parentPath, entry.name);
|
|
2851
|
+
presentDirs.add(entry.parentPath);
|
|
2782
2852
|
if (!extensions.some((ext) => entryPath.endsWith(`.${ext}`))) continue;
|
|
2783
2853
|
presentPaths.add(entryPath);
|
|
2784
2854
|
if (!(entryPath in config.fsTree)) {
|
|
@@ -2787,9 +2857,18 @@ async function rescanDirectoryForDelta(watchPath, config, extensions, onEventFun
|
|
|
2787
2857
|
}
|
|
2788
2858
|
}
|
|
2789
2859
|
const watchPrefix = watchPath + path8.sep;
|
|
2860
|
+
const firedDirPrefixes = [];
|
|
2790
2861
|
for (const trackedPath of Object.keys(config.fsTree)) {
|
|
2791
|
-
if (trackedPath.startsWith(watchPrefix)
|
|
2862
|
+
if (!trackedPath.startsWith(watchPrefix) || presentPaths.has(trackedPath)) continue;
|
|
2863
|
+
if (firedDirPrefixes.some((p) => trackedPath.startsWith(p + path8.sep))) continue;
|
|
2864
|
+
const parts = trackedPath.slice(watchPrefix.length).split(path8.sep);
|
|
2865
|
+
const goneDirPath = parts.slice(0, -1).map((_, i) => watchPrefix + parts.slice(0, i + 1).join(path8.sep)).find((p) => !presentDirs.has(p)) ?? null;
|
|
2866
|
+
if (goneDirPath !== null) {
|
|
2867
|
+
firedDirPrefixes.push(goneDirPath);
|
|
2868
|
+
handleWatchEvent(config, extensions, "unlinkDir", goneDirPath, onEventFunc, onFinishFunc);
|
|
2869
|
+
} else {
|
|
2792
2870
|
handleWatchEvent(config, extensions, "unlink", trackedPath, onEventFunc, onFinishFunc);
|
|
2871
|
+
}
|
|
2793
2872
|
}
|
|
2794
2873
|
} catch {
|
|
2795
2874
|
}
|
|
@@ -2806,18 +2885,36 @@ function mutateFSTree(fsTree, event, filePath) {
|
|
|
2806
2885
|
}
|
|
2807
2886
|
}
|
|
2808
2887
|
}
|
|
2888
|
+
async function classifyRenameEvent(fullPath, fsTree) {
|
|
2889
|
+
for (const delay of [0, OVERLAYFS_RENAME_RETRY_MS]) {
|
|
2890
|
+
if (delay) await new Promise((resolve) => setTimeout(resolve, delay));
|
|
2891
|
+
try {
|
|
2892
|
+
const statResult = await stat(fullPath);
|
|
2893
|
+
if (statResult.isDirectory()) return "addDir";
|
|
2894
|
+
return fsTree && fullPath in fsTree ? "change" : "add";
|
|
2895
|
+
} catch {
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
if (!fsTree) return null;
|
|
2899
|
+
if (fullPath in fsTree) return "unlink";
|
|
2900
|
+
return Object.keys(fsTree).some(
|
|
2901
|
+
(trackedPath) => trackedPath.startsWith(fullPath + "/") || trackedPath.startsWith(fullPath + "\\")
|
|
2902
|
+
) ? "unlinkDir" : null;
|
|
2903
|
+
}
|
|
2809
2904
|
function colorEvent(event) {
|
|
2810
2905
|
if (event === "change") return yellow("CHANGED:");
|
|
2811
2906
|
if (event === "add" || event === "addDir") return green("ADDED:");
|
|
2812
2907
|
return red("REMOVED:");
|
|
2813
2908
|
}
|
|
2814
|
-
var CHANGE_DEDUPE_MS, SYMLINK_POLL_INTERVAL_MS, OVERLAYFS_RENAME_RETRY_MS;
|
|
2909
|
+
var CHANGE_DEDUPE_MS, SYMLINK_POLL_INTERVAL_MS, OVERLAYFS_RENAME_RETRY_MS, RESCAN_INTERVAL_MS;
|
|
2815
2910
|
var init_file_watcher = __esm({
|
|
2816
2911
|
"lib/setup/file-watcher.ts"() {
|
|
2817
2912
|
init_color();
|
|
2913
|
+
init_default_project_config_values();
|
|
2818
2914
|
CHANGE_DEDUPE_MS = 10;
|
|
2819
2915
|
SYMLINK_POLL_INTERVAL_MS = 500;
|
|
2820
2916
|
OVERLAYFS_RENAME_RETRY_MS = 50;
|
|
2917
|
+
RESCAN_INTERVAL_MS = 1e3;
|
|
2821
2918
|
}
|
|
2822
2919
|
});
|
|
2823
2920
|
|
|
@@ -2934,7 +3031,8 @@ __export(run_exports, {
|
|
|
2934
3031
|
run: () => run
|
|
2935
3032
|
});
|
|
2936
3033
|
import fs12 from "node:fs/promises";
|
|
2937
|
-
import { normalize } from "node:path";
|
|
3034
|
+
import { join as join3, normalize } from "node:path";
|
|
3035
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
2938
3036
|
import { availableParallelism } from "node:os";
|
|
2939
3037
|
async function run(config) {
|
|
2940
3038
|
const browserPromise = config.watch ? null : launchBrowser(config);
|
|
@@ -3226,7 +3324,7 @@ async function addCachedContentMainHTML(projectRoot, cachedContent) {
|
|
|
3226
3324
|
} else {
|
|
3227
3325
|
const html = await readTemplate("setup/tests.hbs");
|
|
3228
3326
|
cachedContent.mainHTML = { filePath: `${projectRoot}/test/tests.html`, html };
|
|
3229
|
-
cachedContent.assets.add(
|
|
3327
|
+
cachedContent.assets.add(join3(resolveQunitxRoot(projectRoot), "vendor/qunit.css"));
|
|
3230
3328
|
}
|
|
3231
3329
|
return cachedContent;
|
|
3232
3330
|
}
|
|
@@ -3299,12 +3397,18 @@ function normalizeInternalAssetPathFromHTML(projectRoot, assetPath, htmlPath) {
|
|
|
3299
3397
|
const currentDirectory = htmlPath ? htmlPath.split("/").slice(0, -1).join("/") : projectRoot;
|
|
3300
3398
|
return assetPath.startsWith("./") ? normalize(`${currentDirectory}/${assetPath.slice(2)}`) : normalize(`${currentDirectory}/${assetPath}`);
|
|
3301
3399
|
}
|
|
3400
|
+
function resolveQunitxRoot(projectRoot) {
|
|
3401
|
+
const mainEntry = createRequire2(`${projectRoot}/package.json`).resolve("qunitx");
|
|
3402
|
+
const match = /^(.*[\\/]qunitx)[\\/]/.exec(mainEntry);
|
|
3403
|
+
if (!match) throw new Error(`Could not derive qunitx root from ${mainEntry}`);
|
|
3404
|
+
return match[1];
|
|
3405
|
+
}
|
|
3302
3406
|
var WATCH_NAV_TIMEOUT_MS, PAGE_CLOSE_GRACE_MS, STDOUT_FLUSH_GRACE_MS, KEEP_ALIVE_INTERVAL_MS, EXIT_CODE_SIGTERM;
|
|
3303
3407
|
var init_run = __esm({
|
|
3304
3408
|
"lib/commands/run.ts"() {
|
|
3305
3409
|
init_browser();
|
|
3306
3410
|
init_chrome_prelaunch();
|
|
3307
|
-
|
|
3411
|
+
init_web();
|
|
3308
3412
|
init_bind_server_to_port();
|
|
3309
3413
|
init_web_server();
|
|
3310
3414
|
init_open_output_in_browser();
|
|
@@ -3338,7 +3442,7 @@ init_color();
|
|
|
3338
3442
|
var package_default = {
|
|
3339
3443
|
name: "qunitx-cli",
|
|
3340
3444
|
type: "module",
|
|
3341
|
-
version: "0.
|
|
3445
|
+
version: "0.22.0",
|
|
3342
3446
|
description: "Browser runner for QUnitx: run your qunitx tests in google-chrome",
|
|
3343
3447
|
author: "Izel Nakri",
|
|
3344
3448
|
license: "MIT",
|
|
@@ -3395,8 +3499,11 @@ var package_default = {
|
|
|
3395
3499
|
devDependencies: {
|
|
3396
3500
|
"js-yaml": "^4.1.1",
|
|
3397
3501
|
prettier: "^3.8.3",
|
|
3398
|
-
qunitx: "^1.2.
|
|
3399
|
-
|
|
3502
|
+
qunitx: "^1.2.9",
|
|
3503
|
+
react: "^19.2.5",
|
|
3504
|
+
"react-dom": "^19.2.5",
|
|
3505
|
+
typescript: "^6.0.3",
|
|
3506
|
+
vue: "^3.5.33"
|
|
3400
3507
|
},
|
|
3401
3508
|
volta: {
|
|
3402
3509
|
node: "24.14.0"
|
|
@@ -3432,7 +3539,7 @@ ${color("--timeout")} : change default timeout per test case
|
|
|
3432
3539
|
${color("--output")} : folder to distribute built qunitx html and js that a webservers can run[default: tmp]
|
|
3433
3540
|
${color("--failFast")} : run the target file or folders with immediate abort if a single test fails
|
|
3434
3541
|
${color("--port")} : HTTP server port (auto-selects a free port if the given port is taken)[default: 1234]
|
|
3435
|
-
${color("--extensions")} : comma-separated file extensions to track for discovery and watch-mode rebuilds[default: js,ts]
|
|
3542
|
+
${color("--extensions")} : comma-separated file extensions to track for discovery and watch-mode rebuilds[default: js,ts,jsx,tsx]
|
|
3436
3543
|
${color("--browser")} : browser engine to run tests in: chromium, firefox, webkit[default: chromium]
|
|
3437
3544
|
${color("--before")} : run a script before the tests(i.e start a new web server before tests)
|
|
3438
3545
|
${color("--after")} : run a script after the tests(i.e save test results to a file)
|
|
@@ -3491,17 +3598,8 @@ async function findProjectRoot() {
|
|
|
3491
3598
|
}
|
|
3492
3599
|
}
|
|
3493
3600
|
|
|
3494
|
-
// lib/setup/default-project-config-values.ts
|
|
3495
|
-
var defaultProjectConfigValues = {
|
|
3496
|
-
output: "tmp",
|
|
3497
|
-
timeout: 2e4,
|
|
3498
|
-
failFast: false,
|
|
3499
|
-
port: 1234,
|
|
3500
|
-
extensions: ["js", "ts"],
|
|
3501
|
-
browser: "chromium"
|
|
3502
|
-
};
|
|
3503
|
-
|
|
3504
3601
|
// lib/commands/init.ts
|
|
3602
|
+
init_default_project_config_values();
|
|
3505
3603
|
init_read_template();
|
|
3506
3604
|
async function initializeProject() {
|
|
3507
3605
|
const projectRoot = await findProjectRoot();
|
|
@@ -3565,12 +3663,6 @@ function convertToPascalCase(str) {
|
|
|
3565
3663
|
}
|
|
3566
3664
|
|
|
3567
3665
|
// 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
3666
|
async function generateTestFiles() {
|
|
3575
3667
|
const projectRoot = await findProjectRoot();
|
|
3576
3668
|
const moduleName = pathToModuleName(process.argv[3]);
|
|
@@ -3586,37 +3678,25 @@ async function generateTestFiles() {
|
|
|
3586
3678
|
await fs5.writeFile(path10, testJSContent.replace("{{moduleName}}", moduleName));
|
|
3587
3679
|
console.log(green(`${path10} written`));
|
|
3588
3680
|
}
|
|
3681
|
+
function pathToModuleName(filePath) {
|
|
3682
|
+
const withoutExt = filePath.replace(/\.(js|ts)$/, "");
|
|
3683
|
+
const segments = withoutExt.split("/");
|
|
3684
|
+
const targetNames = segments[0] === "test" || segments[0] === "tests" ? segments.slice(1) : segments;
|
|
3685
|
+
return targetNames.map(convertToPascalCase).join(" | ");
|
|
3686
|
+
}
|
|
3589
3687
|
|
|
3590
3688
|
// lib/setup/config.ts
|
|
3689
|
+
init_default_project_config_values();
|
|
3591
3690
|
import fs7 from "node:fs/promises";
|
|
3691
|
+
import { createRequire } from "node:module";
|
|
3692
|
+
import { pathToFileURL } from "node:url";
|
|
3592
3693
|
|
|
3593
3694
|
// lib/setup/fs-tree.ts
|
|
3695
|
+
init_default_project_config_values();
|
|
3594
3696
|
import fs6, { glob as fsGlob } from "node:fs/promises";
|
|
3595
3697
|
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
3698
|
async function buildFSTree(fileAbsolutePaths, config = {}) {
|
|
3619
|
-
const targetExtensions = config.extensions ||
|
|
3699
|
+
const targetExtensions = config.extensions || defaultProjectConfigValues.extensions;
|
|
3620
3700
|
const fsTree = {};
|
|
3621
3701
|
await Promise.all(
|
|
3622
3702
|
fileAbsolutePaths.map(async (fileAbsolutePath) => {
|
|
@@ -3648,57 +3728,65 @@ async function buildFSTree(fileAbsolutePaths, config = {}) {
|
|
|
3648
3728
|
);
|
|
3649
3729
|
return fsTree;
|
|
3650
3730
|
}
|
|
3651
|
-
|
|
3652
|
-
// lib/setup/test-file-paths.ts
|
|
3653
|
-
import { matchesGlob } from "node:path";
|
|
3654
|
-
function isGlob2(str) {
|
|
3731
|
+
function isGlob(str) {
|
|
3655
3732
|
return /[*?{[]/.test(str);
|
|
3656
3733
|
}
|
|
3657
|
-
function
|
|
3658
|
-
const
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3734
|
+
async function readDirRecursive(dir, filter) {
|
|
3735
|
+
const entries = await fs6.readdir(dir, { recursive: true, withFileTypes: true });
|
|
3736
|
+
const candidates = entries.filter(
|
|
3737
|
+
(dirent) => (dirent.isFile() || dirent.isSymbolicLink()) && filter(dirent.name)
|
|
3738
|
+
);
|
|
3739
|
+
const resolvedPaths = await Promise.all(
|
|
3740
|
+
candidates.map(async (dirent) => {
|
|
3741
|
+
const fullPath = path3.join(dirent.parentPath, dirent.name);
|
|
3742
|
+
if (dirent.isFile()) return fullPath;
|
|
3743
|
+
try {
|
|
3744
|
+
const statResult = await fs6.stat(fullPath);
|
|
3745
|
+
return statResult.isFile() ? fullPath : null;
|
|
3746
|
+
} catch {
|
|
3747
|
+
return null;
|
|
3665
3748
|
}
|
|
3666
|
-
|
|
3667
|
-
},
|
|
3668
|
-
[[], [], []]
|
|
3749
|
+
})
|
|
3669
3750
|
);
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3751
|
+
return resolvedPaths.filter((resolvedPath) => resolvedPath !== null);
|
|
3752
|
+
}
|
|
3753
|
+
|
|
3754
|
+
// lib/setup/test-file-paths.ts
|
|
3755
|
+
import { matchesGlob } from "node:path";
|
|
3756
|
+
var GLOB_CHARS = /[*?{[]/;
|
|
3757
|
+
function setupTestFilePaths(inputs2) {
|
|
3758
|
+
const folders = [];
|
|
3759
|
+
const filesWithGlob = [];
|
|
3760
|
+
const filesWithoutGlob = [];
|
|
3761
|
+
inputs2.forEach((input) => {
|
|
3762
|
+
if (!pathIsFile(input)) {
|
|
3763
|
+
folders.push({ input, globFormat: `${input}/**` });
|
|
3764
|
+
} else if (isGlob2(input)) {
|
|
3765
|
+
filesWithGlob.push({ input, globFormat: input });
|
|
3766
|
+
} else {
|
|
3767
|
+
filesWithoutGlob.push({ input, globFormat: input });
|
|
3679
3768
|
}
|
|
3680
3769
|
});
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3770
|
+
const dedupedFolders = folders.filter((folder) => !isIncludedIn(folders, folder));
|
|
3771
|
+
const dedupedGlobFiles = filesWithGlob.filter(
|
|
3772
|
+
(file) => !isIncludedIn(dedupedFolders, file) && !isIncludedIn(filesWithGlob, file)
|
|
3773
|
+
);
|
|
3774
|
+
const dedupedPlainFiles = filesWithoutGlob.reduce((acc, file) => {
|
|
3775
|
+
if (!isIncludedIn(dedupedFolders, file) && !isIncludedIn(dedupedGlobFiles, file) && !isIncludedIn(acc, file)) {
|
|
3776
|
+
acc.push(file);
|
|
3684
3777
|
}
|
|
3685
|
-
|
|
3686
|
-
|
|
3778
|
+
return acc;
|
|
3779
|
+
}, []);
|
|
3780
|
+
return dedupedFolders.concat(dedupedGlobFiles, dedupedPlainFiles).map((meta) => meta.input);
|
|
3687
3781
|
}
|
|
3688
3782
|
function pathIsFile(path10) {
|
|
3689
|
-
|
|
3690
|
-
return inputs2[inputs2.length - 1].includes(".");
|
|
3783
|
+
return path10.includes(".", path10.lastIndexOf("/") + 1);
|
|
3691
3784
|
}
|
|
3692
|
-
function
|
|
3693
|
-
return paths.some((path10) =>
|
|
3694
|
-
if (path10 === targetPath) {
|
|
3695
|
-
return false;
|
|
3696
|
-
}
|
|
3697
|
-
return matchesGlob(targetPath.input, buildGlobFormat(path10));
|
|
3698
|
-
});
|
|
3785
|
+
function isIncludedIn(paths, target) {
|
|
3786
|
+
return paths.some((path10) => path10 !== target && matchesGlob(target.input, path10.globFormat));
|
|
3699
3787
|
}
|
|
3700
|
-
function
|
|
3701
|
-
return
|
|
3788
|
+
function isGlob2(str) {
|
|
3789
|
+
return GLOB_CHARS.test(str);
|
|
3702
3790
|
}
|
|
3703
3791
|
|
|
3704
3792
|
// lib/utils/parse-cli-flags.ts
|
|
@@ -3793,15 +3881,17 @@ async function setupConfig() {
|
|
|
3793
3881
|
const projectRoot = await findProjectRoot();
|
|
3794
3882
|
const cliConfigFlags = parseCliFlags(projectRoot);
|
|
3795
3883
|
const projectPackageJSON = await readConfigFromPackageJSON(projectRoot);
|
|
3884
|
+
const { plugins: rawPlugins, ...userQunitx } = projectPackageJSON.qunitx ?? {};
|
|
3885
|
+
const pluginsPromise = resolvePlugins(rawPlugins, projectRoot);
|
|
3796
3886
|
const inputs2 = cliConfigFlags.inputs.concat(readInputsFromPackageJSON(projectPackageJSON));
|
|
3797
3887
|
const config = {
|
|
3798
3888
|
...defaultProjectConfigValues,
|
|
3799
3889
|
htmlPaths: [],
|
|
3800
|
-
...
|
|
3890
|
+
...userQunitx,
|
|
3801
3891
|
...cliConfigFlags,
|
|
3802
3892
|
projectRoot,
|
|
3803
3893
|
inputs: inputs2,
|
|
3804
|
-
testFileLookupPaths: setupTestFilePaths(
|
|
3894
|
+
testFileLookupPaths: setupTestFilePaths(inputs2),
|
|
3805
3895
|
lastFailedTestFiles: null,
|
|
3806
3896
|
lastRanTestFiles: null,
|
|
3807
3897
|
COUNTER: {
|
|
@@ -3818,7 +3908,10 @@ async function setupConfig() {
|
|
|
3818
3908
|
_onTestsJsServed: null
|
|
3819
3909
|
};
|
|
3820
3910
|
config.htmlPaths = normalizeHTMLPaths(config.projectRoot, config.htmlPaths);
|
|
3821
|
-
config.fsTree = await
|
|
3911
|
+
[config.fsTree, config.plugins] = await Promise.all([
|
|
3912
|
+
buildFSTree(config.testFileLookupPaths, config),
|
|
3913
|
+
pluginsPromise
|
|
3914
|
+
]);
|
|
3822
3915
|
return config;
|
|
3823
3916
|
}
|
|
3824
3917
|
async function readConfigFromPackageJSON(projectRoot) {
|
|
@@ -3832,6 +3925,22 @@ function readInputsFromPackageJSON(packageJSON) {
|
|
|
3832
3925
|
const qunitx = packageJSON.qunitx;
|
|
3833
3926
|
return qunitx && qunitx.inputs ? qunitx.inputs : [];
|
|
3834
3927
|
}
|
|
3928
|
+
function resolvePlugins(raw, projectRoot) {
|
|
3929
|
+
if (raw == null) return Promise.resolve([]);
|
|
3930
|
+
if (!Array.isArray(raw)) {
|
|
3931
|
+
console.error(`# qunitx: package.json#qunitx.plugins must be an array`);
|
|
3932
|
+
process.exit(1);
|
|
3933
|
+
}
|
|
3934
|
+
const projectRequire = createRequire(`${projectRoot}/package.json`);
|
|
3935
|
+
return Promise.all(
|
|
3936
|
+
raw.map(async (entry) => {
|
|
3937
|
+
const [spec, options] = Array.isArray(entry) ? entry : [entry];
|
|
3938
|
+
const mod = await import(pathToFileURL(projectRequire.resolve(spec)).href);
|
|
3939
|
+
const exported = mod.default ?? mod;
|
|
3940
|
+
return typeof exported === "function" ? exported(options) : exported;
|
|
3941
|
+
})
|
|
3942
|
+
);
|
|
3943
|
+
}
|
|
3835
3944
|
|
|
3836
3945
|
// cli.ts
|
|
3837
3946
|
process4.title = "qunitx";
|