qunitx-cli 0.17.8 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +526 -124
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -387,9 +387,9 @@ var init_color = __esm({
|
|
|
387
387
|
|
|
388
388
|
// lib/utils/path-exists.ts
|
|
389
389
|
import fs2 from "node:fs/promises";
|
|
390
|
-
async function pathExists(
|
|
390
|
+
async function pathExists(path7) {
|
|
391
391
|
try {
|
|
392
|
-
await fs2.access(
|
|
392
|
+
await fs2.access(path7);
|
|
393
393
|
return true;
|
|
394
394
|
} catch {
|
|
395
395
|
return false;
|
|
@@ -488,12 +488,18 @@ function dumpValue(value, indent) {
|
|
|
488
488
|
if (Array.isArray(value)) {
|
|
489
489
|
if (value.length === 0) return "[]";
|
|
490
490
|
const next2 = `${indent} `;
|
|
491
|
-
return "\n" + value.map((item) =>
|
|
491
|
+
return "\n" + value.map((item) => {
|
|
492
|
+
const v = dumpValue(item, next2);
|
|
493
|
+
return v[0] === "\n" ? `${next2}-${v}` : `${next2}- ${v}`;
|
|
494
|
+
}).join("\n");
|
|
492
495
|
}
|
|
493
496
|
const entries = Object.entries(value);
|
|
494
497
|
if (entries.length === 0) return "{}";
|
|
495
498
|
const next = `${indent} `;
|
|
496
|
-
return "\n" + entries.map(([entryKey, entryValue]) =>
|
|
499
|
+
return "\n" + entries.map(([entryKey, entryValue]) => {
|
|
500
|
+
const v = dumpValue(entryValue, next);
|
|
501
|
+
return v[0] === "\n" ? `${next}${entryKey}:${v}` : `${next}${entryKey}: ${v}`;
|
|
502
|
+
}).join("\n");
|
|
497
503
|
}
|
|
498
504
|
function yamlLine(key, value) {
|
|
499
505
|
const serialized = dumpValue(value, "");
|
|
@@ -510,12 +516,12 @@ function dumpYaml({
|
|
|
510
516
|
at
|
|
511
517
|
}) {
|
|
512
518
|
return `name: ${dumpString(name, "")}
|
|
513
|
-
` + yamlLine("actual", actual) + yamlLine("expected", expected) + yamlLine("message", message) + yamlLine("stack", stack) + yamlLine("at", at);
|
|
519
|
+
` + yamlLine("actual", actual) + yamlLine("expected", expected) + (message !== null ? yamlLine("message", message) : "") + (stack !== null ? yamlLine("stack", stack) : "") + (at !== null ? yamlLine("at", at) : "");
|
|
514
520
|
}
|
|
515
521
|
var NEEDS_QUOTING;
|
|
516
522
|
var init_dump_yaml = __esm({
|
|
517
523
|
"lib/tap/dump-yaml.ts"() {
|
|
518
|
-
NEEDS_QUOTING =
|
|
524
|
+
NEEDS_QUOTING = /^$|^\s|^(null|true|false|~|yes|no|on|off|y|n)$|^[{[!|>'"#%@`]|^[-?:](\s|$)|^---|^[-+]?(\d|\.\d)|^\d{4}-\d{2}-\d{2}|: |#/i;
|
|
519
525
|
}
|
|
520
526
|
});
|
|
521
527
|
|
|
@@ -538,46 +544,56 @@ function TAPDisplayTestResult(COUNTER, details) {
|
|
|
538
544
|
COUNTER.testCount++;
|
|
539
545
|
if (details.status === "skipped") {
|
|
540
546
|
COUNTER.skipCount++;
|
|
541
|
-
|
|
547
|
+
process.stdout.write(`ok ${COUNTER.testCount} ${details.fullName.join(" | ")} # skip
|
|
548
|
+
`);
|
|
542
549
|
} else if (details.status === "todo") {
|
|
543
|
-
|
|
550
|
+
COUNTER.todoCount = (COUNTER.todoCount ?? 0) + 1;
|
|
551
|
+
process.stdout.write(`not ok ${COUNTER.testCount} ${details.fullName.join(" | ")} # TODO
|
|
552
|
+
`);
|
|
544
553
|
} else if (details.status === "failed") {
|
|
545
554
|
COUNTER.failCount++;
|
|
546
|
-
|
|
547
|
-
`not ok ${COUNTER.testCount}
|
|
548
|
-
|
|
549
|
-
`# (${details.runtime.toFixed(0)} ms)`
|
|
555
|
+
process.stdout.write(
|
|
556
|
+
`not ok ${COUNTER.testCount} ${details.fullName.join(" | ")} # (${details.runtime.toFixed(0)} ms)
|
|
557
|
+
`
|
|
550
558
|
);
|
|
551
559
|
details.assertions.forEach((assertion, index) => {
|
|
552
560
|
if (!assertion.passed && assertion.todo === false) {
|
|
553
561
|
COUNTER.errorCount = (COUNTER.errorCount ?? 0) + 1;
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
console.log(
|
|
562
|
+
process.stdout.write(" ---\n");
|
|
563
|
+
process.stdout.write(
|
|
557
564
|
indentString(
|
|
558
565
|
dumpYaml({
|
|
559
566
|
name: `Assertion #${index + 1}`,
|
|
560
|
-
actual: assertion.actual ? JSON.parse(JSON.stringify(assertion.actual, getCircularReplacer())) : assertion.actual,
|
|
561
|
-
expected: assertion.expected ? JSON.parse(JSON.stringify(assertion.expected, getCircularReplacer())) : assertion.expected,
|
|
567
|
+
actual: assertion.actual !== null && typeof assertion.actual === "object" ? JSON.parse(JSON.stringify(assertion.actual, getCircularReplacer())) : assertion.actual,
|
|
568
|
+
expected: assertion.expected !== null && typeof assertion.expected === "object" ? JSON.parse(JSON.stringify(assertion.expected, getCircularReplacer())) : assertion.expected,
|
|
562
569
|
message: assertion.message || null,
|
|
563
|
-
|
|
564
|
-
|
|
570
|
+
// Trim leading/trailing whitespace: Chrome stacks start with " at ..."
|
|
571
|
+
// (4 spaces per frame) which would otherwise render as "stack: at ..." in YAML.
|
|
572
|
+
stack: assertion.stack?.trim() || null,
|
|
573
|
+
at: extractStackAt(assertion.stack)
|
|
565
574
|
}),
|
|
566
575
|
4
|
|
567
576
|
)
|
|
568
577
|
);
|
|
569
|
-
|
|
578
|
+
process.stdout.write(" ...\n");
|
|
570
579
|
}
|
|
571
580
|
});
|
|
572
581
|
} else if (details.status === "passed") {
|
|
573
582
|
COUNTER.passCount++;
|
|
574
|
-
|
|
575
|
-
`ok ${COUNTER.testCount}
|
|
576
|
-
|
|
577
|
-
`# (${details.runtime.toFixed(0)} ms)`
|
|
583
|
+
process.stdout.write(
|
|
584
|
+
`ok ${COUNTER.testCount} ${details.fullName.join(" | ")} # (${details.runtime.toFixed(0)} ms)
|
|
585
|
+
`
|
|
578
586
|
);
|
|
579
587
|
}
|
|
580
588
|
}
|
|
589
|
+
function extractStackAt(stack) {
|
|
590
|
+
if (!stack) return null;
|
|
591
|
+
const chromeMatch = stack.match(/\(([^)\n]+:[0-9]+:[0-9]+)\)/);
|
|
592
|
+
if (chromeMatch) return chromeMatch[1].replace("file://", "");
|
|
593
|
+
const geckoMatch = stack.match(/@([^\s\n@]+:[0-9]+:[0-9]+)/);
|
|
594
|
+
if (geckoMatch) return geckoMatch[1];
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
581
597
|
function getCircularReplacer() {
|
|
582
598
|
const ancestors = [];
|
|
583
599
|
return function(_key, value) {
|
|
@@ -728,8 +744,8 @@ var init_http = __esm({
|
|
|
728
744
|
});
|
|
729
745
|
}
|
|
730
746
|
/** Registers a GET route handler. */
|
|
731
|
-
get(
|
|
732
|
-
this.#registerRouteHandler("GET",
|
|
747
|
+
get(path7, handler) {
|
|
748
|
+
this.#registerRouteHandler("GET", path7, handler);
|
|
733
749
|
}
|
|
734
750
|
/**
|
|
735
751
|
* Starts listening on the given port (0 = OS-assigned).
|
|
@@ -760,30 +776,30 @@ var init_http = __esm({
|
|
|
760
776
|
});
|
|
761
777
|
}
|
|
762
778
|
/** Registers a POST route handler. */
|
|
763
|
-
post(
|
|
764
|
-
this.#registerRouteHandler("POST",
|
|
779
|
+
post(path7, handler) {
|
|
780
|
+
this.#registerRouteHandler("POST", path7, handler);
|
|
765
781
|
}
|
|
766
782
|
/** Registers a DELETE route handler. */
|
|
767
|
-
delete(
|
|
768
|
-
this.#registerRouteHandler("DELETE",
|
|
783
|
+
delete(path7, handler) {
|
|
784
|
+
this.#registerRouteHandler("DELETE", path7, handler);
|
|
769
785
|
}
|
|
770
786
|
/** Registers a PUT route handler. */
|
|
771
|
-
put(
|
|
772
|
-
this.#registerRouteHandler("PUT",
|
|
787
|
+
put(path7, handler) {
|
|
788
|
+
this.#registerRouteHandler("PUT", path7, handler);
|
|
773
789
|
}
|
|
774
790
|
/** Adds a middleware function to the chain. */
|
|
775
791
|
use(middleware) {
|
|
776
792
|
this.middleware.push(middleware);
|
|
777
793
|
}
|
|
778
|
-
#registerRouteHandler(method,
|
|
794
|
+
#registerRouteHandler(method, path7, handler) {
|
|
779
795
|
if (!this.routes[method]) {
|
|
780
796
|
this.routes[method] = {};
|
|
781
797
|
}
|
|
782
|
-
this.routes[method][
|
|
783
|
-
path:
|
|
798
|
+
this.routes[method][path7] = {
|
|
799
|
+
path: path7,
|
|
784
800
|
handler,
|
|
785
|
-
paramNames: this.#extractParamNames(
|
|
786
|
-
isWildcard:
|
|
801
|
+
paramNames: this.#extractParamNames(path7),
|
|
802
|
+
isWildcard: path7 === "/*"
|
|
787
803
|
};
|
|
788
804
|
}
|
|
789
805
|
#handleRequest(req, res) {
|
|
@@ -821,13 +837,13 @@ var init_http = __esm({
|
|
|
821
837
|
return null;
|
|
822
838
|
}
|
|
823
839
|
return routes[url] || Object.values(routes).find((route) => {
|
|
824
|
-
const { path:
|
|
825
|
-
if (!isWildcard && !
|
|
840
|
+
const { path: path7, isWildcard } = route;
|
|
841
|
+
if (!isWildcard && !path7.includes(":")) {
|
|
826
842
|
return false;
|
|
827
843
|
}
|
|
828
|
-
if (isWildcard || this.#matchPathSegments(
|
|
844
|
+
if (isWildcard || this.#matchPathSegments(path7, url)) {
|
|
829
845
|
if (route.paramNames.length > 0) {
|
|
830
|
-
const regexPattern = this.#buildRegexPattern(
|
|
846
|
+
const regexPattern = this.#buildRegexPattern(path7, route.paramNames);
|
|
831
847
|
const regex = new RegExp(`^${regexPattern}$`);
|
|
832
848
|
const regexMatches = regex.exec(url);
|
|
833
849
|
if (regexMatches) {
|
|
@@ -839,8 +855,8 @@ var init_http = __esm({
|
|
|
839
855
|
return false;
|
|
840
856
|
}) || routes["/*"] || null;
|
|
841
857
|
}
|
|
842
|
-
#matchPathSegments(
|
|
843
|
-
const pathSegments =
|
|
858
|
+
#matchPathSegments(path7, url) {
|
|
859
|
+
const pathSegments = path7.split("/");
|
|
844
860
|
const urlSegments = url.split("/");
|
|
845
861
|
if (pathSegments.length !== urlSegments.length) {
|
|
846
862
|
return false;
|
|
@@ -857,14 +873,14 @@ var init_http = __esm({
|
|
|
857
873
|
}
|
|
858
874
|
return true;
|
|
859
875
|
}
|
|
860
|
-
#buildRegexPattern(
|
|
861
|
-
let regexPattern =
|
|
876
|
+
#buildRegexPattern(path7, _paramNames) {
|
|
877
|
+
let regexPattern = path7.replace(/:[^/]+/g, "([^/]+)");
|
|
862
878
|
regexPattern = regexPattern.replace(/\//g, "\\/");
|
|
863
879
|
return regexPattern;
|
|
864
880
|
}
|
|
865
|
-
#extractParamNames(
|
|
881
|
+
#extractParamNames(path7) {
|
|
866
882
|
const paramRegex = /:(\w+)/g;
|
|
867
|
-
const paramMatches =
|
|
883
|
+
const paramMatches = path7.match(paramRegex);
|
|
868
884
|
return paramMatches ? paramMatches.map((match) => match.slice(1)) : [];
|
|
869
885
|
}
|
|
870
886
|
#extractParams(route, _url) {
|
|
@@ -898,7 +914,7 @@ function setupWebServer(config, cachedContent) {
|
|
|
898
914
|
config._onWsOpen?.();
|
|
899
915
|
} else if (event === "connection") {
|
|
900
916
|
config._phase = "running";
|
|
901
|
-
if (!config._groupMode)
|
|
917
|
+
if (!config._groupMode) process.stdout.write("TAP version 13\n");
|
|
902
918
|
if (config.debug && config._groupMode) {
|
|
903
919
|
const allFiles = Object.keys(config.fsTree);
|
|
904
920
|
const relFiles = allFiles.map(
|
|
@@ -907,7 +923,8 @@ function setupWebServer(config, cachedContent) {
|
|
|
907
923
|
const shown = relFiles.slice(0, 2);
|
|
908
924
|
const rest = relFiles.length - shown.length;
|
|
909
925
|
const fileList = rest > 0 ? `${shown.join(" ")} +${rest} more` : shown.join(" ");
|
|
910
|
-
|
|
926
|
+
process.stdout.write(`# ${blue(`\u2500\u2500 ${fileList} \u2500\u2500`)}
|
|
927
|
+
`);
|
|
911
928
|
}
|
|
912
929
|
config._resetTestTimeout?.();
|
|
913
930
|
} else if (event === "testEnd" && !abort) {
|
|
@@ -915,8 +932,9 @@ function setupWebServer(config, cachedContent) {
|
|
|
915
932
|
config.lastFailedTestFiles = config.lastRanTestFiles;
|
|
916
933
|
}
|
|
917
934
|
if (config.debug && details.runtime > config.timeout * 0.8) {
|
|
918
|
-
|
|
919
|
-
`# SLOW (${details.runtime.toFixed(0)}ms / ${config.timeout}ms timeout): ${details.fullName.join(" | ")}
|
|
935
|
+
process.stdout.write(
|
|
936
|
+
`# SLOW (${details.runtime.toFixed(0)}ms / ${config.timeout}ms timeout): ${details.fullName.join(" | ")}
|
|
937
|
+
`
|
|
920
938
|
);
|
|
921
939
|
}
|
|
922
940
|
config._resetTestTimeout?.();
|
|
@@ -924,8 +942,9 @@ function setupWebServer(config, cachedContent) {
|
|
|
924
942
|
} else if (event === "done") {
|
|
925
943
|
config._phase = "done";
|
|
926
944
|
if (config.debug && config._groupMode) {
|
|
927
|
-
|
|
928
|
-
`# group done: ${details.passed} passed, ${details.failed} failed (${details.runtime}ms)
|
|
945
|
+
process.stdout.write(
|
|
946
|
+
`# group done: ${details.passed} passed, ${details.failed} failed (${details.runtime}ms)
|
|
947
|
+
`
|
|
929
948
|
);
|
|
930
949
|
}
|
|
931
950
|
if (typeof config._testRunDone === "function") {
|
|
@@ -937,8 +956,9 @@ function setupWebServer(config, cachedContent) {
|
|
|
937
956
|
});
|
|
938
957
|
server.get("/tests.js", (_req, res) => {
|
|
939
958
|
const bytes = cachedContent.allTestCode?.length ?? null;
|
|
940
|
-
|
|
941
|
-
`# [HTTPServer] GET /tests.js \u2192 ${bytes !== null ? `${bytes} bytes` : "NOT READY (allTestCode is null)"}
|
|
959
|
+
process.stdout.write(
|
|
960
|
+
`# [HTTPServer] GET /tests.js \u2192 ${bytes !== null ? `${bytes} bytes` : "NOT READY (allTestCode is null)"}
|
|
961
|
+
`
|
|
942
962
|
);
|
|
943
963
|
if (bytes === null) {
|
|
944
964
|
res.writeHead(503, { "Content-Type": "application/javascript", "Cache-Control": "no-store" });
|
|
@@ -953,8 +973,9 @@ function setupWebServer(config, cachedContent) {
|
|
|
953
973
|
});
|
|
954
974
|
server.get("/filtered-tests.js", (_req, res) => {
|
|
955
975
|
const bytes = cachedContent.filteredTestCode?.length ?? null;
|
|
956
|
-
|
|
957
|
-
`# [HTTPServer] GET /filtered-tests.js \u2192 ${bytes !== null ? `${bytes} bytes` : "NOT READY (filteredTestCode is null)"}
|
|
976
|
+
process.stdout.write(
|
|
977
|
+
`# [HTTPServer] GET /filtered-tests.js \u2192 ${bytes !== null ? `${bytes} bytes` : "NOT READY (filteredTestCode is null)"}
|
|
978
|
+
`
|
|
958
979
|
);
|
|
959
980
|
if (bytes === null) {
|
|
960
981
|
res.writeHead(503, { "Content-Type": "application/javascript", "Cache-Control": "no-store" });
|
|
@@ -968,6 +989,23 @@ function setupWebServer(config, cachedContent) {
|
|
|
968
989
|
res.end(cachedContent.filteredTestCode);
|
|
969
990
|
});
|
|
970
991
|
server.get("/", async (_req, res) => {
|
|
992
|
+
if (cachedContent._buildError) {
|
|
993
|
+
const htmlContent2 = buildErrorHTML(cachedContent._buildError);
|
|
994
|
+
res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
|
|
995
|
+
res.write(htmlContent2);
|
|
996
|
+
res.end();
|
|
997
|
+
return await fsPromise.writeFile(
|
|
998
|
+
`${config.projectRoot}/${config.output}/index.html`,
|
|
999
|
+
htmlContent2
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
if (cachedContent._noTestsWarning) {
|
|
1003
|
+
const htmlContent2 = buildNoTestsHTML(cachedContent._noTestsWarning);
|
|
1004
|
+
res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
|
|
1005
|
+
res.write(htmlContent2);
|
|
1006
|
+
res.end();
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
971
1009
|
const TEST_RUNTIME_TO_INJECT = testRuntimeToInject(config.port, config);
|
|
972
1010
|
const htmlContent = escapeAndInjectTestsToHTML(
|
|
973
1011
|
mainHTMLWithReplacedAssets,
|
|
@@ -983,6 +1021,23 @@ function setupWebServer(config, cachedContent) {
|
|
|
983
1021
|
);
|
|
984
1022
|
});
|
|
985
1023
|
server.get("/qunitx.html", async (_req, res) => {
|
|
1024
|
+
if (cachedContent._buildError) {
|
|
1025
|
+
const htmlContent2 = buildErrorHTML(cachedContent._buildError);
|
|
1026
|
+
res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
|
|
1027
|
+
res.write(htmlContent2);
|
|
1028
|
+
res.end();
|
|
1029
|
+
return await fsPromise.writeFile(
|
|
1030
|
+
`${config.projectRoot}/${config.output}/qunitx.html`,
|
|
1031
|
+
htmlContent2
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
if (cachedContent._noTestsWarning) {
|
|
1035
|
+
const htmlContent2 = buildNoTestsHTML(cachedContent._noTestsWarning);
|
|
1036
|
+
res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
|
|
1037
|
+
res.write(htmlContent2);
|
|
1038
|
+
res.end();
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
986
1041
|
const TEST_RUNTIME_TO_INJECT = testRuntimeToInject(config.port, config);
|
|
987
1042
|
const htmlContent = escapeAndInjectTestsToHTML(
|
|
988
1043
|
mainHTMLWithReplacedAssets,
|
|
@@ -1026,7 +1081,10 @@ function setupWebServer(config, cachedContent) {
|
|
|
1026
1081
|
} else {
|
|
1027
1082
|
fs8.createReadStream(filePath).pipe(res);
|
|
1028
1083
|
}
|
|
1029
|
-
|
|
1084
|
+
process.stdout.write(
|
|
1085
|
+
`# [HTTPServer] GET ${url} ${statusCode} - ${/* @__PURE__ */ new Date() - requestStartedAt}ms
|
|
1086
|
+
`
|
|
1087
|
+
);
|
|
1030
1088
|
});
|
|
1031
1089
|
return server;
|
|
1032
1090
|
}
|
|
@@ -1140,7 +1198,15 @@ function testRuntimeToInject(port, config) {
|
|
|
1140
1198
|
|
|
1141
1199
|
if (!window.QUnit) {
|
|
1142
1200
|
console.log('QUnit not found after WebSocket connected');
|
|
1143
|
-
window.
|
|
1201
|
+
if (window.IS_PLAYWRIGHT) {
|
|
1202
|
+
// Signal the Playwright runner that the run is complete with 0 tests rather than
|
|
1203
|
+
// waiting for the inactivity timeout. The runner treats totalTests === 0 as a
|
|
1204
|
+
// "no tests registered" warning (not a failure), so this gives a fast, clean result.
|
|
1205
|
+
window.QUNIT_RESULT = { totalTests: 0, finishedTests: 0, failedTests: 0, currentTest: null };
|
|
1206
|
+
window.socket.send(JSON.stringify({ event: 'done', details: { passed: 0, failed: 0, runtime: 0 } }));
|
|
1207
|
+
} else {
|
|
1208
|
+
window.testTimeout = ${config.timeout};
|
|
1209
|
+
}
|
|
1144
1210
|
return;
|
|
1145
1211
|
}
|
|
1146
1212
|
|
|
@@ -1192,6 +1258,211 @@ function escapeAndInjectTestsToHTML(html, testRuntimeCode, testBundleUrl) {
|
|
|
1192
1258
|
return injectScript(html, `${testRuntimeCode}
|
|
1193
1259
|
<script src="${testBundleUrl}" async></script>`);
|
|
1194
1260
|
}
|
|
1261
|
+
function buildNoTestsHTML(files) {
|
|
1262
|
+
const escaped = files.map((f) => f.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")).join("\n");
|
|
1263
|
+
return `<!DOCTYPE html>
|
|
1264
|
+
<html lang="en">
|
|
1265
|
+
<head>
|
|
1266
|
+
<meta charset="utf-8">
|
|
1267
|
+
<meta name="viewport" content="width=device-width">
|
|
1268
|
+
<title>No Tests Registered \u2014 qunitx</title>
|
|
1269
|
+
<style>
|
|
1270
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
1271
|
+
#qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-tests, #qunit-tests li {
|
|
1272
|
+
font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
|
|
1273
|
+
}
|
|
1274
|
+
#qunit-header {
|
|
1275
|
+
padding: 0.5em 0 0.5em 1em;
|
|
1276
|
+
color: #C2CCD1;
|
|
1277
|
+
background-color: #0D3349;
|
|
1278
|
+
font-size: 1.5em;
|
|
1279
|
+
line-height: 1em;
|
|
1280
|
+
font-weight: 400;
|
|
1281
|
+
border-radius: 5px 5px 0 0;
|
|
1282
|
+
}
|
|
1283
|
+
#qunit-banner { height: 5px; background-color: #F0AD4E; }
|
|
1284
|
+
#qunit-userAgent {
|
|
1285
|
+
padding: 0.5em 1em;
|
|
1286
|
+
color: #fff;
|
|
1287
|
+
background-color: #EC971F;
|
|
1288
|
+
text-shadow: rgba(0,0,0,.3) 2px 2px 1px;
|
|
1289
|
+
font-size: small;
|
|
1290
|
+
}
|
|
1291
|
+
#qunit-tests { list-style: none; font-size: smaller; }
|
|
1292
|
+
#qunit-tests li.warn {
|
|
1293
|
+
display: list-item;
|
|
1294
|
+
padding: 0.4em 1em;
|
|
1295
|
+
border-bottom: 1px solid #fff;
|
|
1296
|
+
color: #000;
|
|
1297
|
+
background-color: #FCF8E3;
|
|
1298
|
+
border-left: 5px solid #F0AD4E;
|
|
1299
|
+
}
|
|
1300
|
+
#qunit-tests li.warn:last-child { border-radius: 0 0 5px 5px; }
|
|
1301
|
+
.qunit-assert-list { margin-top: 0.5em; padding: 0.5em; background-color: #fff; border-radius: 5px; list-style: none; }
|
|
1302
|
+
.qunit-assert-list > li {
|
|
1303
|
+
padding: 5px;
|
|
1304
|
+
background-color: #FFF8DC;
|
|
1305
|
+
border-left: 10px solid #F0AD4E;
|
|
1306
|
+
color: #8A6D3B;
|
|
1307
|
+
}
|
|
1308
|
+
.qunit-assert-list pre {
|
|
1309
|
+
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
|
|
1310
|
+
font-size: 12px;
|
|
1311
|
+
line-height: 1.6;
|
|
1312
|
+
white-space: pre-wrap;
|
|
1313
|
+
word-break: break-word;
|
|
1314
|
+
color: #8A6D3B;
|
|
1315
|
+
margin: 0;
|
|
1316
|
+
}
|
|
1317
|
+
#qunit-testresult {
|
|
1318
|
+
padding: 0.5em 1em;
|
|
1319
|
+
color: #366097;
|
|
1320
|
+
background-color: #E2F0F7;
|
|
1321
|
+
border-bottom: 1px solid #fff;
|
|
1322
|
+
font-size: small;
|
|
1323
|
+
}
|
|
1324
|
+
.dots span { display: inline-block; animation: pulse 1.4s ease-in-out infinite; }
|
|
1325
|
+
.dots span:nth-child(2) { animation-delay: .2s; }
|
|
1326
|
+
.dots span:nth-child(3) { animation-delay: .4s; }
|
|
1327
|
+
@keyframes pulse { 0%,100% { opacity: .2; } 50% { opacity: 1; } }
|
|
1328
|
+
</style>
|
|
1329
|
+
</head>
|
|
1330
|
+
<body>
|
|
1331
|
+
<div id="qunit">
|
|
1332
|
+
<h1 id="qunit-header">qunitx</h1>
|
|
1333
|
+
<h2 id="qunit-banner"></h2>
|
|
1334
|
+
<div id="qunit-userAgent">Warning: No Tests Registered</div>
|
|
1335
|
+
<ol id="qunit-tests">
|
|
1336
|
+
<li class="warn">
|
|
1337
|
+
<strong>0 QUnit tests were registered in the bundled file(s)</strong>
|
|
1338
|
+
<ol class="qunit-assert-list">
|
|
1339
|
+
<li><pre>${escaped}</pre></li>
|
|
1340
|
+
</ol>
|
|
1341
|
+
</li>
|
|
1342
|
+
</ol>
|
|
1343
|
+
<div id="qunit-testresult">
|
|
1344
|
+
Watching for changes <span class="dots"><span>●</span><span>●</span><span>●</span></span>
|
|
1345
|
+
</div>
|
|
1346
|
+
</div>
|
|
1347
|
+
<script>
|
|
1348
|
+
if (location.port) {
|
|
1349
|
+
(function () {
|
|
1350
|
+
var retries = 0;
|
|
1351
|
+
function connect() {
|
|
1352
|
+
var ws = new WebSocket('ws://' + location.hostname + ':' + location.port);
|
|
1353
|
+
ws.addEventListener('message', function (e) { if (e.data === 'refresh') location.reload(true); });
|
|
1354
|
+
ws.addEventListener('close', function () { if (retries++ < 120) setTimeout(connect, 1000); });
|
|
1355
|
+
ws.addEventListener('error', function () { ws.close(); });
|
|
1356
|
+
}
|
|
1357
|
+
connect();
|
|
1358
|
+
})();
|
|
1359
|
+
}
|
|
1360
|
+
</script>
|
|
1361
|
+
</body>
|
|
1362
|
+
</html>`;
|
|
1363
|
+
}
|
|
1364
|
+
function buildErrorHTML(buildError) {
|
|
1365
|
+
const escaped = buildError.formatted.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1366
|
+
return `<!DOCTYPE html>
|
|
1367
|
+
<html lang="en">
|
|
1368
|
+
<head>
|
|
1369
|
+
<meta charset="utf-8">
|
|
1370
|
+
<meta name="viewport" content="width=device-width">
|
|
1371
|
+
<title>Build Error \u2014 qunitx</title>
|
|
1372
|
+
<style>
|
|
1373
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
1374
|
+
#qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-tests, #qunit-tests li {
|
|
1375
|
+
font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
|
|
1376
|
+
}
|
|
1377
|
+
#qunit-header {
|
|
1378
|
+
padding: 0.5em 0 0.5em 1em;
|
|
1379
|
+
color: #C2CCD1;
|
|
1380
|
+
background-color: #0D3349;
|
|
1381
|
+
font-size: 1.5em;
|
|
1382
|
+
line-height: 1em;
|
|
1383
|
+
font-weight: 400;
|
|
1384
|
+
border-radius: 5px 5px 0 0;
|
|
1385
|
+
}
|
|
1386
|
+
#qunit-banner { height: 5px; background-color: #EE5757; }
|
|
1387
|
+
#qunit-userAgent {
|
|
1388
|
+
padding: 0.5em 1em;
|
|
1389
|
+
color: #fff;
|
|
1390
|
+
background-color: #2B81AF;
|
|
1391
|
+
text-shadow: rgba(0,0,0,.5) 2px 2px 1px;
|
|
1392
|
+
font-size: small;
|
|
1393
|
+
}
|
|
1394
|
+
#qunit-tests { list-style: none; font-size: smaller; }
|
|
1395
|
+
#qunit-tests li.fail {
|
|
1396
|
+
display: list-item;
|
|
1397
|
+
padding: 0.4em 1em;
|
|
1398
|
+
border-bottom: 1px solid #fff;
|
|
1399
|
+
color: #000;
|
|
1400
|
+
background-color: #EE5757;
|
|
1401
|
+
}
|
|
1402
|
+
#qunit-tests li.fail:last-child { border-radius: 0 0 5px 5px; }
|
|
1403
|
+
.qunit-assert-list { margin-top: 0.5em; padding: 0.5em; background-color: #fff; border-radius: 5px; list-style: none; }
|
|
1404
|
+
.qunit-assert-list > li {
|
|
1405
|
+
padding: 5px;
|
|
1406
|
+
background-color: #fff;
|
|
1407
|
+
border-left: 10px solid #EE5757;
|
|
1408
|
+
color: #710909;
|
|
1409
|
+
}
|
|
1410
|
+
.qunit-assert-list pre {
|
|
1411
|
+
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
|
|
1412
|
+
font-size: 12px;
|
|
1413
|
+
line-height: 1.6;
|
|
1414
|
+
white-space: pre-wrap;
|
|
1415
|
+
word-break: break-word;
|
|
1416
|
+
color: #710909;
|
|
1417
|
+
margin: 0;
|
|
1418
|
+
}
|
|
1419
|
+
#qunit-testresult {
|
|
1420
|
+
padding: 0.5em 1em;
|
|
1421
|
+
color: #366097;
|
|
1422
|
+
background-color: #E2F0F7;
|
|
1423
|
+
border-bottom: 1px solid #fff;
|
|
1424
|
+
font-size: small;
|
|
1425
|
+
}
|
|
1426
|
+
.dots span { display: inline-block; animation: pulse 1.4s ease-in-out infinite; }
|
|
1427
|
+
.dots span:nth-child(2) { animation-delay: .2s; }
|
|
1428
|
+
.dots span:nth-child(3) { animation-delay: .4s; }
|
|
1429
|
+
@keyframes pulse { 0%,100% { opacity: .2; } 50% { opacity: 1; } }
|
|
1430
|
+
</style>
|
|
1431
|
+
</head>
|
|
1432
|
+
<body>
|
|
1433
|
+
<div id="qunit">
|
|
1434
|
+
<h1 id="qunit-header">qunitx</h1>
|
|
1435
|
+
<h2 id="qunit-banner"></h2>
|
|
1436
|
+
<div id="qunit-userAgent">Build Error: ${buildError.type}</div>
|
|
1437
|
+
<ol id="qunit-tests">
|
|
1438
|
+
<li class="fail">
|
|
1439
|
+
<strong>esbuild failed to bundle test files</strong>
|
|
1440
|
+
<ol class="qunit-assert-list">
|
|
1441
|
+
<li><pre>${escaped}</pre></li>
|
|
1442
|
+
</ol>
|
|
1443
|
+
</li>
|
|
1444
|
+
</ol>
|
|
1445
|
+
<div id="qunit-testresult">
|
|
1446
|
+
Watching for changes <span class="dots"><span>●</span><span>●</span><span>●</span></span>
|
|
1447
|
+
</div>
|
|
1448
|
+
</div>
|
|
1449
|
+
<script>
|
|
1450
|
+
if (location.port) {
|
|
1451
|
+
(function () {
|
|
1452
|
+
var retries = 0;
|
|
1453
|
+
function connect() {
|
|
1454
|
+
var ws = new WebSocket('ws://' + location.hostname + ':' + location.port);
|
|
1455
|
+
ws.addEventListener('message', function (e) { if (e.data === 'refresh') location.reload(true); });
|
|
1456
|
+
ws.addEventListener('close', function () { if (retries++ < 120) setTimeout(connect, 1000); });
|
|
1457
|
+
ws.addEventListener('error', function () { ws.close(); });
|
|
1458
|
+
}
|
|
1459
|
+
connect();
|
|
1460
|
+
})();
|
|
1461
|
+
}
|
|
1462
|
+
</script>
|
|
1463
|
+
</body>
|
|
1464
|
+
</html>`;
|
|
1465
|
+
}
|
|
1195
1466
|
var fsPromise;
|
|
1196
1467
|
var init_web_server = __esm({
|
|
1197
1468
|
"lib/setup/web-server.ts"() {
|
|
@@ -1259,7 +1530,9 @@ async function setupBrowser(config, cachedContent, existingBrowser = null) {
|
|
|
1259
1530
|
perfLog(`browser.js: setupWebServer took ${Date.now() - setupStart}ms`);
|
|
1260
1531
|
const browser = resolvedExistingBrowser || await launchBrowser(config);
|
|
1261
1532
|
const pageStart = Date.now();
|
|
1262
|
-
const
|
|
1533
|
+
const isHeadedWatchMode = config.open === true && config.watch;
|
|
1534
|
+
const getPage = isHeadedWatchMode ? () => browser.contexts()[0]?.pages()[0] ?? browser.newPage() : () => browser.newPage();
|
|
1535
|
+
const [page] = await Promise.all([getPage(), bindServerToPort(server, config)]);
|
|
1263
1536
|
perfLog(`browser.js: newPage + bindServerToPort took ${Date.now() - pageStart}ms`);
|
|
1264
1537
|
await page.addInitScript(() => {
|
|
1265
1538
|
window.IS_PLAYWRIGHT = true;
|
|
@@ -1365,15 +1638,23 @@ var init_run_user_module = __esm({
|
|
|
1365
1638
|
});
|
|
1366
1639
|
|
|
1367
1640
|
// lib/tap/display-final-result.ts
|
|
1368
|
-
function TAPDisplayFinalResult({ testCount, passCount, skipCount, failCount }, timeTaken) {
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1641
|
+
function TAPDisplayFinalResult({ testCount, passCount, skipCount, todoCount, failCount }, timeTaken) {
|
|
1642
|
+
process.stdout.write("\n");
|
|
1643
|
+
process.stdout.write(`1..${testCount}
|
|
1644
|
+
`);
|
|
1645
|
+
process.stdout.write(`# tests ${testCount}
|
|
1646
|
+
`);
|
|
1647
|
+
process.stdout.write(`# pass ${passCount}
|
|
1648
|
+
`);
|
|
1649
|
+
process.stdout.write(`# skip ${skipCount}
|
|
1650
|
+
`);
|
|
1651
|
+
process.stdout.write(`# todo ${todoCount}
|
|
1652
|
+
`);
|
|
1653
|
+
process.stdout.write(`# fail ${failCount}
|
|
1654
|
+
`);
|
|
1655
|
+
process.stdout.write(`# duration ${timeTaken}
|
|
1656
|
+
`);
|
|
1657
|
+
process.stdout.write("\n");
|
|
1377
1658
|
}
|
|
1378
1659
|
var init_display_final_result = __esm({
|
|
1379
1660
|
"lib/tap/display-final-result.ts"() {
|
|
@@ -1382,7 +1663,36 @@ var init_display_final_result = __esm({
|
|
|
1382
1663
|
|
|
1383
1664
|
// lib/commands/run/tests-in-browser.ts
|
|
1384
1665
|
import fs9 from "node:fs/promises";
|
|
1666
|
+
import path5 from "node:path";
|
|
1385
1667
|
import esbuild from "esbuild";
|
|
1668
|
+
function deriveBuildErrorType(error) {
|
|
1669
|
+
const msgs = error?.errors ?? [];
|
|
1670
|
+
const text = msgs[0]?.text ?? (error instanceof Error ? error.message : String(error));
|
|
1671
|
+
if (/could not resolve|cannot find module|no such file/i.test(text))
|
|
1672
|
+
return "Module Resolution Error";
|
|
1673
|
+
if (/unexpected token|expected .* but found|unterminated/i.test(text)) return "Syntax Error";
|
|
1674
|
+
if (/is not (defined|a function)|cannot read prop/i.test(text)) return "Reference Error";
|
|
1675
|
+
return "Build Error";
|
|
1676
|
+
}
|
|
1677
|
+
function formatBuildErrors(error) {
|
|
1678
|
+
const msgs = error?.errors ?? [];
|
|
1679
|
+
if (msgs.length > 0) {
|
|
1680
|
+
return msgs.map((msg, i) => {
|
|
1681
|
+
const loc = msg.location;
|
|
1682
|
+
const lineNum = loc ? String(loc.line) : "";
|
|
1683
|
+
const pad = loc ? " ".repeat(lineNum.length) : "";
|
|
1684
|
+
const locationLines = loc ? [
|
|
1685
|
+
` ${loc.file}:${loc.line}:${loc.column}`,
|
|
1686
|
+
` ${lineNum} \u2502 ${loc.lineText}`,
|
|
1687
|
+
` ${pad} \u2502 ${" ".repeat(loc.column)}${"~".repeat(Math.max(1, loc.length))}`
|
|
1688
|
+
] : [];
|
|
1689
|
+
const noteLines = msg.notes.filter((n) => n.text).map((n) => ` Note: ${n.text}`);
|
|
1690
|
+
return [`[${i + 1}] ${msg.text}`].concat(locationLines, noteLines).join("\n");
|
|
1691
|
+
}).join("\n\n");
|
|
1692
|
+
}
|
|
1693
|
+
const raw = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
1694
|
+
return raw.replace(/\x1b\[[0-9;]*[mGKH]/g, "").replace(/\r\n/g, "\n");
|
|
1695
|
+
}
|
|
1386
1696
|
async function buildTestBundle(config, cachedContent) {
|
|
1387
1697
|
const { projectRoot, output } = config;
|
|
1388
1698
|
const allTestFilePaths = Object.keys(config.fsTree);
|
|
@@ -1399,8 +1709,12 @@ async function buildTestBundle(config, cachedContent) {
|
|
|
1399
1709
|
contents: allTestFilePaths.map((filePath) => `import "${filePath}";`).join(""),
|
|
1400
1710
|
resolveDir: process.cwd()
|
|
1401
1711
|
},
|
|
1712
|
+
// Allow test files outside the project root (e.g. /tmp/my-test.ts) to import
|
|
1713
|
+
// packages from any node_modules on the ancestor chain of cwd — the same lookup
|
|
1714
|
+
// order Node itself uses when resolving require() from process.cwd().
|
|
1715
|
+
nodePaths: ancestorNodeModules(process.cwd()),
|
|
1402
1716
|
bundle: true,
|
|
1403
|
-
logLevel: "
|
|
1717
|
+
logLevel: "silent",
|
|
1404
1718
|
outfile,
|
|
1405
1719
|
keepNames: true,
|
|
1406
1720
|
legalComments: "none",
|
|
@@ -1412,26 +1726,47 @@ async function buildTestBundle(config, cachedContent) {
|
|
|
1412
1726
|
// all browsers and does not require changes to user test code.
|
|
1413
1727
|
footer: { js: 'window.dispatchEvent(new CustomEvent("qunitx:tests-ready"));' }
|
|
1414
1728
|
};
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1729
|
+
cachedContent._buildError = null;
|
|
1730
|
+
cachedContent._noTestsWarning = null;
|
|
1731
|
+
try {
|
|
1732
|
+
const [allTestCode] = await Promise.all([
|
|
1733
|
+
config.watch ? buildIncrementally(buildOptions, allTestFilePaths.join("\0"), cachedContent, needsDisk) : buildWithOverlayfsRetry(buildOptions, needsDisk),
|
|
1734
|
+
Promise.all(
|
|
1735
|
+
cachedContent.htmlPathsToRunTests.map(async (htmlPath) => {
|
|
1736
|
+
const targetPath = `${config.projectRoot}/${config.output}${htmlPath}`;
|
|
1737
|
+
if (htmlPath !== "/") {
|
|
1738
|
+
await fs9.rm(targetPath, { force: true, recursive: true });
|
|
1739
|
+
await fs9.mkdir(targetPath.split("/").slice(0, -1).join("/"), { recursive: true });
|
|
1740
|
+
}
|
|
1741
|
+
})
|
|
1742
|
+
)
|
|
1743
|
+
]);
|
|
1744
|
+
cachedContent.allTestCode = allTestCode;
|
|
1745
|
+
} catch (error) {
|
|
1746
|
+
cachedContent._buildError = {
|
|
1747
|
+
type: deriveBuildErrorType(error),
|
|
1748
|
+
formatted: formatBuildErrors(error)
|
|
1749
|
+
};
|
|
1750
|
+
await fs9.writeFile(
|
|
1751
|
+
`${projectRoot}/${output}/index.html`,
|
|
1752
|
+
buildErrorHTML(cachedContent._buildError)
|
|
1753
|
+
);
|
|
1754
|
+
throw error;
|
|
1755
|
+
}
|
|
1428
1756
|
}
|
|
1429
1757
|
async function runTestsInBrowser(config, cachedContent = {}, connections, targetTestFilesToFilter) {
|
|
1430
1758
|
const { projectRoot, output } = config;
|
|
1431
1759
|
const allTestFilePaths = Object.keys(config.fsTree);
|
|
1432
1760
|
const runHasFilter = !!targetTestFilesToFilter;
|
|
1433
1761
|
if (!config._groupMode) {
|
|
1434
|
-
config.COUNTER = {
|
|
1762
|
+
config.COUNTER = {
|
|
1763
|
+
testCount: 0,
|
|
1764
|
+
failCount: 0,
|
|
1765
|
+
skipCount: 0,
|
|
1766
|
+
todoCount: 0,
|
|
1767
|
+
passCount: 0,
|
|
1768
|
+
errorCount: 0
|
|
1769
|
+
};
|
|
1435
1770
|
}
|
|
1436
1771
|
config.lastRanTestFiles = targetTestFilesToFilter || allTestFilePaths;
|
|
1437
1772
|
try {
|
|
@@ -1463,6 +1798,20 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
1463
1798
|
}
|
|
1464
1799
|
const TIME_TAKEN = TIME_COUNTER.stop();
|
|
1465
1800
|
if (!config._groupMode) {
|
|
1801
|
+
if (config.COUNTER.testCount === 0 && !cachedContent._buildError) {
|
|
1802
|
+
const displayFiles = allTestFilePaths.map(
|
|
1803
|
+
(f) => f.startsWith(`${projectRoot}/`) ? f.slice(projectRoot.length + 1) : f
|
|
1804
|
+
);
|
|
1805
|
+
cachedContent._noTestsWarning = displayFiles;
|
|
1806
|
+
const fileWord = allTestFilePaths.length === 1 ? "file" : "files";
|
|
1807
|
+
console.log(
|
|
1808
|
+
`# Warning: 0 tests registered \u2014 no QUnit test cases found in ${allTestFilePaths.length} ${fileWord}`
|
|
1809
|
+
);
|
|
1810
|
+
fs9.writeFile(`${projectRoot}/${output}/index.html`, buildNoTestsHTML(displayFiles)).catch(
|
|
1811
|
+
() => {
|
|
1812
|
+
}
|
|
1813
|
+
);
|
|
1814
|
+
}
|
|
1466
1815
|
TAPDisplayFinalResult(config.COUNTER, TIME_TAKEN);
|
|
1467
1816
|
if (config.after) {
|
|
1468
1817
|
await runUserModule(`${process.cwd()}/${config.after}`, config.COUNTER, "after");
|
|
@@ -1478,8 +1827,18 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
1478
1827
|
}
|
|
1479
1828
|
} catch (error) {
|
|
1480
1829
|
config.lastFailedTestFiles = config.lastRanTestFiles;
|
|
1481
|
-
console.log(error);
|
|
1482
1830
|
const exception = new BundleError(error);
|
|
1831
|
+
if (!cachedContent._buildError && error.errors?.length) {
|
|
1832
|
+
cachedContent._buildError = {
|
|
1833
|
+
type: deriveBuildErrorType(error),
|
|
1834
|
+
formatted: formatBuildErrors(error)
|
|
1835
|
+
};
|
|
1836
|
+
fs9.writeFile(
|
|
1837
|
+
`${projectRoot}/${output}/qunitx.html`,
|
|
1838
|
+
buildErrorHTML(cachedContent._buildError)
|
|
1839
|
+
).catch(() => {
|
|
1840
|
+
});
|
|
1841
|
+
}
|
|
1483
1842
|
if (config.watch) {
|
|
1484
1843
|
console.log(`# ${exception}`);
|
|
1485
1844
|
} else {
|
|
@@ -1497,8 +1856,9 @@ function buildFilteredTests(filteredTests, outputPath, config) {
|
|
|
1497
1856
|
contents: filteredTests.map((filePath) => `import "${filePath}";`).join(""),
|
|
1498
1857
|
resolveDir: process.cwd()
|
|
1499
1858
|
},
|
|
1859
|
+
nodePaths: ancestorNodeModules(process.cwd()),
|
|
1500
1860
|
bundle: true,
|
|
1501
|
-
logLevel: "
|
|
1861
|
+
logLevel: "silent",
|
|
1502
1862
|
outfile: outputPath,
|
|
1503
1863
|
legalComments: "none",
|
|
1504
1864
|
target: esbuildTarget(config.browser),
|
|
@@ -1513,15 +1873,13 @@ async function runWithOverlayfsRetry(getContents, needsDisk) {
|
|
|
1513
1873
|
const MAX_RETRIES = 3;
|
|
1514
1874
|
const EMPTY_BUNDLE_THRESHOLD = 500;
|
|
1515
1875
|
let { result, js } = await getContents();
|
|
1876
|
+
const initialSize = js.length;
|
|
1516
1877
|
for (let retry = 1; retry <= MAX_RETRIES; retry++) {
|
|
1517
1878
|
if (js.length >= EMPTY_BUNDLE_THRESHOLD) break;
|
|
1518
|
-
console.log(
|
|
1519
|
-
`# [buildWithOverlayfsRetry] bundle is ${js.length} bytes (< ${EMPTY_BUNDLE_THRESHOLD}) on attempt ${retry}/${MAX_RETRIES} \u2014 overlayfs flush race, retrying in ${RETRY_DELAY_MS}ms`
|
|
1520
|
-
);
|
|
1521
1879
|
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
|
|
1522
1880
|
({ result, js } = await getContents());
|
|
1523
1881
|
}
|
|
1524
|
-
if (js.length < EMPTY_BUNDLE_THRESHOLD) {
|
|
1882
|
+
if (js.length < EMPTY_BUNDLE_THRESHOLD && js.length !== initialSize) {
|
|
1525
1883
|
console.log(
|
|
1526
1884
|
`# [buildWithOverlayfsRetry] bundle is ${js.length} bytes after ${MAX_RETRIES} retries \u2014 proceeding`
|
|
1527
1885
|
);
|
|
@@ -1612,13 +1970,15 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
|
|
|
1612
1970
|
config._resetTestTimeout = null;
|
|
1613
1971
|
config._testRunDone = null;
|
|
1614
1972
|
}
|
|
1615
|
-
if (!QUNIT_RESULT
|
|
1973
|
+
if (!QUNIT_RESULT) {
|
|
1616
1974
|
if (targetError) console.log(targetError);
|
|
1617
1975
|
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";
|
|
1618
1976
|
console.log(`# TIMEOUT: ${wsReason}`);
|
|
1619
1977
|
console.log("BROWSER: runtime error thrown during executing tests");
|
|
1620
1978
|
console.error("BROWSER: runtime error thrown during executing tests");
|
|
1621
1979
|
await failOnNonWatchMode(config.watch, { server, browser }, config._groupMode);
|
|
1980
|
+
} else if (QUNIT_RESULT.totalTests === 0) {
|
|
1981
|
+
return;
|
|
1622
1982
|
} else if (QUNIT_RESULT.totalTests > QUNIT_RESULT.finishedTests) {
|
|
1623
1983
|
if (targetError) console.log(targetError);
|
|
1624
1984
|
console.log(
|
|
@@ -1644,7 +2004,7 @@ async function failOnNonWatchMode(watchMode = false, connections = {}, groupMode
|
|
|
1644
2004
|
process.exit(1);
|
|
1645
2005
|
}
|
|
1646
2006
|
}
|
|
1647
|
-
var BundleError;
|
|
2007
|
+
var BundleError, ancestorNodeModules;
|
|
1648
2008
|
var init_tests_in_browser = __esm({
|
|
1649
2009
|
"lib/commands/run/tests-in-browser.ts"() {
|
|
1650
2010
|
init_color();
|
|
@@ -1652,6 +2012,7 @@ var init_tests_in_browser = __esm({
|
|
|
1652
2012
|
init_time_counter();
|
|
1653
2013
|
init_run_user_module();
|
|
1654
2014
|
init_display_final_result();
|
|
2015
|
+
init_web_server();
|
|
1655
2016
|
BundleError = class extends Error {
|
|
1656
2017
|
constructor(message) {
|
|
1657
2018
|
super(message);
|
|
@@ -1659,13 +2020,16 @@ var init_tests_in_browser = __esm({
|
|
|
1659
2020
|
this.message = `esbuild Bundle Error: ${message}`.split("\n").join("\n# ");
|
|
1660
2021
|
}
|
|
1661
2022
|
};
|
|
2023
|
+
ancestorNodeModules = (dir) => dir.split(path5.sep).map(
|
|
2024
|
+
(_, i, parts) => path5.join(parts.slice(0, parts.length - i).join(path5.sep) || path5.sep, "node_modules")
|
|
2025
|
+
);
|
|
1662
2026
|
}
|
|
1663
2027
|
});
|
|
1664
2028
|
|
|
1665
2029
|
// lib/setup/file-watcher.ts
|
|
1666
2030
|
import fs10 from "node:fs";
|
|
1667
2031
|
import { stat, lstat } from "node:fs/promises";
|
|
1668
|
-
import
|
|
2032
|
+
import path6 from "node:path";
|
|
1669
2033
|
function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFunc) {
|
|
1670
2034
|
const extensions = config.extensions || ["js", "ts"];
|
|
1671
2035
|
const readyPromises = [];
|
|
@@ -1695,7 +2059,7 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
1695
2059
|
const lastChangeMs = {};
|
|
1696
2060
|
const childWatcher = fs10.watch(watchPath, { recursive: true }, async (eventType, filename) => {
|
|
1697
2061
|
if (!ready || !filename) return;
|
|
1698
|
-
const fullPath =
|
|
2062
|
+
const fullPath = filename === path6.basename(watchPath) ? watchPath : path6.join(watchPath, filename);
|
|
1699
2063
|
if (eventType === "change") {
|
|
1700
2064
|
if (!config._building) {
|
|
1701
2065
|
const now = Date.now();
|
|
@@ -1720,8 +2084,8 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
1720
2084
|
}
|
|
1721
2085
|
handleWatchEvent(config, extensions, event, fullPath, onEventFunc, onFinishFunc);
|
|
1722
2086
|
});
|
|
1723
|
-
const parentDir =
|
|
1724
|
-
const watchedBasename =
|
|
2087
|
+
const parentDir = path6.dirname(watchPath);
|
|
2088
|
+
const watchedBasename = path6.basename(watchPath);
|
|
1725
2089
|
let parentUnlinkFired = false;
|
|
1726
2090
|
const parentWatcher = fs10.watch(parentDir, async (eventType, filename) => {
|
|
1727
2091
|
if (!ready || filename !== watchedBasename || eventType !== "rename") return;
|
|
@@ -1796,7 +2160,8 @@ function handleWatchEvent(config, extensions, event, filePath, onEventFunc, onFi
|
|
|
1796
2160
|
"#",
|
|
1797
2161
|
magenta().bold("==================================================================")
|
|
1798
2162
|
);
|
|
1799
|
-
|
|
2163
|
+
const displayPath = filePath.startsWith(config.projectRoot) ? filePath.slice(config.projectRoot.length) : filePath;
|
|
2164
|
+
console.log("#", colorEvent(event), displayPath);
|
|
1800
2165
|
console.log(
|
|
1801
2166
|
"#",
|
|
1802
2167
|
magenta().bold("==================================================================")
|
|
@@ -1823,13 +2188,13 @@ function handleWatchEvent(config, extensions, event, filePath, onEventFunc, onFi
|
|
|
1823
2188
|
}
|
|
1824
2189
|
});
|
|
1825
2190
|
}
|
|
1826
|
-
function mutateFSTree(fsTree, event,
|
|
2191
|
+
function mutateFSTree(fsTree, event, path7) {
|
|
1827
2192
|
if (event === "add") {
|
|
1828
|
-
fsTree[
|
|
2193
|
+
fsTree[path7] = null;
|
|
1829
2194
|
} else if (event === "unlink") {
|
|
1830
|
-
delete fsTree[
|
|
2195
|
+
delete fsTree[path7];
|
|
1831
2196
|
} else if (event === "unlinkDir") {
|
|
1832
|
-
const dirPrefix =
|
|
2197
|
+
const dirPrefix = path7.endsWith("/") ? path7 : path7 + "/";
|
|
1833
2198
|
for (const treePath of Object.keys(fsTree)) {
|
|
1834
2199
|
if (treePath.startsWith(dirPrefix)) delete fsTree[treePath];
|
|
1835
2200
|
}
|
|
@@ -1961,14 +2326,18 @@ import { availableParallelism } from "node:os";
|
|
|
1961
2326
|
async function run(config) {
|
|
1962
2327
|
const cachedContent = await buildCachedContent(config, config.htmlPaths);
|
|
1963
2328
|
if (config.watch) {
|
|
1964
|
-
|
|
2329
|
+
const preBuildPromise = buildTestBundle(config, cachedContent);
|
|
2330
|
+
preBuildPromise.catch(() => {
|
|
2331
|
+
});
|
|
2332
|
+
cachedContent._preBuildPromise = preBuildPromise;
|
|
1965
2333
|
const [connections] = await Promise.all([
|
|
1966
2334
|
setupBrowser(config, cachedContent),
|
|
1967
2335
|
writeOutputStaticFiles(config, cachedContent)
|
|
1968
2336
|
]);
|
|
1969
2337
|
config.expressApp = connections.server;
|
|
1970
2338
|
setupKeyboardEvents(config, cachedContent, connections);
|
|
1971
|
-
|
|
2339
|
+
const isHeadedWatchMode = config.open === true && config.watch;
|
|
2340
|
+
if (config.open && !isHeadedWatchMode) {
|
|
1972
2341
|
void openOutputInBrowser(config);
|
|
1973
2342
|
}
|
|
1974
2343
|
if (config.before) {
|
|
@@ -1983,6 +2352,10 @@ async function run(config) {
|
|
|
1983
2352
|
]);
|
|
1984
2353
|
throw error;
|
|
1985
2354
|
}
|
|
2355
|
+
if (isHeadedWatchMode && (cachedContent._buildError || cachedContent._noTestsWarning)) {
|
|
2356
|
+
await connections.page.goto(`http://localhost:${config.port}/`, { waitUntil: "commit", timeout: 5e3 }).catch(() => {
|
|
2357
|
+
});
|
|
2358
|
+
}
|
|
1986
2359
|
if (config.watch) {
|
|
1987
2360
|
const { ready: watcherReady } = setupFileWatchers(
|
|
1988
2361
|
config.testFileLookupPaths,
|
|
@@ -2006,7 +2379,13 @@ async function run(config) {
|
|
|
2006
2379
|
}
|
|
2007
2380
|
await runTestsInBrowser(config, cachedContent, connections, [file]);
|
|
2008
2381
|
},
|
|
2009
|
-
(_path, _event) =>
|
|
2382
|
+
async (_path, _event) => {
|
|
2383
|
+
connections.server.publish("refresh");
|
|
2384
|
+
if (isHeadedWatchMode && (cachedContent._buildError || cachedContent._noTestsWarning)) {
|
|
2385
|
+
await connections.page.goto(`http://localhost:${config.port}/`, { waitUntil: "commit", timeout: 5e3 }).catch(() => {
|
|
2386
|
+
});
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2010
2389
|
);
|
|
2011
2390
|
await watcherReady;
|
|
2012
2391
|
}
|
|
@@ -2015,7 +2394,14 @@ async function run(config) {
|
|
|
2015
2394
|
const allFiles = Object.keys(config.fsTree);
|
|
2016
2395
|
const groupCount = Math.min(allFiles.length, availableParallelism());
|
|
2017
2396
|
const groups = splitIntoGroups(allFiles, groupCount);
|
|
2018
|
-
config.COUNTER = {
|
|
2397
|
+
config.COUNTER = {
|
|
2398
|
+
testCount: 0,
|
|
2399
|
+
failCount: 0,
|
|
2400
|
+
skipCount: 0,
|
|
2401
|
+
todoCount: 0,
|
|
2402
|
+
passCount: 0,
|
|
2403
|
+
errorCount: 0
|
|
2404
|
+
};
|
|
2019
2405
|
config.lastRanTestFiles = allFiles;
|
|
2020
2406
|
const groupConfigs = groups.map((groupFiles, i) => ({
|
|
2021
2407
|
...config,
|
|
@@ -2026,9 +2412,10 @@ async function run(config) {
|
|
|
2026
2412
|
_phase: "bundling"
|
|
2027
2413
|
}));
|
|
2028
2414
|
const groupCachedContents = groups.map(() => ({ ...cachedContent }));
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
`# Running ${allFiles.length} test file${allFiles.length === 1 ? "" : "s"} across ${groupCount} group${groupCount === 1 ? "" : "s"}
|
|
2415
|
+
process.stdout.write("TAP version 13\n");
|
|
2416
|
+
process.stdout.write(
|
|
2417
|
+
`# Running ${allFiles.length} test file${allFiles.length === 1 ? "" : "s"} across ${groupCount} group${groupCount === 1 ? "" : "s"}
|
|
2418
|
+
`
|
|
2032
2419
|
);
|
|
2033
2420
|
const [browser] = await Promise.all([
|
|
2034
2421
|
launchBrowser(config),
|
|
@@ -2103,6 +2490,12 @@ async function run(config) {
|
|
|
2103
2490
|
config.COUNTER.failCount > 0 ? 1 : 0
|
|
2104
2491
|
);
|
|
2105
2492
|
process.exitCode = exitCode;
|
|
2493
|
+
if (config.COUNTER.testCount === 0 && exitCode === 0) {
|
|
2494
|
+
const fileWord = allFiles.length === 1 ? "file" : "files";
|
|
2495
|
+
console.log(
|
|
2496
|
+
`# Warning: 0 tests registered \u2014 no QUnit test cases found in ${allFiles.length} ${fileWord}`
|
|
2497
|
+
);
|
|
2498
|
+
}
|
|
2106
2499
|
TAPDisplayFinalResult(config.COUNTER, TIME_COUNTER.stop());
|
|
2107
2500
|
if (config.after) {
|
|
2108
2501
|
await runUserModule(`${process.cwd()}/${config.after}`, config.COUNTER, "after");
|
|
@@ -2226,7 +2619,7 @@ init_color();
|
|
|
2226
2619
|
var package_default = {
|
|
2227
2620
|
name: "qunitx-cli",
|
|
2228
2621
|
type: "module",
|
|
2229
|
-
version: "0.
|
|
2622
|
+
version: "0.19.0",
|
|
2230
2623
|
description: "Browser runner for QUnitx: run your qunitx tests in google-chrome",
|
|
2231
2624
|
author: "Izel Nakri",
|
|
2232
2625
|
license: "MIT",
|
|
@@ -2458,17 +2851,17 @@ function pathToModuleName(filePath) {
|
|
|
2458
2851
|
async function generateTestFiles() {
|
|
2459
2852
|
const projectRoot = await findProjectRoot();
|
|
2460
2853
|
const moduleName = pathToModuleName(process.argv[3]);
|
|
2461
|
-
const
|
|
2462
|
-
if (await pathExists(
|
|
2463
|
-
console.log(`${
|
|
2854
|
+
const path7 = process.argv[3].endsWith(".js") || process.argv[3].endsWith(".ts") ? `${projectRoot}/${process.argv[3]}` : `${projectRoot}/${process.argv[3]}.js`;
|
|
2855
|
+
if (await pathExists(path7)) {
|
|
2856
|
+
console.log(`${path7} already exists!`);
|
|
2464
2857
|
return;
|
|
2465
2858
|
}
|
|
2466
2859
|
const testJSContent = await readTemplate("test.js");
|
|
2467
|
-
const targetFolderPaths =
|
|
2860
|
+
const targetFolderPaths = path7.split("/");
|
|
2468
2861
|
targetFolderPaths.pop();
|
|
2469
2862
|
await fs5.mkdir(targetFolderPaths.join("/"), { recursive: true });
|
|
2470
|
-
await fs5.writeFile(
|
|
2471
|
-
console.log(green(`${
|
|
2863
|
+
await fs5.writeFile(path7, testJSContent.replace("{{moduleName}}", moduleName));
|
|
2864
|
+
console.log(green(`${path7} written`));
|
|
2472
2865
|
}
|
|
2473
2866
|
|
|
2474
2867
|
// lib/setup/config.ts
|
|
@@ -2569,20 +2962,20 @@ function setupTestFilePaths(_projectRoot, inputs2) {
|
|
|
2569
2962
|
});
|
|
2570
2963
|
return result.map((metaItem) => metaItem.input);
|
|
2571
2964
|
}
|
|
2572
|
-
function pathIsFile(
|
|
2573
|
-
const inputs2 =
|
|
2965
|
+
function pathIsFile(path7) {
|
|
2966
|
+
const inputs2 = path7.split("/");
|
|
2574
2967
|
return inputs2[inputs2.length - 1].includes(".");
|
|
2575
2968
|
}
|
|
2576
2969
|
function pathIsIncludedInPaths(paths, targetPath) {
|
|
2577
|
-
return paths.some((
|
|
2578
|
-
if (
|
|
2970
|
+
return paths.some((path7) => {
|
|
2971
|
+
if (path7 === targetPath) {
|
|
2579
2972
|
return false;
|
|
2580
2973
|
}
|
|
2581
|
-
return matchesGlob(targetPath.input, buildGlobFormat(
|
|
2974
|
+
return matchesGlob(targetPath.input, buildGlobFormat(path7));
|
|
2582
2975
|
});
|
|
2583
2976
|
}
|
|
2584
|
-
function buildGlobFormat(
|
|
2585
|
-
return
|
|
2977
|
+
function buildGlobFormat(path7) {
|
|
2978
|
+
return path7.isFile ? path7.input : `${path7.input}/**`;
|
|
2586
2979
|
}
|
|
2587
2980
|
|
|
2588
2981
|
// lib/utils/parse-cli-flags.ts
|
|
@@ -2636,7 +3029,9 @@ function parseCliFlags(projectRoot) {
|
|
|
2636
3029
|
console.warn(`# Warning: Unknown flag "${arg}" \u2014 ignored`);
|
|
2637
3030
|
return result;
|
|
2638
3031
|
}
|
|
2639
|
-
result.inputs.add(
|
|
3032
|
+
result.inputs.add(
|
|
3033
|
+
arg.startsWith(projectRoot) || arg.startsWith("/") ? arg : `${process.cwd()}/${arg}`
|
|
3034
|
+
);
|
|
2640
3035
|
return result;
|
|
2641
3036
|
},
|
|
2642
3037
|
{ inputs: /* @__PURE__ */ new Set([]) }
|
|
@@ -2674,7 +3069,14 @@ async function setupConfig() {
|
|
|
2674
3069
|
testFileLookupPaths: setupTestFilePaths(projectRoot, inputs2),
|
|
2675
3070
|
lastFailedTestFiles: null,
|
|
2676
3071
|
lastRanTestFiles: null,
|
|
2677
|
-
COUNTER: {
|
|
3072
|
+
COUNTER: {
|
|
3073
|
+
testCount: 0,
|
|
3074
|
+
failCount: 0,
|
|
3075
|
+
skipCount: 0,
|
|
3076
|
+
todoCount: 0,
|
|
3077
|
+
passCount: 0,
|
|
3078
|
+
errorCount: 0
|
|
3079
|
+
},
|
|
2678
3080
|
_testRunDone: null,
|
|
2679
3081
|
_resetTestTimeout: null,
|
|
2680
3082
|
_onWsOpen: null,
|